package dsl

import (
	"errors"
	"regexp"
	"sort"
	"strings"
)

var (
	errEnvValueNotString        = errors.New("automation env value must be a string")
	errEnvExpressionUnsupported = errors.New("automation env value must be a literal string or full secrets/inputs expression")
	envReferenceNamePatternText = `[A-Za-z_][A-Za-z0-9_]*`
	envReferenceNamePattern     = regexp.MustCompile(`^` + envReferenceNamePatternText + `$`)
	envExpressionPattern        = regexp.MustCompile(`^\$\{\{\s*(secrets|inputs)\.(` + envReferenceNamePatternText + `)\s*\}\}$`)
)

// EnvValueKind classifies a supported automation env value.
type EnvValueKind string

const (
	EnvValueLiteral EnvValueKind = "literal"
	EnvValueSecret  EnvValueKind = "secret"
	EnvValueInput   EnvValueKind = "input"
)

// EnvValue is the parsed representation of a supported automation env value.
type EnvValue struct {
	Kind  EnvValueKind
	Value string
	Name  string
}

// EnvReferences summarizes supported env expressions used by a job.
type EnvReferences struct {
	SecretNames []string
	UsesInputs  bool
}

// ParseEnvValue parses the supported automation env value subset.
func ParseEnvValue(raw any) (EnvValue, error) {
	text, ok := raw.(string)
	if !ok {
		return EnvValue{}, errEnvValueNotString
	}
	trimmed := strings.TrimSpace(text)
	if containsExpressionDelimiter(trimmed) {
		matches := envExpressionPattern.FindStringSubmatch(trimmed)
		if len(matches) != 3 {
			return EnvValue{}, errEnvExpressionUnsupported
		}
		switch matches[1] {
		case "secrets":
			return EnvValue{Kind: EnvValueSecret, Name: matches[2]}, nil
		case "inputs":
			return EnvValue{Kind: EnvValueInput, Name: matches[2]}, nil
		default:
			return EnvValue{}, errEnvExpressionUnsupported
		}
	}
	return EnvValue{Kind: EnvValueLiteral, Value: text}, nil
}

// ValidEnvReferenceName reports whether name can be referenced from safe env
// expressions such as `${{ secrets.NAME }}` or `${{ inputs.NAME }}`.
func ValidEnvReferenceName(name string) bool {
	return envReferenceNamePattern.MatchString(name)
}

// SecretReferences returns sorted secret names referenced from effective job
// and step env values after automation env inheritance.
func SecretReferences(spec DefinitionSpec) ([]string, error) {
	collector := newEnvReferenceCollector()
	for _, jobID := range sortedJobIDs(spec.Jobs) {
		if err := collector.collectJob(JobWithInheritedEnv(spec.Env, spec.Jobs[jobID])); err != nil {
			return nil, err
		}
	}
	return collector.references().SecretNames, nil
}

// JobEnvReferences returns sorted secret names and whether inputs are referenced
// from job and step env values.
func JobEnvReferences(job JobSpec) (EnvReferences, error) {
	collector := newEnvReferenceCollector()
	if err := collector.collectJob(job); err != nil {
		return EnvReferences{}, err
	}
	return collector.references(), nil
}

// JobWithInheritedEnv returns a job copy with automation env overridden by job env.
func JobWithInheritedEnv(automationEnv map[string]any, job JobSpec) JobSpec {
	if len(automationEnv) == 0 {
		return job
	}
	merged := make(map[string]any, len(automationEnv)+len(job.Env))
	for key, value := range automationEnv {
		merged[key] = value
	}
	for key, value := range job.Env {
		merged[key] = value
	}
	job.Env = merged
	return job
}

type envReferenceCollector struct {
	secretNames map[string]struct{}
	usesInputs  bool
}

func newEnvReferenceCollector() *envReferenceCollector {
	return &envReferenceCollector{secretNames: map[string]struct{}{}}
}

func (c *envReferenceCollector) collectJob(job JobSpec) error {
	if err := c.collectEnv(job.Env); err != nil {
		return err
	}
	for _, step := range job.Steps {
		if err := c.collectEnv(step.Env); err != nil {
			return err
		}
	}
	return nil
}

func (c *envReferenceCollector) collectEnv(env map[string]any) error {
	for _, raw := range env {
		value, err := ParseEnvValue(raw)
		if err != nil {
			return err
		}
		switch value.Kind {
		case EnvValueSecret:
			c.secretNames[value.Name] = struct{}{}
		case EnvValueInput:
			c.usesInputs = true
		}
	}
	return nil
}

func (c *envReferenceCollector) references() EnvReferences {
	if len(c.secretNames) == 0 {
		return EnvReferences{UsesInputs: c.usesInputs}
	}
	names := make([]string, 0, len(c.secretNames))
	for name := range c.secretNames {
		names = append(names, name)
	}
	sort.Strings(names)
	return EnvReferences{SecretNames: names, UsesInputs: c.usesInputs}
}
