package controlplane

import (
	"fmt"
	"sort"
	"strings"

	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
)

func validateApplyAllSourceRevisionAdvanceTarget(advance normalizedApplyAllSourceRevisionAdvance, target *packs.SourceBackedPack) error {
	if target == nil || strings.TrimSpace(target.Name()) != advance.Name {
		return fmt.Errorf("resolved target pack name does not match plan")
	}
	meta := packs.PackHashMetadataForPack(target)
	kind, uri, err := canonicalPackSourceIdentity(meta.SourceKind, meta.SourceURI)
	if err != nil || kind != advance.Kind || uri != advance.URI || !meta.SourceRevisionValid || strings.TrimSpace(meta.SourceRevision) != advance.ToRevision {
		return fmt.Errorf("resolved target source identity/revision does not match planned %s:%s@%s", advance.Kind, advance.URI, advance.ToRevision)
	}
	return nil
}

func orderApplyAllSourceRevisionAdvanceTuple(
	plan []normalizedApplyAllSourceRevisionAdvance,
	targets map[string]*packs.SourceBackedPack,
) ([]normalizedApplyAllSourceRevisionAdvance, error) {
	if len(plan) != len(targets) {
		return nil, fmt.Errorf("source revision advance plan has %d entries but %d resolved targets", len(plan), len(targets))
	}
	byName := make(map[string]normalizedApplyAllSourceRevisionAdvance, len(plan))
	inputOrder := make(map[string]int, len(plan))
	indegree := make(map[string]int, len(plan))
	dependents := make(map[string][]string, len(plan))
	for index, advance := range plan {
		name := strings.ToLower(strings.TrimSpace(advance.Name))
		target, ok := targets[name]
		if !ok || target == nil {
			return nil, fmt.Errorf("resolved target pack %q is missing", advance.Name)
		}
		if err := validateApplyAllSourceRevisionAdvanceTarget(advance, target); err != nil {
			return nil, fmt.Errorf("validate resolved target pack %q: %w", advance.Name, err)
		}
		byName[name] = advance
		inputOrder[name] = index
		indegree[name] = 0
	}
	for _, advance := range plan {
		name := strings.ToLower(strings.TrimSpace(advance.Name))
		target := targets[name]
		for _, dependency := range target.PackDependencies() {
			dependencyName := strings.ToLower(strings.TrimSpace(dependency.Name))
			dependencyTarget, planned := targets[dependencyName]
			if !planned {
				continue
			}
			version := strings.TrimSpace(packs.PackHashMetadataForPack(dependencyTarget).PackVersion)
			if !packs.VersionSatisfiesConstraint(version, dependency.VersionConstraint) {
				return nil, fmt.Errorf("target pack %s dependency %s version %q does not satisfy %q", advance.Name, dependency.Name, version, dependency.VersionConstraint)
			}
			indegree[name]++
			dependents[dependencyName] = append(dependents[dependencyName], name)
		}
	}

	ready := make([]string, 0, len(plan))
	for _, advance := range plan {
		name := strings.ToLower(strings.TrimSpace(advance.Name))
		if indegree[name] == 0 {
			ready = append(ready, name)
		}
	}
	ordered := make([]normalizedApplyAllSourceRevisionAdvance, 0, len(plan))
	for len(ready) > 0 {
		name := ready[0]
		ready = ready[1:]
		ordered = append(ordered, byName[name])
		for _, dependent := range dependents[name] {
			indegree[dependent]--
			if indegree[dependent] == 0 {
				ready = append(ready, dependent)
			}
		}
		sort.SliceStable(ready, func(i, j int) bool {
			return inputOrder[ready[i]] < inputOrder[ready[j]]
		})
	}
	if len(ordered) != len(plan) {
		return nil, fmt.Errorf("target pack dependency cycle prevents source revision convergence")
	}
	return ordered, nil
}
