package automation

import (
	"context"
	"fmt"
	"sort"
	"strings"

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

// AutomationRunnerReadiness is the read-side runner pool summary for one
// repository automation snapshot.
type AutomationRunnerReadiness struct {
	RequiredLabels      []string                        `json:"required_labels,omitempty"`
	RequiredGroup       string                          `json:"required_group,omitempty"`
	MatchingRunnerCount int64                           `json:"matching_runner_count"`
	OnlineRunnerCount   int64                           `json:"online_runner_count"`
	OfflineRunnerCount  int64                           `json:"offline_runner_count"`
	IdleRunnerCount     int64                           `json:"idle_runner_count"`
	BusyRunnerCount     int64                           `json:"busy_runner_count"`
	QueueDepth          int64                           `json:"queue_depth"`
	ReasonCode          string                          `json:"reason_code,omitempty"`
	Runnable            bool                            `json:"runnable"`
	Pools               []AutomationRunnerReadinessPool `json:"pools,omitempty"`
}

// AutomationRunnerReadinessPool is the per-pool readiness fact derived from one
// unique runner-group plus label requirement set.
type AutomationRunnerReadinessPool struct {
	RequiredLabels      []string `json:"required_labels,omitempty"`
	RequiredGroup       string   `json:"required_group,omitempty"`
	MatchingRunnerCount int64    `json:"matching_runner_count"`
	OnlineRunnerCount   int64    `json:"online_runner_count"`
	OfflineRunnerCount  int64    `json:"offline_runner_count"`
	IdleRunnerCount     int64    `json:"idle_runner_count"`
	BusyRunnerCount     int64    `json:"busy_runner_count"`
	QueueDepth          int64    `json:"queue_depth"`
	ReasonCode          string   `json:"reason_code,omitempty"`
	Runnable            bool     `json:"runnable"`
}

type automationRunnerPoolKey struct {
	group    string
	labelKey string
}

type automationRunnerState struct {
	runnerGroup string
	labelKey    string
}

// GetRepositoryAutomationRunnerReadiness resolves one saved repository automation
// snapshot and returns the current runner readiness summary for its required
// runner pools.
func (s *AutomationsService) GetRepositoryAutomationRunnerReadiness(ctx context.Context, workspaceID string, subject string, input AutomationRepositoryAutomationQuery) (AutomationRunnerReadiness, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return AutomationRunnerReadiness{}, err
	}
	input = normalizeAutomationRepositoryQuery(input)
	if err := validateAutomationRepository(input.RepositoryOwner, input.RepositoryName); err != nil {
		return AutomationRunnerReadiness{}, err
	}
	if !isAutomationSourcePath(input.DefinitionPath) {
		return AutomationRunnerReadiness{}, invalidArgument("Automation definition path must be under .agentteam and end in .yml or .yaml")
	}
	if err := s.automationAuthorizer().CanReadAutomationRepo(ctx, s.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{RepositoryOwner: input.RepositoryOwner, RepositoryName: input.RepositoryName})); err != nil {
		return AutomationRunnerReadiness{}, err
	}
	if s.sourceResolver == nil {
		return AutomationRunnerReadiness{}, invalidArgument("Automation source resolver is not configured")
	}
	snapshot, err := s.sourceResolver.ResolveAutomationSource(ctx, AutomationSourceRequest{
		WorkspaceID:         workspaceID,
		RepositoryOwner:     input.RepositoryOwner,
		RepositoryName:      input.RepositoryName,
		DefinitionPath:      input.DefinitionPath,
		GitRef:              input.GitRef,
		AuthorizationHeader: input.AuthorizationHeader,
	})
	if err != nil {
		return AutomationRunnerReadiness{}, fmt.Errorf("resolve automation source: %w", err)
	}
	if err := s.bindAutomationRepository(ctx, workspaceID, subject, snapshot.RepositoryOwner, snapshot.RepositoryName, snapshot.GitRef); err != nil {
		return AutomationRunnerReadiness{}, err
	}
	if err := validateAutomationSourceBytes(snapshot.Content, s.automationLimits()); err != nil {
		return AutomationRunnerReadiness{}, err
	}
	spec, err := decodeAutomationSpec(snapshot.Content)
	if err != nil {
		return AutomationRunnerReadiness{}, invalidArgument("Automation source must be valid AgentTeam Automation YAML or normalized JSON")
	}
	report := dsl.ValidateDefinitionWithLimits(spec, s.automationLimits().validationLimits())
	if !report.Valid {
		return AutomationRunnerReadiness{}, invalidArgument("Automation spec is invalid")
	}
	return s.automationRunnerReadinessForSpec(ctx, strings.TrimSpace(workspaceID), spec)
}

func (s *AutomationsService) automationRunnerReadinessForSpec(ctx context.Context, workspaceID string, spec dsl.DefinitionSpec) (AutomationRunnerReadiness, error) {
	if err := s.ensureStore(); err != nil {
		return AutomationRunnerReadiness{}, err
	}
	poolKeys := automationRunnerPoolsFromSpec(spec)
	if len(poolKeys) == 0 {
		return AutomationRunnerReadiness{Runnable: true}, nil
	}
	runners, err := s.store.ListActiveRunners(ctx, workspaceID)
	if err != nil {
		return AutomationRunnerReadiness{}, err
	}
	sessions, err := s.store.ListActiveRunnerSessions(ctx, workspaceID, s.now())
	if err != nil {
		return AutomationRunnerReadiness{}, err
	}
	queueJobs, err := s.store.ListQueueJobsByStatuses(ctx, workspaceID, automationstore.AutomationJobStatusReady, automationstore.AutomationJobStatusLeased)
	if err != nil {
		return AutomationRunnerReadiness{}, err
	}
	activeSessionByRunnerID := make(map[string]automationstore.AutomationRunnerSession, len(sessions))
	busyLeaseOwners := map[string]struct{}{}
	for _, session := range sessions {
		if existing, ok := activeSessionByRunnerID[session.RunnerID]; ok {
			if existing.UpdatedAt.After(session.UpdatedAt) {
				continue
			}
		}
		activeSessionByRunnerID[session.RunnerID] = session
	}
	queueDepthByPool := map[automationRunnerPoolKey]int64{}
	for _, job := range queueJobs {
		if job.Status == automationstore.AutomationJobStatusLeased && strings.TrimSpace(job.LeaseOwner) != "" {
			busyLeaseOwners[strings.TrimSpace(job.LeaseOwner)] = struct{}{}
		}
		key := automationRunnerPoolKey{
			group:    strings.TrimSpace(job.RunnerGroup),
			labelKey: dsl.RunnerLabelKey(decodeAutomationRunnerLabels(job.LabelsJSON)),
		}
		queueDepthByPool[key]++
	}
	runnerStates := make([]automationRunnerState, 0, len(runners))
	for _, runner := range runners {
		runnerStates = append(runnerStates, automationRunnerState{
			runnerGroup: strings.TrimSpace(runner.RunnerGroup),
			labelKey:    dsl.RunnerLabelKey(decodeAutomationRunnerLabels(runner.LabelsJSON)),
		})
	}

	pools := make([]AutomationRunnerReadinessPool, 0, len(poolKeys))
	for _, key := range poolKeys {
		pool := AutomationRunnerReadinessPool{
			RequiredLabels: append([]string(nil), automationRunnerLabelsFromKey(key.labelKey)...),
			RequiredGroup:  key.group,
			QueueDepth:     queueDepthByPool[key],
			Runnable:       true,
		}
		for idx, runner := range runners {
			state := runnerStates[idx]
			if !automationRunnerMatchesPool(state, key) {
				continue
			}
			pool.MatchingRunnerCount++
			session, online := activeSessionByRunnerID[runner.RunnerID]
			if !online {
				continue
			}
			pool.OnlineRunnerCount++
			if _, busy := busyLeaseOwners[strings.TrimSpace(session.LeaseOwner)]; busy {
				pool.BusyRunnerCount++
			}
		}
		pool.OfflineRunnerCount = pool.MatchingRunnerCount - pool.OnlineRunnerCount
		pool.IdleRunnerCount = pool.OnlineRunnerCount - pool.BusyRunnerCount
		pool.ReasonCode = automationRunnerPoolReasonCode(pool, len(runners))
		pool.Runnable = pool.MatchingRunnerCount > 0
		pools = append(pools, pool)
	}
	summary := automationRunnerReadinessSummary(pools)
	summary.Pools = pools
	return summary, nil
}

func automationRunnerReadinessSummary(pools []AutomationRunnerReadinessPool) AutomationRunnerReadiness {
	summary := AutomationRunnerReadiness{Runnable: true}
	if len(pools) == 0 {
		return summary
	}
	selected := pools[0]
	for _, pool := range pools {
		if !pool.Runnable {
			selected = pool
			break
		}
	}
	for _, pool := range pools {
		if !pool.Runnable {
			summary.Runnable = false
			break
		}
	}
	summary.RequiredLabels = append([]string(nil), selected.RequiredLabels...)
	summary.RequiredGroup = selected.RequiredGroup
	summary.MatchingRunnerCount = selected.MatchingRunnerCount
	summary.OnlineRunnerCount = selected.OnlineRunnerCount
	summary.OfflineRunnerCount = selected.OfflineRunnerCount
	summary.IdleRunnerCount = selected.IdleRunnerCount
	summary.BusyRunnerCount = selected.BusyRunnerCount
	summary.QueueDepth = selected.QueueDepth
	summary.ReasonCode = selected.ReasonCode
	return summary
}

func automationRunnerPoolReasonCode(pool AutomationRunnerReadinessPool, totalRunners int) string {
	switch {
	case pool.MatchingRunnerCount == 0 && totalRunners > 0:
		return "runner_label_mismatch"
	case pool.MatchingRunnerCount == 0:
		return "runner_no_matching_runner"
	case pool.OnlineRunnerCount == 0:
		return "runner_no_online_runner"
	case pool.BusyRunnerCount >= pool.OnlineRunnerCount && pool.OnlineRunnerCount > 0:
		return "runner_all_matching_runners_busy"
	default:
		return ""
	}
}

func automationRunnerPoolsFromSpec(spec dsl.DefinitionSpec) []automationRunnerPoolKey {
	if len(spec.Jobs) == 0 {
		return nil
	}
	keys := make([]automationRunnerPoolKey, 0, len(spec.Jobs))
	seen := map[automationRunnerPoolKey]struct{}{}
	for _, jobID := range sortedAutomationRunnerPoolJobIDs(spec.Jobs) {
		job := spec.Jobs[jobID]
		group := strings.TrimSpace(automationRunnerGroupFromRunsOn(job.RunsOn))
		if group == "" && !automationRunsOnRequiresManagedRunner(job.RunsOn) {
			continue
		}
		labels := automationRunnerLabelsFromRunsOn(job.RunsOn)
		if group == "" && len(labels) == 0 {
			continue
		}
		key := automationRunnerPoolKey{
			group:    group,
			labelKey: dsl.RunnerLabelKey(labels),
		}
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		keys = append(keys, key)
	}
	sort.Slice(keys, func(i, j int) bool {
		if keys[i].group != keys[j].group {
			return keys[i].group < keys[j].group
		}
		return keys[i].labelKey < keys[j].labelKey
	})
	return keys
}

func automationRunsOnRequiresManagedRunner(value any) bool {
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed) == "self-hosted"
	case []string:
		for _, label := range typed {
			if strings.TrimSpace(label) == "self-hosted" {
				return true
			}
		}
	case []any:
		for _, item := range typed {
			if label, ok := item.(string); ok && strings.TrimSpace(label) == "self-hosted" {
				return true
			}
		}
	case map[string]any:
		if group, ok := typed["group"].(string); ok && strings.TrimSpace(group) != "" {
			return true
		}
		return automationRunsOnRequiresManagedRunner(typed["labels"])
	case map[string]string:
		if strings.TrimSpace(typed["group"]) != "" {
			return true
		}
		if labels := strings.TrimSpace(typed["labels"]); labels != "" {
			return automationRunsOnRequiresManagedRunner(labels)
		}
	}
	return false
}

func automationRunnerMatchesPool(runner automationRunnerState, pool automationRunnerPoolKey) bool {
	if pool.group != "" && runner.runnerGroup != pool.group {
		return false
	}
	required := automationRunnerLabelsFromKey(pool.labelKey)
	available := automationRunnerLabelsFromKey(runner.labelKey)
	availableSet := make(map[string]struct{}, len(available))
	for _, label := range available {
		availableSet[label] = struct{}{}
	}
	for _, label := range required {
		if _, ok := availableSet[label]; !ok {
			return false
		}
	}
	return true
}

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

func automationRunnerGroupFromRunsOn(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 ""
}

func automationRunnerLabelsFromKey(key string) []string {
	key = strings.TrimSpace(key)
	if key == "" {
		return nil
	}
	parts := strings.Split(key, ",")
	out := make([]string, 0, len(parts))
	for _, part := range parts {
		if trimmed := strings.TrimSpace(part); trimmed != "" {
			out = append(out, trimmed)
		}
	}
	return out
}

func sortedAutomationRunnerPoolJobIDs(jobs map[string]dsl.JobSpec) []string {
	out := make([]string, 0, len(jobs))
	for jobID := range jobs {
		out = append(out, jobID)
	}
	sort.Strings(out)
	return out
}
