package tpl

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

	"github.com/OliveTin/OliveTin/internal/entities"
	"github.com/OliveTin/OliveTin/internal/env"
	"github.com/OliveTin/OliveTin/internal/installationinfo"
	log "github.com/sirupsen/logrus"
)

func jsonFunc(v any) (string, error) {
	if v == nil {
		return "null", nil
	}
	data, err := json.Marshal(v)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

var tpl = template.New("tpl").
	Option("missingkey=error").
	Funcs(template.FuncMap{"Json": jsonFunc})

type olivetinInfo struct {
	Build   *installationinfo.BuildInfo
	Runtime *installationinfo.RuntimeInfo
}

var legacyArgumentRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\s*}}`)
var legacyEntityPropertiesRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+)\s*}}`)

type generalTemplateContext struct {
	OliveTin olivetinInfo
	Env      map[string]string
}

type actionTemplateContext struct {
	CurrentEntity interface{}
	Arguments     map[string]string

	// These are deliberately repeated because embedding structs
	// won't work in text/template.
	OliveTin olivetinInfo
	Env      map[string]string
}

var (
	cachedOliveTinInfo olivetinInfo
	cachedEnvMap       map[string]string
)

func init() {
	cachedOliveTinInfo = olivetinInfo{
		Build:   installationinfo.Build,
		Runtime: installationinfo.Runtime,
	}

	cachedEnvMap = env.BuildEnvMap()
}

func GetNewGeneralTemplateContext() *generalTemplateContext {
	return &generalTemplateContext{
		OliveTin: cachedOliveTinInfo,
		Env:      cachedEnvMap,
	}
}

func migrateLegacyEntityProperties(rawShellCommand string) string {
	foundArgumentNames := legacyEntityPropertiesRegex.FindAllStringSubmatch(rawShellCommand, -1)

	for _, match := range foundArgumentNames {
		entityName := match[1]
		argName := match[2]
		fullMatch := match[0] // The entire matched string like "{{ server.hostname }}"

		if strings.Contains(argName, ".") {
			replacement := "{{ .CurrentEntity." + argName + " }}"

			rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)

			log.WithFields(log.Fields{
				"old": entityName,
				"new": ".CurrentEntity",
			}).Debugf("Legacy entity variable name found, changing to CurrentEntity")
			continue
		}

		if !strings.HasPrefix(argName, ".Arguments.") {
			replacement := "{{ .CurrentEntity." + argName + " }}"

			rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)

			log.WithFields(log.Fields{
				"old": argName,
				"new": ".CurrentEntity." + argName,
			}).Debugf("Legacy variable name found, changing to CurrentEntity")
		}
	}

	return rawShellCommand
}

func migrateLegacyArgumentNames(rawShellCommand string) string {
	matches := legacyArgumentRegex.FindAllStringSubmatchIndex(rawShellCommand, -1)

	for i := len(matches) - 1; i >= 0; i-- {
		match := matches[i]
		fullMatchStart := match[0]
		fullMatchEnd := match[1]
		argNameStart := match[2]
		argNameEnd := match[3]

		argName := rawShellCommand[argNameStart:argNameEnd]

		log.WithFields(log.Fields{
			"old": argName,
			"new": ".Arguments." + argName,
		}).Debugf("Legacy variable name found, changing to Argument")

		replacement := "{{ .Arguments." + argName + " }}"
		rawShellCommand = rawShellCommand[:fullMatchStart] + replacement + rawShellCommand[fullMatchEnd:]
	}

	return rawShellCommand
}

func ParseTemplateWithActionContext(source string, ent *entities.Entity, args map[string]string) (string, error) {
	source = migrateLegacyArgumentNames(source)
	source = migrateLegacyEntityProperties(source)

	var entdata any

	if ent != nil {
		entdata = ent.Data
	}

	templateVariables := &actionTemplateContext{
		OliveTin: cachedOliveTinInfo,
		Env:      cachedEnvMap,

		Arguments:     args,
		CurrentEntity: entdata,
	}

	result, err := parseTemplate(source, templateVariables)

	if isMissingArgumentError, argName := checkMissingArgumentError(err); isMissingArgumentError {
		return "", fmt.Errorf("required arg not provided: %s", argName)
	}

	if err != nil {
		return "", err
	}

	return result, nil
}

func checkMissingArgumentError(err error) (bool, string) {
	if err == nil {
		return false, ""
	}

	if strings.Contains(err.Error(), "map has no entry for key") {
		re := regexp.MustCompile(`\.Arguments\.(\w+)`)
		match := re.FindStringSubmatch(err.Error())
		if len(match) > 1 {
			return true, match[1]
		}
	}

	return false, ""
}

func parseTemplate(source string, data any) (string, error) {
	t, err := tpl.Parse(source)

	if err != nil {
		return "", err
	}

	var sb strings.Builder
	err = t.Execute(&sb, data)

	if err != nil {
		log.WithFields(log.Fields{
			"source": source,
			"err":    err,
		}).Errorf("Error executing template")

		return "", err
	} else {
		return sb.String(), nil
	}
}

const maxExecToolConfigDepth = 64

// ApplyTemplatesToExecToolConfig recursively applies action templates to all string values in config.
// Depth-first over maps and slices; only string values are templated. Returns an error if any template fails.
func ApplyTemplatesToExecToolConfig(config map[string]any, ent *entities.Entity, args map[string]string) (map[string]any, error) {
	out, err := applyTemplatesToValue(config, ent, args, 0)
	if err != nil {
		return nil, err
	}
	if out == nil {
		return nil, nil
	}
	return out.(map[string]any), nil
}

func applyTemplatesToValue(v any, ent *entities.Entity, args map[string]string, depth int) (any, error) {
	if depth > maxExecToolConfigDepth {
		return nil, fmt.Errorf("execTool config nested too deeply")
	}
	if s, ok := v.(string); ok {
		return ParseTemplateWithActionContext(s, ent, args)
	}
	return applyTemplatesToComposite(v, ent, args, depth)
}

func applyTemplatesToComposite(v any, ent *entities.Entity, args map[string]string, depth int) (any, error) {
	if m, ok := v.(map[string]any); ok {
		return applyTemplatesToMap(m, ent, args, depth)
	}
	if items, ok := v.([]any); ok {
		return applyTemplatesToSliceAny(items, ent, args, depth)
	}
	if strItems, ok := v.([]string); ok {
		return applyTemplatesToSliceAny(stringSliceAsAnySlice(strItems), ent, args, depth)
	}
	return v, nil
}

func stringSliceAsAnySlice(val []string) []any {
	out := make([]any, len(val))
	for i, s := range val {
		out[i] = s
	}
	return out
}

func applyTemplatesToMap(val map[string]any, ent *entities.Entity, args map[string]string, depth int) (map[string]any, error) {
	result := make(map[string]any, len(val))
	for k, elem := range val {
		transformed, err := applyTemplatesToValue(elem, ent, args, depth+1)
		if err != nil {
			return nil, err
		}
		result[k] = transformed
	}
	return result, nil
}

func applyTemplatesToSliceAny(val []any, ent *entities.Entity, args map[string]string, depth int) ([]any, error) {
	result := make([]any, len(val))
	for i, elem := range val {
		transformed, err := applyTemplatesToValue(elem, ent, args, depth+1)
		if err != nil {
			return nil, err
		}
		result[i] = transformed
	}
	return result, nil
}

func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string {
	result, err := ParseTemplateWithActionContext(source, ent, nil)
	if err != nil {
		log.WithFields(log.Fields{
			"source": source,
			"err":    err,
		}).Errorf("Error parsing template of action before exec")
		return ""
	}
	return result
}

/*
func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
	source = strings.TrimSpace(source)

	tplBool := ParseTemplateOfActionBeforeExec(source, ent)

	return tplBool == "true"
}
*/
