// Package runtime implements durable MeshX automation execution primitives.
package runtime

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/automation/dsl"
	"github.com/tidbcloud/AgentTeam/internal/store/automation"
)

const (
	DefaultQueueName = "default"

	// ServerManagedRunnerGroup is the internal runner group for jobs that require
	// MeshX server-only runtime capabilities.
	ServerManagedRunnerGroup = dsl.ReservedServerManagedRunnerGroup
)

type IDFactory func(prefix string) (string, error)

type InitialRuntime struct {
	JobRuns []automation.AutomationJobRun
	Events  []automation.AutomationEvent
	Jobs    []automation.AutomationJob
	Waits   []automation.AutomationWait
}

func BuildInitialRuntime(run automation.AutomationRun, spec dsl.DefinitionSpec, now time.Time, newID IDFactory) (InitialRuntime, error) {
	if newID == nil {
		return InitialRuntime{}, fmt.Errorf("automation runtime id factory is nil")
	}
	orderedJobs, err := topologicalJobs(spec.Jobs)
	if err != nil {
		return InitialRuntime{}, err
	}
	jobRunByJobID := map[string]string{}
	serverManagedByRunID := map[string]bool{}
	var jobRuns []automation.AutomationJobRun
	for _, jobID := range orderedJobs {
		job := spec.Jobs[jobID]
		job = dsl.JobWithInheritedEnv(spec.Env, job)
		jobRunID, err := newID("wfj_")
		if err != nil {
			return InitialRuntime{}, err
		}
		dependsOn := make([]string, 0, len(job.Needs))
		for _, need := range job.Needs {
			if id := jobRunByJobID[need]; id != "" {
				dependsOn = append(dependsOn, id)
			}
		}
		dependsJSON, _ := json.Marshal(dependsOn)
		inputJSON, _ := json.Marshal(job)
		runsOnJSON := automationRunsOnJSON(job.RunsOn)
		status := automation.AutomationJobRunStatusPending
		if len(dependsOn) == 0 {
			status = automation.AutomationJobRunStatusQueued
		}
		waitKind := agentteamWaitKind(job)
		if len(dependsOn) == 0 && waitKind != "" {
			status = automation.AutomationJobRunStatusWaiting
		}
		jobRun := automation.AutomationJobRun{ID: jobRunID, WorkspaceID: run.WorkspaceID, RunID: run.ID, JobID: jobID, JobType: "github_actions_job", Status: status, MaxAttempts: 3, RunsOnJSON: runsOnJSON, InputJSON: string(inputJSON), DependsOnJSON: string(dependsJSON), ScheduledAt: now, CreatedAt: now, UpdatedAt: now}
		jobRuns = append(jobRuns, jobRun)
		jobRunByJobID[jobID] = jobRunID
		serverManagedByRunID[jobRunID] = jobRequiresServerManagedRunner(job)
	}
	eventID, err := newID("wfe_")
	if err != nil {
		return InitialRuntime{}, err
	}
	runtime := InitialRuntime{JobRuns: jobRuns, Events: []automation.AutomationEvent{{ID: eventID, WorkspaceID: run.WorkspaceID, RunID: run.ID, EventType: "run_started", ActorPrincipalID: run.CreatedByPrincipalID, Summary: "Automation run started", CreatedAt: now}}}
	for _, jobRun := range jobRuns {
		switch jobRun.Status {
		case automation.AutomationJobRunStatusQueued:
			runtime.Jobs = append(runtime.Jobs, assignmentForJobRun(run.WorkspaceID, run.ID, jobRun, serverManagedByRunID[jobRun.ID], now))
		case automation.AutomationJobRunStatusWaiting:
			wait, event, err := initialWaitForJobRun(run, jobRun, now, newID)
			if err != nil {
				return InitialRuntime{}, err
			}
			runtime.Waits = append(runtime.Waits, wait)
			runtime.Events = append(runtime.Events, event)
		}
	}
	return runtime, nil
}

func agentteamWaitKind(job dsl.JobSpec) string {
	if strings.TrimSpace(job.If) != "" || len(job.Steps) != 1 || strings.TrimSpace(job.Steps[0].If) != "" {
		return ""
	}
	switch strings.TrimSpace(job.Steps[0].Uses) {
	case "agentteam/agent-task":
		return automation.AutomationWaitKindAgentTask
	case "agentteam/wait-event":
		return automation.AutomationWaitKindWaitEvent
	default:
		return ""
	}
}

func initialWaitForJobRun(run automation.AutomationRun, jobRun automation.AutomationJobRun, now time.Time, newID IDFactory) (automation.AutomationWait, automation.AutomationEvent, error) {
	waitID, err := newID("wfw_")
	if err != nil {
		return automation.AutomationWait{}, automation.AutomationEvent{}, err
	}
	eventID, err := newID("wfe_")
	if err != nil {
		return automation.AutomationWait{}, automation.AutomationEvent{}, err
	}
	waitKind := agentteamWaitKindFromInput(jobRun.InputJSON)
	activeKey := activeWaitKey(jobRun.WorkspaceID, jobRun.RunID, jobRun.ID)
	resourceType, resourceID, resumeEventType := waitSubscription(jobRun)
	if waitKind == automation.AutomationWaitKindAgentTask && strings.TrimSpace(resourceID) == "" {
		return automation.AutomationWait{}, automation.AutomationEvent{}, fmt.Errorf("agentteam/agent-task wait requires task_id or resource_id")
	}
	signalKey := automation.AutomationWaitSignalKey(jobRun.WorkspaceID, automation.AutomationWaitSignal{WaitKind: waitKind, EventType: resumeEventType, ResourceType: resourceType, ResourceID: resourceID})
	requester := strings.TrimSpace(run.CreatedByPrincipalID)
	wait := automation.AutomationWait{ID: waitID, WorkspaceID: jobRun.WorkspaceID, RunID: jobRun.RunID, JobRunID: jobRun.ID, WaitKind: waitKind, Status: automation.AutomationWaitStatusActive, ActiveKey: &activeKey, SignalKey: signalKey, ResourceType: resourceType, ResourceID: resourceID, ResumeEventType: resumeEventType, InputJSON: jobRun.InputJSON, CreatedBy: requester, CreatedAt: now, UpdatedAt: now}
	event := automation.AutomationEvent{ID: eventID, WorkspaceID: jobRun.WorkspaceID, RunID: jobRun.RunID, JobRunID: jobRun.ID, EventType: "wait_started", ActorPrincipalID: requester, ResourceType: resourceType, ResourceID: resourceID, Summary: "Automation wait started", MetadataJSON: jobRun.InputJSON, CreatedAt: now}
	return wait, event, nil
}

func firstString(values map[string]any, keys ...string) string {
	for _, key := range keys {
		value, ok := values[key]
		if !ok {
			continue
		}
		switch typed := value.(type) {
		case string:
			if trimmed := strings.TrimSpace(typed); trimmed != "" {
				return trimmed
			}
		case fmt.Stringer:
			if trimmed := strings.TrimSpace(typed.String()); trimmed != "" {
				return trimmed
			}
		}
	}
	return ""
}

func agentteamWaitKindFromInput(inputJSON string) string {
	var job dsl.JobSpec
	_ = json.Unmarshal([]byte(inputJSON), &job)
	return agentteamWaitKind(job)
}

func activeWaitKey(workspaceID string, runID string, jobRunID string) string {
	return workspaceID + ":" + runID + ":" + jobRunID
}

func waitSubscription(jobRun automation.AutomationJobRun) (string, string, string) {
	var job dsl.JobSpec
	_ = json.Unmarshal([]byte(jobRun.InputJSON), &job)
	waitKind := agentteamWaitKind(job)
	stepWith := agentteamWaitStepWith(job, waitKind)
	switch waitKind {
	case automation.AutomationWaitKindAgentTask:
		return "agent_task",
			firstString(stepWith, "task_id", "taskId", "task-id", "resource_id", "resourceId", "resource-id"),
			"agent_task_completed"
	case automation.AutomationWaitKindWaitEvent:
		return firstStringOrDefault(stepWith, "event", "resource_type", "resourceType", "resource-type"),
			firstStringOrDefault(stepWith, jobRun.RunID+":"+jobRun.JobID, "resource_id", "resourceId", "resource-id", "event_key", "eventKey", "event-key", "topic"),
			firstStringOrDefault(stepWith, "automation_event_received", "resume_event_type", "resumeEventType", "resume-event-type", "event_type", "eventType", "event-type")
	default:
		return "", "", ""
	}
}

func agentteamWaitStepWith(job dsl.JobSpec, waitKind string) map[string]any {
	if waitKind == "" {
		return nil
	}
	for _, step := range job.Steps {
		switch waitKind {
		case automation.AutomationWaitKindAgentTask:
			if strings.TrimSpace(step.Uses) == "agentteam/agent-task" {
				return step.With
			}
		case automation.AutomationWaitKindWaitEvent:
			if strings.TrimSpace(step.Uses) == "agentteam/wait-event" {
				return step.With
			}
		}
	}
	return nil
}

func firstStringOrDefault(values map[string]any, fallback string, keys ...string) string {
	if value := firstString(values, keys...); value != "" {
		return value
	}
	return fallback
}

func topologicalJobs(jobs map[string]dsl.JobSpec) ([]string, error) {
	ids := make([]string, 0, len(jobs))
	for id := range jobs {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	visiting := map[string]bool{}
	visited := map[string]bool{}
	var out []string
	var visit func(string) error
	visit = func(id string) error {
		if visiting[id] {
			return fmt.Errorf("automation job needs graph contains a cycle")
		}
		if visited[id] {
			return nil
		}
		visiting[id] = true
		for _, need := range jobs[id].Needs {
			if _, ok := jobs[need]; !ok {
				return fmt.Errorf("automation job %s needs missing job %s", id, need)
			}
			if err := visit(need); err != nil {
				return err
			}
		}
		visiting[id] = false
		visited[id] = true
		out = append(out, id)
		return nil
	}
	for _, id := range ids {
		if err := visit(id); err != nil {
			return nil, err
		}
	}
	return out, nil
}

func assignmentForJobRun(workspaceID string, runID string, jobRun automation.AutomationJobRun, requiresServerManagedRunner bool, now time.Time) automation.AutomationJob {
	runnerGroup, labels := runnerTargetFromRunsOnJSON(jobRun.RunsOnJSON)
	if requiresServerManagedRunner {
		runnerGroup = ServerManagedRunnerGroup
	} else if dsl.IsReservedRunnerGroup(runnerGroup) {
		runnerGroup = ""
	}
	return automation.AutomationJob{WorkspaceID: workspaceID, RunID: runID, JobRunID: jobRun.ID, QueueName: DefaultQueueName, Status: automation.AutomationJobStatusReady, Priority: 100, ScheduledAt: jobRun.ScheduledAt, Attempt: 0, MaxAttempts: jobRun.MaxAttempts, IdempotencyKey: runID + ":" + jobRun.JobID, RunnerGroup: runnerGroup, LabelsJSON: runnerLabelsJSON(labels), ShardBucket: shardForRun(runID), CreatedAt: now, UpdatedAt: now}
}

func jobRequiresServerManagedRunner(job dsl.JobSpec) bool {
	for _, step := range job.Steps {
		switch strings.TrimSpace(step.Uses) {
		case dsl.AgentTeamCheckoutUse, dsl.AgentTeamPublishWikiUse, dsl.AgentTeamPublishTreeUse:
			return true
		}
	}
	return false
}

func jobRuntimeTraitsFromInput(inputJSON string) (string, bool) {
	var job dsl.JobSpec
	if err := json.Unmarshal([]byte(inputJSON), &job); err != nil {
		return "", false
	}
	return agentteamWaitKind(job), jobRequiresServerManagedRunner(job)
}

func automationRunsOnJSON(value any) string {
	if value == nil {
		return ""
	}
	encoded, err := json.Marshal(value)
	if err != nil {
		return ""
	}
	return string(encoded)
}

func runnerTargetFromRunsOnJSON(raw string) (string, []string) {
	if strings.TrimSpace(raw) == "" {
		return "", nil
	}
	var value any
	if err := json.Unmarshal([]byte(raw), &value); err != nil {
		return "", nil
	}
	return runnerTargetFromRunsOn(value)
}

func runnerTargetFromRunsOn(value any) (string, []string) {
	switch typed := value.(type) {
	case string:
		return "", NormalizeRunnerLabels([]string{typed})
	case []string:
		return "", NormalizeRunnerLabels(typed)
	case []any:
		labels := make([]string, 0, len(typed))
		for _, item := range typed {
			if text, ok := item.(string); ok {
				labels = append(labels, text)
			}
		}
		return "", NormalizeRunnerLabels(labels)
	case map[string]any:
		group := stringValue(typed["group"])
		_, labels := runnerTargetFromRunsOn(typed["labels"])
		return group, labels
	case map[string]string:
		group := typed["group"]
		label := typed["labels"]
		return group, NormalizeRunnerLabels([]string{label})
	default:
		return "", nil
	}
}

func runnerLabelsJSON(labels []string) string {
	labels = NormalizeRunnerLabels(labels)
	if len(labels) == 0 {
		return "[]"
	}
	encoded, _ := json.Marshal(labels)
	return string(encoded)
}

// NormalizeRunnerLabels trims runner labels, drops empty labels, and removes
// case-insensitive duplicates while preserving first-seen casing and order.
func NormalizeRunnerLabels(labels []string) []string {
	out := make([]string, 0, len(labels))
	seen := map[string]struct{}{}
	for _, label := range labels {
		trimmed := strings.TrimSpace(label)
		if trimmed == "" {
			continue
		}
		key := strings.ToLower(trimmed)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, trimmed)
	}
	return out
}

func stringValue(value any) string {
	if text, ok := value.(string); ok {
		return strings.TrimSpace(text)
	}
	return ""
}

func shardForRun(runID string) int {
	var sum int
	for _, ch := range runID {
		sum += int(ch)
	}
	return sum % 16
}

func randomRuntimeID(prefix string) (string, error) {
	var bytes [8]byte
	if _, err := rand.Read(bytes[:]); err != nil {
		return "", err
	}
	return prefix + hex.EncodeToString(bytes[:]), nil
}
