package managed_resources //nolint:staticcheck

import (
	"bytes"
	"encoding/json"
	"fmt"
	"regexp"
	"text/template"

	"buf.build/go/protovalidate"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"sigs.k8s.io/yaml"
)

var (
	envInfoKeyRegexStr = regexp.QuoteMeta(api.AgentKeyPrefix) + `/env-.+-\d+-\d+`
	envInfoKeyRegex    = regexp.MustCompile(fmt.Sprintf(`^%s$`, envInfoKeyRegexStr))

	envAgentIDRegexStr   = regexp.QuoteMeta(api.AgentIDKey) + `-\d+`
	envProjectIDRegexStr = regexp.QuoteMeta(api.ProjectIDKey) + `-\d+`
	envSlugRegexStr      = regexp.QuoteMeta(api.EnvironmentSlugKey) + `-.+`

	envLabelKeyRegex = regexp.MustCompile(fmt.Sprintf(
		`^(?:%s|%s|%s|%s)$`,
		envInfoKeyRegexStr,
		envAgentIDRegexStr,
		envProjectIDRegexStr,
		envSlugRegexStr),
	)
)

type Renderer struct {
	Mapper    meta.RESTMapper
	Validator protovalidate.Validator
}

// RenderEnvironmentTemplate renders a template.
// All returned errors are an instance of RendererError.
func (r *Renderer) RenderEnvironmentTemplate(tpl *EnvironmentTemplate, info *TemplatingInfo,
	processRenderedObj func(*unstructured.Unstructured) error) (*RenderedEnvironmentTemplate, error) {

	// 1. Unmarshal
	var raw EnvironmentTemplateData[json.RawMessage]
	err := yaml.UnmarshalStrict(tpl.Data, &raw)
	if err != nil {
		return nil, NewErrorf(InvalidArgument, "Invalid template: %v", err)
	}

	// 2. Render - templating
	vars := info2vars(info)
	renderedObjs := make([]unstructured.Unstructured, 0, len(raw.Objects))
	for i, o := range raw.Objects {
		renderedObject, rendErr := renderObject(o, vars)
		if rendErr != nil {
			return nil, NewErrorf(InvalidArgument, "Invalid template in object[%d]: %v", i, rendErr)
		}
		renderedObjs = append(renderedObjs, renderedObject)
	}

	// 3. Render - process objects
	err = processObjects(renderedObjs, tpl, info, processRenderedObj)
	if err != nil {
		return nil, NewErrorf(InvalidArgument, "Invalid template: processing objects: %v", err)
	}

	// 4. Return result
	return renderedObjs2renderedTemplate(tpl.Name, renderedObjs, raw.EnvironmentTemplateDataFields)
}

func (r *Renderer) ExtractEnvironmentInfosFromObject(obj *unstructured.Unstructured) ([]*EnvironmentInfo, error) {
	var res []*EnvironmentInfo //nolint:prealloc
	for key, val := range obj.GetAnnotations() {
		if !envInfoKeyRegex.MatchString(key) {
			continue
		}
		var info EnvironmentInfo
		err := json.Unmarshal([]byte(val), &info)
		if err != nil {
			return nil, err
		}
		err = r.Validator.Validate(&info)
		if err != nil {
			return nil, err
		}
		res = append(res, &info)
	}
	return res, nil
}

// RemoveEnvironmentInfosOnObject removes existing environment info annotations and labels.
func (r *Renderer) RemoveEnvironmentInfosOnObject(obj *unstructured.Unstructured) {
	removeEnvironmentInfosAnnotationOnObject(obj)
	removeEnvironmentInfosLabelsOnObject(obj)
}

func (r *Renderer) SetEnvironmentInfosOnObject(obj *unstructured.Unstructured, infos []*EnvironmentInfo) error {
	for _, envInfo := range infos {
		err := setEnvironmentInfoOnObjects([]unstructured.Unstructured{*obj}, envInfo)
		if err != nil {
			return err
		}
	}

	return nil
}

func removeEnvironmentInfosAnnotationOnObject(obj *unstructured.Unstructured) {
	a := obj.GetAnnotations()
	if len(a) == 0 { // handles nil and empty
		return
	}
	for key := range a {
		if envInfoKeyRegex.MatchString(key) {
			delete(a, key)
		}
	}
	obj.SetAnnotations(a)
}

func removeEnvironmentInfosLabelsOnObject(obj *unstructured.Unstructured) {
	l := obj.GetLabels()
	if len(l) == 0 { // handles nil and empty
		return
	}
	for key := range l {
		if envLabelKeyRegex.MatchString(key) {
			delete(l, key)
		}
	}
	obj.SetLabels(l)
}

func renderedObjs2renderedTemplate(name string, renderedObjs []unstructured.Unstructured, fields EnvironmentTemplateDataFields) (*RenderedEnvironmentTemplate, error) {
	outObjs := make([]map[string]any, 0, len(renderedObjs))
	for _, obj := range renderedObjs {
		outObjs = append(outObjs, obj.Object)
	}
	data, err := yaml.Marshal(&EnvironmentTemplateData[map[string]any]{
		Objects:                       outObjs,
		EnvironmentTemplateDataFields: fields,
	})
	if err != nil {
		return nil, NewErrorf(Internal, "YAML marshal: %v", err)
	}
	return &RenderedEnvironmentTemplate{
		Name: name,
		Data: data,
	}, nil
}

func setEnvironmentInfoOnObjects(objs []unstructured.Unstructured, info *EnvironmentInfo) error {
	// 1. Prepare labels and annotations
	envInfoAnnValBytes, err := json.Marshal(info)
	if err != nil {
		return err
	}
	envInfoAnnVal := string(envInfoAnnValBytes)

	envInfoKey := fmt.Sprintf("%s/env-%s-%d-%d",
		api.AgentKeyPrefix,
		info.EnvironmentSlug,
		info.ProjectId,
		info.AgentId,
	)
	labelKeys := []string{
		envInfoKey,
		fmt.Sprintf("%s-%d", api.AgentIDKey, info.AgentId),
		fmt.Sprintf("%s-%d", api.ProjectIDKey, info.ProjectId),
		fmt.Sprintf("%s-%s", api.EnvironmentSlugKey, info.EnvironmentSlug),
	}

	for i := range objs {
		obj := &objs[i]

		// 2. Set environment info annotation and various labels
		setEnvInfoAnnotation(obj, envInfoKey, envInfoAnnVal)
		setEnvInfoLabels(obj, labelKeys)
	}
	return nil
}

func processObjects(objs []unstructured.Unstructured, tpl *EnvironmentTemplate, info *TemplatingInfo,
	processRenderedObj func(*unstructured.Unstructured) error) error {

	err := setEnvironmentInfoOnObjects(objs, info.ToEnvironmentInfo(tpl.Name))
	if err != nil {
		return err
	}

	for i := range objs {
		err = processRenderedObj(&objs[i])
		if err != nil {
			return err
		}
	}
	return nil
}

func setEnvInfoAnnotation(obj *unstructured.Unstructured, envInfoKey, envInfoAnnVal string) {
	a := obj.GetAnnotations()
	if a == nil {
		a = map[string]string{}
	}
	a[envInfoKey] = envInfoAnnVal
	obj.SetAnnotations(a)
}

func setEnvInfoLabels(obj *unstructured.Unstructured, labelKeys []string) {
	l := obj.GetLabels()
	if l == nil {
		l = map[string]string{}
	}
	for _, k := range labelKeys {
		l[k] = ""
	}
	obj.SetLabels(l)
}

func info2vars(info *TemplatingInfo) map[string]any {
	return map[string]any{
		"agent": map[string]any{
			"id":   info.Agent.Id,
			"name": info.Agent.Name,
			"url":  info.Agent.Url,
		},
		"environment": map[string]any{
			"id":       info.Environment.Id,
			"name":     info.Environment.Name,
			"slug":     info.Environment.Slug,
			"url":      info.Environment.GetUrl(), // empty string if not provided
			"page_url": info.Environment.PageUrl,
			"tier":     info.Environment.Tier,
		},
		"project": map[string]any{
			"id":   info.Project.Id,
			"slug": info.Project.Slug,
			"path": info.Project.Path,
			"url":  info.Project.Url,
		},
		"ci_pipeline": map[string]any{
			"id": info.Pipeline.Id,
		},
		"ci_job": map[string]any{
			"id": info.Job.Id,
		},
		"user": map[string]any{
			"id":       info.User.Id,
			"username": info.User.Username,
		},
	}
}

func renderObject(src []byte, vars map[string]any) (unstructured.Unstructured, error) {
	var buf bytes.Buffer
	t := template.New("object").Option("missingkey=error")
	t, err := t.Parse(string(src))
	if err != nil {
		return unstructured.Unstructured{}, fmt.Errorf("parse: %w", err)
	}
	err = t.Execute(&buf, vars)
	if err != nil {
		return unstructured.Unstructured{}, fmt.Errorf("execute: %w", err)
	}
	var renderedObject map[string]any
	err = yaml.UnmarshalStrict(buf.Bytes(), &renderedObject)
	if err != nil {
		return unstructured.Unstructured{}, fmt.Errorf("resulting YAML is invalid: %w", err)
	}
	return unstructured.Unstructured{Object: renderedObject}, nil
}

type EnvironmentTemplateData[O any] struct {
	Objects []O `json:"objects"`
	EnvironmentTemplateDataFields
}

// EnvironmentTemplateDataFields holds all fields except objects.
// This simplifies copying of these fields.
type EnvironmentTemplateDataFields struct {
	ApplyResources  string `json:"apply_resources"`
	DeleteResources string `json:"delete_resources"`
}
