package roadmapsync

import "strings"

// Step is the provider-neutral creation-cycle step the retry decision resumes
// from. The values mirror the persisted step ledger so a caller can read the
// ledger and pass the completed set in without re-mapping strings.
type Step string

const (
	StepDevelopMatched           Step = "develop_matched"
	StepOriginalCommentPreserved Step = "original_comment_preserved"
	StepPlaneBodyUpdated         Step = "plane_body_updated"
	StepPlaneTodoMoved           Step = "plane_todo_moved"
)

// OrderedSteps is the creation-cycle order. A reconcile resumes at the first
// step not present in the completed set.
var OrderedSteps = []Step{
	StepDevelopMatched,
	StepOriginalCommentPreserved,
	StepPlaneBodyUpdated,
	StepPlaneTodoMoved,
}

// Action is the next thing a creation cycle should do after reconciling the
// persisted identity and step ledger against the current trigger.
type Action string

const (
	// ActionResume runs the remaining steps starting at NextStep.
	ActionResume Action = "resume"
	// ActionComplete means every step is already recorded; nothing to do.
	ActionComplete Action = "complete"
	// ActionConflict means the trigger disagrees with the persisted identity
	// (revision mismatch) and a human/upstream decision is required instead of a
	// silent overwrite.
	ActionConflict Action = "conflict"
	// ActionSkipSelfMutation means the trigger was NomadCode's own mutation, so
	// it must not be treated as a new authoring trigger.
	ActionSkipSelfMutation Action = "skip_self_mutation"
)

// ExistingIdentity is the provider-neutral view of a persisted identity row the
// reconcile needs: the stored identity plus the revisions it was last written
// with. Found is false when no identity is persisted yet.
type ExistingIdentity struct {
	Found            bool
	Identity         Identity
	ProviderRevision string
	RoadmapRevision  string
}

// ReconcileInput carries the current trigger and the persisted state to
// reconcile it against. Existing and CompletedSteps come from the identity map
// and step ledger; an empty CompletedSteps means a fresh cycle.
type ReconcileInput struct {
	// Identity is the trigger's provider-neutral identity (the Plane ticket or
	// Milestone path being re-processed).
	Identity Identity
	// ProviderRevision / RoadmapRevision are the revisions the current trigger
	// observed. A mismatch against the persisted revisions is a conflict.
	ProviderRevision string
	RoadmapRevision  string

	// Actor is who caused this trigger. When it equals SelfActor the trigger is
	// NomadCode's own mutation and is not a new authoring trigger.
	Actor     string
	SelfActor string

	Existing       ExistingIdentity
	CompletedSteps map[Step]bool
}

// ReconcileResult is the typed decision: what to do next, the resume step when
// ActionResume, whether an existing identity was reused, and a reason that is
// always populated so a not-resume decision is never silent.
type ReconcileResult struct {
	Action       Action
	NextStep     Step
	IdentityKept bool
	Reason       string
}

// Reasons for a reconcile decision. Stable strings so a caller can branch on
// the cause.
const (
	reasonInvalidReconcileIdentity = "trigger identity is invalid or incomplete"
	reasonIdentityKeyMismatch      = "persisted identity key does not match trigger (provider/tenant/project/work item/path)"
	reasonSelfMutation             = "trigger actor is NomadCode self mutation, not a new authoring trigger"
	reasonProviderRevMismatch      = "trigger provider revision does not match persisted identity"
	reasonRoadmapRevMismatch       = "trigger roadmap revision does not match persisted identity"
	reasonAllStepsComplete         = "all creation cycle steps already completed"
	reasonResumeRemaining          = "resuming creation cycle at first incomplete step"
)

// ReconcileCreationCycle decides the next action for a (possibly re-processed)
// creation cycle. It is the idempotency join described by plan 05: it reuses an
// existing identity when the same Plane ticket or Milestone path is
// re-processed, refuses to overwrite on a revision mismatch, skips NomadCode's
// own mutations, and otherwise resumes at the first step not yet in the ledger.
//
// The function reads no DB itself; callers supply the persisted identity and
// completed-step set so the decision stays provider-neutral and unit-testable.
func ReconcileCreationCycle(in ReconcileInput) ReconcileResult {
	identity, err := in.Identity.Normalize()
	if err != nil {
		return ReconcileResult{Action: ActionConflict, Reason: reasonInvalidReconcileIdentity}
	}

	// Actor guard: a mutation NomadCode itself made (e.g. the body/title rewrite
	// it just projected) must not be re-interpreted as a fresh user trigger.
	self := strings.TrimSpace(in.SelfActor)
	if self != "" && strings.TrimSpace(in.Actor) == self {
		return ReconcileResult{Action: ActionSkipSelfMutation, Reason: reasonSelfMutation}
	}

	// Strict match: when a persisted identity exists it must agree on the full
	// provider-side key (provider, tenant, project, work item id, Milestone
	// path). A found-but-not-strictly-matching row means the trigger disagrees
	// with what is stored (e.g. a different Plane workspace/project for the same
	// work item id/path), which is a conflict rather than a silent fresh resume.
	if in.Existing.Found && !in.Existing.Identity.MatchesStrict(identity) {
		return ReconcileResult{Action: ActionConflict, Reason: reasonIdentityKeyMismatch}
	}
	identityKept := in.Existing.Found

	// Revision guard: when an identity already exists for this work item/path,
	// the trigger must observe the same revisions, otherwise resuming would
	// silently overwrite work projected from a different develop state.
	if identityKept {
		if mismatch(in.Existing.ProviderRevision, in.ProviderRevision) {
			return ReconcileResult{Action: ActionConflict, IdentityKept: true, Reason: reasonProviderRevMismatch}
		}
		if mismatch(in.Existing.RoadmapRevision, in.RoadmapRevision) {
			return ReconcileResult{Action: ActionConflict, IdentityKept: true, Reason: reasonRoadmapRevMismatch}
		}
	}

	next, ok := firstIncompleteStep(in.CompletedSteps)
	if !ok {
		return ReconcileResult{Action: ActionComplete, IdentityKept: identityKept, Reason: reasonAllStepsComplete}
	}
	return ReconcileResult{
		Action:       ActionResume,
		NextStep:     next,
		IdentityKept: identityKept,
		Reason:       reasonResumeRemaining,
	}
}

// firstIncompleteStep returns the earliest ordered step not present in the
// completed set. ok is false when every step is complete.
func firstIncompleteStep(completed map[Step]bool) (Step, bool) {
	for _, step := range OrderedSteps {
		if !completed[step] {
			return step, true
		}
	}
	return "", false
}

// mismatch reports whether a persisted revision and a trigger revision disagree.
// Empty values are treated as "unknown" and never conflict, so a first cycle
// that has not recorded a revision is not blocked.
func mismatch(persisted, trigger string) bool {
	p := strings.TrimSpace(persisted)
	t := strings.TrimSpace(trigger)
	if p == "" || t == "" {
		return false
	}
	return p != t
}
