package watch_graph //nolint:staticcheck

import (
	"bytes"
	"compress/gzip"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"
	"unsafe"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/ioz"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/cli-runtime/pkg/resource"
)

func (vx *wgObject2vertex) collectHelmReleaseFromSecret(obj *unstructured.Unstructured) *Error {
	return vx.collectHelmRelease(obj, true)
}

func (vx *wgObject2vertex) collectHelmReleaseFromConfigMap(obj *unstructured.Unstructured) *Error {
	return vx.collectHelmRelease(obj, false)
}

func (vx *wgObject2vertex) collectHelmRelease(obj *unstructured.Unstructured, isSecret bool) *Error {
	if !vx.isHelmReleaseObject(obj, isSecret) {
		return nil
	}
	releaseData, found, err := unstructured.NestedString(obj.Object, "data", "release")
	if err != nil {
		vx.appendObjectProcessingWarning(err.Error())
		return nil
	}
	if !found {
		return nil
	}
	release, err := decodeRelease(releaseData, isSecret)
	if err != nil {
		vx.appendObjectProcessingWarning(fmt.Sprintf("error decoding Helm release: %v", err))
		return nil
	}
	manifest := release.Manifest
	release.Manifest = "" // do not send it to the caller, it might contain secrets or be too big.
	rawJSON, err := json.Marshal(release)
	if err != nil {
		vx.appendObjectProcessingWarning(fmt.Sprintf("error encoding Helm release: %v", err))
		return nil
	}
	vx.vd.HelmRelease = rawJSON

	err = vx.newBuilder().
		ContinueOnError().
		NamespaceParam(vx.ns).
		DefaultNamespace().
		Flatten().
		Local().
		Unstructured().
		Stream(strings.NewReader(manifest), "").
		Do().
		Visit(func(info *resource.Info, err error) error {
			if err != nil {
				vx.appendObjectProcessingWarning(err.Error())
				return nil
			}
			gk := info.Object.GetObjectKind().GroupVersionKind().GroupKind()
			vx.setArcToGKNs(gk, info.Name, info.Namespace, ArcAttrs{}, ReferenceArcType)
			return nil
		})
	if err != nil {
		vx.appendObjectProcessingWarning(err.Error())
	}
	return nil
}

func (vx *wgObject2vertex) isHelmReleaseObject(obj *unstructured.Unstructured, isSecret bool) bool {
	if isSecret {
		secretType, _, err := unstructured.NestedString(obj.Object, "type")
		if err != nil {
			vx.appendObjectProcessingWarning(err.Error())
			return false
		}
		if secretType != "helm.sh/release.v1" { //nolint: gosec
			return false
		}
	}
	lbl := obj.GetLabels()
	return lbl["name"] != "" && lbl["owner"] == "helm"
}

// The rest of the code in this file was vendored from Helm and heavily modified to suit our needs.
// Sources:
// - https://github.com/helm/helm/blob/934f761e08988df730c786ba82831457ced11de6/pkg/storage/driver/util.go
// - https://github.com/helm/helm/blob/934f761e08988df730c786ba82831457ced11de6/pkg/release/v1/release.go
// - https://github.com/helm/helm/blob/934f761e08988df730c786ba82831457ced11de6/pkg/release/v1/info.go

/*
Copyright The Helm Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// helmRelease is a stripped down version of Helm's release.Release type.
// We only want to unmarshal what we want to return, discard everything else.
// We don't vendor this type as-is because we want to use json.RawMessage as some fields to avoid
// unmarshaling and marshaling them from and to JSON.
type helmRelease struct {
	// Name is the name of the release
	Name json.RawMessage `json:"name,omitempty"`
	// Info provides information about a release
	Info *helmInfo `json:"info,omitempty"`
	// Chart is the chart that was released.
	Chart *helmChart `json:"chart,omitempty"`
	// Manifest is the string representation of the rendered template.
	Manifest string `json:"manifest,omitempty"`
	// Hooks are all of the hooks declared for this release.
	Hooks json.RawMessage `json:"hooks,omitempty"`
	// Version is an int which represents the revision of the release.
	Version json.RawMessage `json:"version,omitempty"`
	// Namespace is the kubernetes namespace of the release.
	Namespace json.RawMessage `json:"namespace,omitempty"`
}

type helmChart struct {
	// Metadata is the contents of the Chartfile.
	Metadata json.RawMessage `json:"metadata"`
	// Lock is the contents of Chart.lock.
	Lock json.RawMessage `json:"lock"`
}

type helmInfo struct {
	// FirstDeployed is when the release was first deployed.
	FirstDeployed json.RawMessage `json:"first_deployed,omitempty"`
	// LastDeployed is when the release was last deployed.
	LastDeployed json.RawMessage `json:"last_deployed,omitempty"`
	// Deleted tracks when this object was deleted.
	Deleted json.RawMessage `json:"deleted"`
	// Description is human-friendly "log entry" about this release.
	Description json.RawMessage `json:"description,omitempty"`
	// Status is the current state of the release
	Status json.RawMessage `json:"status,omitempty"`
}

var magicGzip = []byte{0x1f, 0x8b, 0x08}

// decodeRelease decodes the bytes of data into a release
// type. Data must contain a base64 encoded gzipped string of a
// valid release, otherwise an error is returned.
func decodeRelease(data string, isSecret bool) (*helmRelease, error) {
	dataBytes := unsafe.Slice(unsafe.StringData(data), len(data)) //nolint: gosec

	// If the data is coming from a Secret, we need to base64 decode it first.
	if isSecret {
		var err error
		dataBytes, err = base64.StdEncoding.AppendDecode(nil, dataBytes)
		if err != nil {
			return nil, err
		}
	}

	// Decode (again)
	dataBytes, err := base64.StdEncoding.AppendDecode(nil, dataBytes)
	if err != nil {
		return nil, err
	}

	// Unmarshal release object bytes
	var hr helmRelease

	// For backwards compatibility with releases that were stored before
	// compression was introduced we skip decompression if the
	// gzip magic header is not found
	if len(dataBytes) > 3 && bytes.Equal(dataBytes[0:3], magicGzip) {
		var gr *gzip.Reader
		gr, err = gzip.NewReader(bytes.NewReader(dataBytes))
		if err != nil {
			return nil, err
		}
		err = ioz.ReadAllFunc(gr, func(data []byte) error {
			return json.Unmarshal(data, &hr)
		})
		if err != nil {
			return nil, err
		}
		err = gr.Close()
		if err != nil {
			return nil, err
		}
	} else {
		err = json.Unmarshal(dataBytes, &hr)
		if err != nil {
			return nil, err
		}
	}
	return &hr, nil
}
