package clusterrecovery

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"sort"
	"time"

	"github.com/pingcap-inc/clinic/pkg/db"
)

type PlanOutput struct {
	Report         PlanReport
	CreateManifest Manifest
	UpdateManifest Manifest
}

type Planner struct {
	source Source
	store  ReadStore
	now    func() time.Time
	runID  func() (string, error)
}

func NewPlanner(source Source, store ReadStore) *Planner {
	return &Planner{
		source: source,
		store:  store,
		now:    time.Now,
		runID:  newRunID,
	}
}

type planWorkItem struct {
	candidate    Candidate
	typeConflict bool
	resolved     *ResolvedSource
	desired      *db.Cluster
	result       PlanResult
}

func (p *Planner) Plan(ctx context.Context, seed *Seed) (*PlanOutput, error) {
	if p == nil || p.source == nil || p.store == nil {
		return nil, errors.New("planner source and store are required")
	}
	if err := ValidateSeed(seed); err != nil {
		return nil, err
	}
	now := p.now().UTC()
	runID, err := p.runID()
	if err != nil {
		return nil, fmt.Errorf("generate recovery run ID: %w", err)
	}
	if !runIDPattern.MatchString(runID) {
		return nil, errors.New("generated recovery run ID is invalid")
	}

	items := make(map[string]*planWorkItem)
	identities := make(map[string]*planWorkItem)
	queue := make([]*planWorkItem, 0, len(seed.Candidates))
	add := func(candidate Candidate) *planWorkItem {
		key := candidateKey(candidate)
		if existing, ok := items[key]; ok {
			mergeCandidate(existing, candidate)
			return existing
		}
		item := &planWorkItem{candidate: candidate}
		item.candidate.Evidence = canonicalEvidence(item.candidate.Evidence)
		items[key] = item
		identityKey := candidate.ClusterID + ":" + fmt.Sprintf("%d", candidate.TenantID)
		if existing := identities[identityKey]; existing != nil && existing.candidate.Kind != candidate.Kind {
			existing.typeConflict = true
			item.typeConflict = true
		} else {
			identities[identityKey] = item
		}
		queue = append(queue, item)
		return item
	}
	for _, seedCandidate := range seed.Candidates {
		add(CandidateFromSeed(seedCandidate))
	}

	for index := 0; index < len(queue); index++ {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		item := queue[index]
		item.result.Candidate = item.candidate
		if item.typeConflict {
			item.result.Classification = ClassificationConflict
			item.result.ReasonCode = "candidate_type_conflict"
			continue
		}
		if item.candidate.Kind == ObjectKindUnsupported {
			item.result.Classification = ClassificationSkipUnsupported
			item.result.ReasonCode = "unsupported_candidate_type"
			continue
		}
		resolved, err := p.source.Resolve(ctx, item.candidate)
		if err != nil {
			item.result.Classification, item.result.ReasonCode = classifySourceError(err)
			continue
		}
		if resolved == nil {
			item.result.Classification = ClassificationSourceError
			item.result.ReasonCode = "empty_source_result"
			continue
		}
		item.resolved = resolved
		item.result.SourceUpdatedAt = timePointer(resolved.SourceUpdatedAt)
		if resolved.Parent != nil {
			add(*resolved.Parent)
		}
		if resolved.Desired == nil {
			if resolved.Terminal {
				item.result.Classification = ClassificationSkipDeleted
				item.result.ReasonCode = "source_terminal_model_missing"
			} else {
				item.result.Classification = ClassificationSourceError
				item.result.ReasonCode = "source_model_missing"
			}
			continue
		}
		if resolved.Desired.ClusterID != item.candidate.ClusterID || resolved.Desired.TenantID != item.candidate.TenantID {
			item.result.Classification = ClassificationConflict
			item.result.ReasonCode = "source_identity_mismatch"
			continue
		}
		if err := validateDesiredLifecycle(resolved.Desired); err != nil ||
			resolved.Terminal != terminalStatus(resolved.Desired.Status) {
			item.result.Classification = ClassificationSourceError
			item.result.ReasonCode = "source_lifecycle_inconsistent"
			continue
		}

		snapshot, err := p.store.ReadSnapshot(ctx, item.candidate.Kind, item.candidate.TenantID, item.candidate.ClusterID)
		if err != nil {
			item.result.Classification = ClassificationDBError
			item.result.ReasonCode = "clinic_snapshot_error"
			continue
		}
		if snapshot == nil {
			item.result.Classification = ClassificationDBError
			item.result.ReasonCode = "clinic_snapshot_missing"
			continue
		}
		if snapshot.Cluster != nil && snapshot.Cluster.TenantID != item.candidate.TenantID {
			item.result.Classification = ClassificationConflict
			item.result.ReasonCode = "clinic_tenant_mismatch"
			continue
		}
		if snapshot.Organization == nil {
			item.result.Classification = ClassificationBlockedOrg
			item.result.ReasonCode = "clinic_org_missing"
			continue
		}
		if snapshot.Cluster != nil && snapshot.Cluster.OrgID != snapshot.Organization.ID {
			item.result.Classification = ClassificationConflict
			item.result.ReasonCode = "clinic_org_mismatch"
			continue
		}

		desired := *resolved.Desired
		desired.OrgID = snapshot.Organization.ID
		item.desired = &desired
		desiredChecksum, err := desiredClusterChecksum(item.candidate.Kind, &desired)
		if err != nil {
			item.result.Classification = ClassificationSourceError
			item.result.ReasonCode = "desired_checksum_error"
			continue
		}
		item.result.DesiredChecksum = desiredChecksum

		if snapshot.Cluster == nil {
			item.result.Classification = ClassificationCreate
			continue
		}
		current := snapshot.Cluster
		if current.ClusterID != desired.ClusterID || current.ClusterType != desired.ClusterType {
			item.result.Classification = ClassificationConflict
			item.result.ReasonCode = "clinic_identity_mismatch"
			continue
		}
		if !immutableSourceFieldsMatch(current, &desired) {
			item.result.Classification = ClassificationConflict
			item.result.ReasonCode = "clinic_immutable_mismatch"
			continue
		}
		if hasTerminalMarker(current) && !resolved.Terminal {
			item.result.Classification = ClassificationConflict
			item.result.ReasonCode = "clinic_row_is_terminal_but_source_is_active"
			continue
		}
		beforeChecksum, err := clusterRecordChecksum(current)
		if err != nil {
			item.result.Classification = ClassificationDBError
			item.result.ReasonCode = "clinic_checksum_error"
			continue
		}
		item.result.BeforeChecksum = beforeChecksum
		changes, err := diffOwnedFields(item.candidate.Kind, current, &desired)
		if err != nil {
			item.result.Classification = ClassificationDBError
			item.result.ReasonCode = "clinic_diff_error"
			continue
		}
		item.result.Changes = changes
		if len(changes) == 0 {
			item.result.Classification = ClassificationNoop
			continue
		}
		item.result.Classification = ClassificationUpdate
	}

	for _, item := range queue {
		item.result.Candidate = item.candidate
		if item.resolved == nil || item.resolved.Parent == nil || !actionableOrNoop(item.result.Classification) {
			continue
		}
		parent := item.resolved.Parent
		item.result.DependencyIDs = []string{parent.ClusterID}
		parentItem := items[candidateKey(*parent)]
		if parentItem == nil || !actionableOrNoop(parentItem.result.Classification) {
			item.result.Classification = ClassificationBlockedParent
			item.result.ReasonCode = "parent_not_recoverable"
			item.result.BeforeChecksum = ""
			item.result.DesiredChecksum = ""
			item.result.Changes = nil
		}
	}

	results := make([]PlanResult, 0, len(queue))
	createActions := make([]Action, 0)
	updateActions := make([]Action, 0)
	counts := make(map[Classification]int)
	for _, item := range queue {
		result := item.result
		result.Candidate.Evidence = canonicalEvidence(result.Candidate.Evidence)
		results = append(results, result)
		counts[result.Classification]++
		sourceUpdatedAt := time.Time{}
		if result.SourceUpdatedAt != nil {
			sourceUpdatedAt = result.SourceUpdatedAt.UTC()
		}
		switch result.Classification {
		case ClassificationCreate:
			createActions = append(createActions, Action{
				Kind:            result.Candidate.Kind,
				ClusterID:       result.Candidate.ClusterID,
				TenantID:        result.Candidate.TenantID,
				ClusterType:     result.Candidate.ClusterType,
				Terminal:        item.resolved.Terminal,
				SourceUpdatedAt: sourceUpdatedAt,
				DesiredChecksum: result.DesiredChecksum,
				DependencyIDs:   result.DependencyIDs,
			})
		case ClassificationUpdate:
			updateActions = append(updateActions, Action{
				Kind:            result.Candidate.Kind,
				ClusterID:       result.Candidate.ClusterID,
				TenantID:        result.Candidate.TenantID,
				ClusterType:     result.Candidate.ClusterType,
				Terminal:        item.resolved.Terminal,
				SourceUpdatedAt: sourceUpdatedAt,
				BeforeChecksum:  result.BeforeChecksum,
				DesiredChecksum: result.DesiredChecksum,
				Fields:          changeFieldNames(result.Changes),
				DependencyIDs:   result.DependencyIDs,
			})
		}
	}
	sortPlanResults(results)

	baseManifest := Manifest{
		SchemaVersion: SchemaVersion,
		Environment:   StagingEnvironment,
		RunID:         runID,
		IncidentStart: seed.IncidentStart.UTC(),
		IncidentEnd:   seed.IncidentEnd.UTC(),
		GeneratedAt:   now,
		ExpiresAt:     now.Add(MaxManifestLifetime),
	}
	createManifest := baseManifest
	createManifest.MutationClass = MutationCreate
	createManifest.Actions = createActions
	if err := FinalizeManifest(&createManifest); err != nil {
		return nil, fmt.Errorf("finalize create manifest: %w", err)
	}
	updateManifest := baseManifest
	updateManifest.MutationClass = MutationUpdate
	updateManifest.Actions = updateActions
	if err := FinalizeManifest(&updateManifest); err != nil {
		return nil, fmt.Errorf("finalize update manifest: %w", err)
	}

	return &PlanOutput{
		Report: PlanReport{
			SchemaVersion: SchemaVersion,
			Environment:   StagingEnvironment,
			RunID:         runID,
			IncidentStart: seed.IncidentStart.UTC(),
			IncidentEnd:   seed.IncidentEnd.UTC(),
			GeneratedAt:   now,
			Results:       results,
			Counts:        counts,
		},
		CreateManifest: createManifest,
		UpdateManifest: updateManifest,
	}, nil
}

func classifySourceError(err error) (Classification, string) {
	switch {
	case errors.Is(err, ErrSourceNotFound):
		return ClassificationSkipNotFound, "source_not_found"
	case errors.Is(err, ErrSourceUnsupported):
		return ClassificationSkipUnsupported, "source_unsupported"
	case errors.Is(err, ErrSourceOwnership):
		return ClassificationConflict, "source_ownership_mismatch"
	case errors.Is(err, ErrSourceTypeMismatch):
		return ClassificationConflict, "source_type_mismatch"
	default:
		return ClassificationSourceError, "source_read_error"
	}
}

func candidateKey(candidate Candidate) string {
	key := string(candidate.Kind) + ":" + candidate.ClusterID + ":" + fmt.Sprintf("%d", candidate.TenantID)
	if candidate.Kind == ObjectKindUnsupported {
		key += ":" + candidate.ClusterType
	}
	return key
}

func mergeCandidate(item *planWorkItem, incoming Candidate) {
	if item.candidate.ClusterType == "" {
		item.candidate.ClusterType = incoming.ClusterType
	} else if incoming.ClusterType != "" && incoming.ClusterType != item.candidate.ClusterType {
		item.typeConflict = true
	}
	item.candidate.Evidence = canonicalEvidence(append(item.candidate.Evidence, incoming.Evidence...))
}

func canonicalEvidence(evidence []Evidence) []Evidence {
	copyEvidence := append([]Evidence(nil), evidence...)
	for i := range copyEvidence {
		copyEvidence[i].ObservedAt = copyEvidence[i].ObservedAt.UTC()
	}
	sort.Slice(copyEvidence, func(i, j int) bool {
		left, right := copyEvidence[i], copyEvidence[j]
		if left.Type != right.Type {
			return left.Type < right.Type
		}
		if !left.ObservedAt.Equal(right.ObservedAt) {
			return left.ObservedAt.Before(right.ObservedAt)
		}
		return left.ParentOf < right.ParentOf
	})
	result := copyEvidence[:0]
	for _, current := range copyEvidence {
		if len(result) > 0 {
			previous := result[len(result)-1]
			if previous.Type == current.Type && previous.ObservedAt.Equal(current.ObservedAt) && previous.ParentOf == current.ParentOf {
				continue
			}
		}
		result = append(result, current)
	}
	return result
}

func actionableOrNoop(classification Classification) bool {
	return classification == ClassificationCreate || classification == ClassificationUpdate || classification == ClassificationNoop
}

func sortPlanResults(results []PlanResult) {
	sort.SliceStable(results, func(i, j int) bool {
		left, right := results[i].Candidate, results[j].Candidate
		if left.Kind != right.Kind {
			return left.Kind == ObjectKindResourcePool
		}
		if left.ClusterID != right.ClusterID {
			return left.ClusterID < right.ClusterID
		}
		return left.TenantID < right.TenantID
	})
}

func newRunID() (string, error) {
	random := make([]byte, 6)
	if _, err := rand.Read(random); err != nil {
		return "", err
	}
	return "staging-" + time.Now().UTC().Format("20060102T150405Z") + "-" + hex.EncodeToString(random), nil
}

func timePointer(value time.Time) *time.Time {
	if value.IsZero() {
		return nil
	}
	canonical := value.UTC()
	return &canonical
}

func immutableSourceFieldsMatch(current, desired *db.Cluster) bool {
	if current == nil || desired == nil || current.CreatedAt.IsZero() || desired.CreatedAt.IsZero() {
		return false
	}
	return current.CreatedAt.UTC().Unix() == desired.CreatedAt.UTC().Unix()
}
