package dsl

import (
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strings"
)

var (
	identifierPattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$`)
)

const (
	// RunnerGroupMaxLength is the persisted queue runner-group key budget.
	RunnerGroupMaxLength = 128
	// RunnerLabelKeyMaxLength is the persisted queue runner-label key budget.
	RunnerLabelKeyMaxLength = 512
)

var supportedAgentTeamUses = map[string]struct{}{
	"agentteam/agent-task":  {},
	"agentteam/wait-event":  {},
	AgentTeamCheckoutUse:    {},
	AgentTeamPublishWikiUse: {},
	AgentTeamPublishTreeUse: {},
}

var agentteamWaitUses = map[string]struct{}{
	"agentteam/agent-task": {},
	"agentteam/wait-event": {},
}

// removedAgentTeamUses are AgentTeam-native step uses that have been removed from the engine.
// They are listed separately so validation can return a precise error message.
var removedAgentTeamUses = map[string]string{
	"agentteam/human-approval": "agentteam/human-approval has been removed",
}

func ValidateDefinition(spec DefinitionSpec) ValidationReport {
	return ValidateDefinitionWithLimits(spec, ValidationLimits{})
}

// ValidationLimits bounds the size of an authored Automation definition during
// validation. Non-positive fields leave the corresponding authoring limit
// disabled; these values do not configure runner concurrency or Workflow run
// budgets.
type ValidationLimits struct {
	MaxJobs          int
	MaxStepsPerJob   int
	MaxRunnerLabels  int
	MaxJobInputBytes int
}

func ValidateDefinitionWithLimits(spec DefinitionSpec, limits ValidationLimits) ValidationReport {
	validator := definitionValidator{spec: spec, limits: limits}
	validator.validate()
	return ValidationReport{Valid: len(validator.errors) == 0, Errors: validator.errors}
}

type definitionValidator struct {
	spec   DefinitionSpec
	limits ValidationLimits
	errors []ValidationError
}

func (v *definitionValidator) validate() {
	if v.spec.SchemaVersion != "" && strings.TrimSpace(v.spec.SchemaVersion) != SchemaVersionV1 {
		v.add("schema_version_invalid", "schema_version", "automation schema_version must be meshx.automation/v1 when provided")
	}
	if len(v.spec.Jobs) == 0 {
		v.add("jobs_required", "jobs", "automation must contain at least one job")
		return
	}
	if v.limits.MaxJobs > 0 && len(v.spec.Jobs) > v.limits.MaxJobs {
		v.add("jobs_limit_exceeded", "jobs", fmt.Sprintf("automation must contain at most %d jobs", v.limits.MaxJobs))
		return
	}
	if v.spec.Concurrency != nil {
		v.add("concurrency_unsupported", "concurrency", "automation concurrency is not supported")
	}
	v.validateEnv("env", v.spec.Env)
	if len(v.spec.Defaults) > 0 {
		v.add("automation_defaults_unsupported", "defaults", "automation defaults are not supported")
	}
	v.scanAutomationExpressions()
	v.validateJobs()
	v.validateJobGraphAcyclic()
}

func (v *definitionValidator) scanAutomationExpressions() {
	v.scanExpressions("name", v.spec.Name)
	v.scanExpressions("run-name", v.spec.RunName)
	v.scanExpressions("on", v.spec.On)
	v.scanExpressions("defaults", v.spec.Defaults)
	v.scanExpressions("permissions", v.spec.Permissions)
}

func (v *definitionValidator) validateJobs() {
	for _, jobID := range sortedJobIDs(v.spec.Jobs) {
		job := v.spec.Jobs[jobID]
		jobPath := fmt.Sprintf("jobs.%s", jobID)
		if !identifierPattern.MatchString(jobID) {
			v.add("job_id_invalid", jobPath, "job id must start with a letter or underscore and contain only letters, digits, underscore, or dash")
		}
		for idx, need := range job.Needs {
			if _, ok := v.spec.Jobs[need]; !ok {
				v.add("job_needs_missing", fmt.Sprintf("%s.needs[%d]", jobPath, idx), "job needs references a missing job")
			}
			if need == jobID {
				v.add("job_needs_self", fmt.Sprintf("%s.needs[%d]", jobPath, idx), "job cannot need itself")
			}
		}
		if len(job.Steps) == 0 {
			v.add("job_steps_required", jobPath+".steps", "job must contain at least one step")
		}
		if strings.TrimSpace(job.If) != "" {
			v.add("job_if_unsupported", jobPath+".if", "automation job if conditions are not supported")
		}
		if v.limits.MaxStepsPerJob > 0 && len(job.Steps) > v.limits.MaxStepsPerJob {
			v.add("job_steps_limit_exceeded", jobPath+".steps", fmt.Sprintf("job must contain at most %d steps", v.limits.MaxStepsPerJob))
			continue
		}
		v.validateUnsupportedJobFields(jobPath, job)
		v.validateRunsOnLabels(jobPath, job.RunsOn)
		v.validateJobInputSize(jobPath, JobWithInheritedEnv(v.spec.Env, job))
		v.validateSteps(jobPath, job)
		v.validateAgentTeamWaitJobShape(jobPath, job)
		v.scanJobExpressions(jobPath, job)
	}
}

func (v *definitionValidator) validateUnsupportedJobFields(jobPath string, job JobSpec) {
	if job.Strategy != nil {
		v.add("job_strategy_unsupported", jobPath+".strategy", "automation job strategy and matrix are not supported")
	}
	if job.Services != nil {
		v.add("job_services_unsupported", jobPath+".services", "automation job services are not supported")
	}
	if job.Container != nil {
		v.add("job_container_unsupported", jobPath+".container", "automation job containers are not supported")
	}
	if job.Concurrency != nil {
		v.add("job_concurrency_unsupported", jobPath+".concurrency", "automation job concurrency is not supported")
	}
	if job.Environment != nil {
		v.add("job_environment_unsupported", jobPath+".environment", "automation job environments are not supported")
	}
	if len(job.Outputs) > 0 {
		v.add("job_outputs_unsupported", jobPath+".outputs", "automation job outputs are not supported")
	}
	if len(job.Defaults) > 0 {
		v.add("job_defaults_unsupported", jobPath+".defaults", "automation job defaults are not supported")
	}
}

func (v *definitionValidator) validateRunsOnLabels(jobPath string, runsOn any) {
	group := runnerGroupFromRunsOn(runsOn)
	if IsReservedRunnerGroup(group) {
		v.add("runner_group_reserved", jobPath+".runs-on.group", "automation job runs-on group is reserved for internal runners")
	}
	if len(group) > RunnerGroupMaxLength {
		v.add("runner_group_length_exceeded", jobPath+".runs-on.group", fmt.Sprintf("automation job runs-on group must be at most %d characters", RunnerGroupMaxLength))
	}
	labels := runnerLabelsFromRunsOn(runsOn)
	if v.limits.MaxRunnerLabels > 0 && len(labels) > v.limits.MaxRunnerLabels {
		v.add("runner_labels_limit_exceeded", jobPath+".runs-on", fmt.Sprintf("automation job runs-on must contain at most %d labels", v.limits.MaxRunnerLabels))
	}
	if len(RunnerLabelKey(labels)) > RunnerLabelKeyMaxLength {
		v.add("runner_label_key_length_exceeded", jobPath+".runs-on", fmt.Sprintf("automation job runs-on label key must be at most %d characters", RunnerLabelKeyMaxLength))
	}
}

func (v *definitionValidator) validateJobInputSize(jobPath string, job JobSpec) {
	if v.limits.MaxJobInputBytes <= 0 {
		return
	}
	encoded, err := json.Marshal(job)
	if err != nil {
		return
	}
	if len(encoded) > v.limits.MaxJobInputBytes {
		v.add("job_input_limit_exceeded", jobPath, fmt.Sprintf("automation job input must be at most %d bytes", v.limits.MaxJobInputBytes))
	}
}

func (v *definitionValidator) validateSteps(jobPath string, job JobSpec) {
	seenStepIDs := map[string]int{}
	for idx, step := range job.Steps {
		stepPath := fmt.Sprintf("%s.steps[%d]", jobPath, idx)
		if step.ID != "" {
			if !identifierPattern.MatchString(step.ID) {
				v.add("step_id_invalid", stepPath+".id", "step id must start with a letter or underscore and contain only letters, digits, underscore, or dash")
			} else if previous, ok := seenStepIDs[step.ID]; ok {
				v.add("step_id_duplicate", stepPath+".id", fmt.Sprintf("step id duplicates steps[%d]", previous))
			} else {
				seenStepIDs[step.ID] = idx
			}
		}
		if strings.TrimSpace(step.Uses) == "" && strings.TrimSpace(step.Run) == "" {
			v.add("step_action_required", stepPath, "step must define uses or run")
		}
		if strings.TrimSpace(step.Uses) != "" && strings.TrimSpace(step.Run) != "" {
			v.add("step_action_exclusive", stepPath, "step cannot define both uses and run")
		}
		if uses := strings.TrimSpace(step.Uses); uses != "" && strings.TrimSpace(step.Run) == "" {
			if removedMessage, ok := removedAgentTeamUses[uses]; ok {
				v.add("step_uses_removed", stepPath+".uses", removedMessage)
			} else if _, ok := supportedAgentTeamUses[uses]; !ok {
				v.add("step_uses_unsupported", stepPath+".uses", "only AgentTeam-native uses steps are supported")
			}
			switch uses {
			case AgentTeamCheckoutUse:
				v.validateAgentTeamCheckoutStep(stepPath, step)
			case AgentTeamPublishWikiUse:
				v.validateAgentTeamPublishWikiStep(stepPath, step)
			case AgentTeamPublishTreeUse:
				v.validateAgentTeamPublishTreeStep(stepPath, step)
			}
		}
		if strings.TrimSpace(step.If) != "" {
			v.add("step_if_unsupported", stepPath+".if", "automation step if conditions are not supported")
		}
		v.validateEnv(stepPath+".env", step.Env)
		v.scanStepExpressions(stepPath, step)
	}
}

func (v *definitionValidator) validateAgentTeamCheckoutStep(stepPath string, step StepSpec) {
	_, errs := ParseAgentTeamCheckoutOptions(step.With)
	for _, err := range errs {
		v.add(err.Code, stepPath+".with."+err.Key, err.Message)
	}
}

func (v *definitionValidator) validateAgentTeamPublishWikiStep(stepPath string, step StepSpec) {
	_, errs := ParseAgentTeamPublishWikiOptions(step.With)
	for _, err := range errs {
		v.add(err.Code, stepPath+".with."+err.Key, err.Message)
	}
}

func (v *definitionValidator) validateAgentTeamPublishTreeStep(stepPath string, step StepSpec) {
	_, errs := ParseAgentTeamPublishTreeOptions(step.With)
	for _, err := range errs {
		v.add(err.Code, stepPath+".with."+err.Key, err.Message)
	}
}

func (v *definitionValidator) validateAgentTeamWaitJobShape(jobPath string, job JobSpec) {
	hasWaitStep := false
	for idx, step := range job.Steps {
		uses := strings.TrimSpace(step.Uses)
		if _, ok := agentteamWaitUses[uses]; ok {
			hasWaitStep = true
			if uses == "agentteam/agent-task" {
				if firstStepWithString(step.With, "task_id", "taskId", "task-id", "resource_id", "resourceId", "resource-id") == "" {
					v.add("agentteam_agent_task_id_required", fmt.Sprintf("%s.steps[%d].with.task_id", jobPath, idx), "agentteam/agent-task waits must set task_id or resource_id")
				}
				if firstStepWithString(step.With, "resource_type", "resourceType", "resource-type", "resume_event_type", "resumeEventType", "resume-event-type", "event_type", "eventType", "event-type") != "" {
					v.add("agentteam_agent_task_signal_identity_fixed", fmt.Sprintf("%s.steps[%d].with", jobPath, idx), "agentteam/agent-task waits use the fixed AgentTeam task completion signal identity")
				}
			}
		}
	}
	if !hasWaitStep {
		return
	}
	if len(job.Steps) != 1 {
		v.add("agentteam_wait_job_mixed_steps", jobPath+".steps", "AgentTeam-native wait jobs must contain exactly one AgentTeam-native wait step")
	}
}

func firstStepWithString(values map[string]any, keys ...string) string {
	for _, key := range keys {
		raw, ok := values[key]
		if !ok {
			continue
		}
		value, ok := raw.(string)
		if !ok {
			continue
		}
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func (v *definitionValidator) scanJobExpressions(jobPath string, job JobSpec) {
	v.scanExpressions(jobPath+".name", job.Name)
	v.scanExpressions(jobPath+".needs", job.Needs)
	v.scanExpressions(jobPath+".if", job.If)
	v.scanExpressions(jobPath+".runs-on", job.RunsOn)
	v.validateEnv(jobPath+".env", job.Env)
	v.scanExpressions(jobPath+".outputs", job.Outputs)
	v.scanExpressions(jobPath+".permissions", job.Permissions)
}

func (v *definitionValidator) scanStepExpressions(stepPath string, step StepSpec) {
	v.scanExpressions(stepPath+".id", step.ID)
	v.scanExpressions(stepPath+".name", step.Name)
	v.scanExpressions(stepPath+".if", step.If)
	v.scanExpressions(stepPath+".uses", step.Uses)
	v.scanExpressions(stepPath+".run", step.Run)
	v.scanExpressions(stepPath+".with", step.With)
}

func (v *definitionValidator) validateEnv(path string, env map[string]any) {
	if len(env) == 0 {
		return
	}
	keys := make([]string, 0, len(env))
	for key := range env {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		envPath := path + "." + key
		if key != strings.TrimSpace(key) || !ValidEnvReferenceName(key) {
			v.add("env_name_invalid", envPath, "automation env name must start with a letter or underscore and contain only letters, digits, or underscore")
			continue
		}
		if _, err := ParseEnvValue(env[key]); err != nil {
			if errors.Is(err, errEnvValueNotString) {
				v.add("env_value_invalid", envPath, "automation env value must be a string")
				continue
			}
			v.add("env_value_unsupported", envPath, "automation env value must be a literal string or a full secrets/inputs expression")
		}
	}
}

func (v *definitionValidator) scanExpressions(path string, value any) {
	switch typed := value.(type) {
	case string:
		if containsExpressionDelimiter(typed) {
			v.add("expression_unsupported", path, "automation expressions are not supported by the current official-runner execution MVP")
		}
	case []string:
		for idx, item := range typed {
			v.scanExpressions(fmt.Sprintf("%s[%d]", path, idx), item)
		}
	case map[string]any:
		keys := make([]string, 0, len(typed))
		for key := range typed {
			keys = append(keys, key)
		}
		sort.Strings(keys)
		for _, key := range keys {
			if containsExpressionDelimiter(key) {
				v.add("expression_unsupported", path+"."+key, "automation expressions are not supported by the current official-runner execution MVP")
			}
			v.scanExpressions(path+"."+key, typed[key])
		}
	case []any:
		for idx, item := range typed {
			v.scanExpressions(fmt.Sprintf("%s[%d]", path, idx), item)
		}
	case map[string]string:
		keys := make([]string, 0, len(typed))
		for key := range typed {
			keys = append(keys, key)
		}
		sort.Strings(keys)
		for _, key := range keys {
			if containsExpressionDelimiter(key) {
				v.add("expression_unsupported", path+"."+key, "automation expressions are not supported by the current official-runner execution MVP")
			}
			v.scanExpressions(path+"."+key, typed[key])
		}
	}
}

func containsExpressionDelimiter(value string) bool {
	return strings.Contains(value, "${{") || strings.Contains(value, "}}")
}

func (v *definitionValidator) validateJobGraphAcyclic() {
	visiting := map[string]bool{}
	visited := map[string]bool{}
	var visit func(string) bool
	visit = func(jobID string) bool {
		if visiting[jobID] {
			return true
		}
		if visited[jobID] {
			return false
		}
		visiting[jobID] = true
		for _, need := range v.spec.Jobs[jobID].Needs {
			if _, ok := v.spec.Jobs[need]; !ok {
				continue
			}
			if visit(need) {
				return true
			}
		}
		visiting[jobID] = false
		visited[jobID] = true
		return false
	}
	for _, jobID := range sortedJobIDs(v.spec.Jobs) {
		if visit(jobID) {
			v.add("job_needs_cycle", "jobs", "job needs graph must be acyclic")
			return
		}
	}
}

func sortedJobIDs(jobs map[string]JobSpec) []string {
	ids := make([]string, 0, len(jobs))
	for id := range jobs {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	return ids
}

func runnerLabelsFromRunsOn(value any) []string {
	switch typed := value.(type) {
	case nil:
		return nil
	case string:
		if strings.TrimSpace(typed) == "" {
			return nil
		}
		return []string{strings.TrimSpace(typed)}
	case []any:
		labels := make([]string, 0, len(typed))
		for _, item := range typed {
			if text, ok := item.(string); ok && strings.TrimSpace(text) != "" {
				labels = append(labels, strings.TrimSpace(text))
			}
		}
		return labels
	case []string:
		labels := make([]string, 0, len(typed))
		for _, item := range typed {
			if strings.TrimSpace(item) != "" {
				labels = append(labels, strings.TrimSpace(item))
			}
		}
		return labels
	case map[string]any:
		return runnerLabelsFromRunsOn(typed["labels"])
	case map[string]string:
		return runnerLabelsFromRunsOn(typed["labels"])
	default:
		return nil
	}
}

func runnerGroupFromRunsOn(value any) string {
	switch typed := value.(type) {
	case map[string]any:
		if group, ok := typed["group"].(string); ok {
			return strings.TrimSpace(group)
		}
	case map[string]string:
		return strings.TrimSpace(typed["group"])
	}
	return ""
}

// RunnerLabelKey returns the normalized queue matching key for runner labels.
func RunnerLabelKey(labels []string) string {
	if len(labels) == 0 {
		return ""
	}
	out := make([]string, 0, len(labels))
	seen := map[string]struct{}{}
	for _, label := range labels {
		normalized := strings.ToLower(strings.TrimSpace(label))
		if normalized == "" {
			continue
		}
		if _, ok := seen[normalized]; ok {
			continue
		}
		seen[normalized] = struct{}{}
		out = append(out, normalized)
	}
	sort.Strings(out)
	return strings.Join(out, ",")
}

func (v *definitionValidator) add(code string, path string, message string) {
	v.errors = append(v.errors, ValidationError{Code: code, Path: path, Message: message})
}
