// Package roadmapsync owns the provider-neutral domain for matching pushed
// agent-roadmap Milestone changes back to the originating provider work item.
// The source of truth is the agent-roadmap on the develop branch (or remote
// develop), never a slot-local edit or an unpushed change.
package roadmapsync

import (
	"errors"
	"strings"

	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

var ErrInvalidIdentity = errors.New("invalid roadmap sync identity")

// Shape distinguishes a Milestone-level identity from a Task-level identity, as
// described in the contract sync identity note. The first slice only requires
// the Milestone shape for develop matching; Task is defined for forward
// compatibility with child work item sync.
type Shape string

const (
	ShapeMilestone Shape = "milestone"
	ShapeTask      Shape = "task"
)

func (s Shape) Valid() bool {
	switch s {
	case ShapeMilestone, ShapeTask:
		return true
	default:
		return false
	}
}

// Identity is the provider-neutral shape used to find and compare the same
// Milestone item regardless of which side changed. Field set follows the
// contract sync identity candidate (packages/contracts note 5.2). The minimal
// required match key is provider + work item id plus the Milestone path; the
// authoring run id is intentionally not a required match key in this slice.
type Identity struct {
	Shape                Shape               `json:"shape"`
	RoadmapMilestonePath string              `json:"roadmap_milestone_path"`
	RoadmapItemID        string              `json:"roadmap_item_id,omitempty"`
	Provider             workitem.ProviderID `json:"provider"`
	Tenant               string              `json:"tenant,omitempty"`
	Project              string              `json:"project,omitempty"`
	WorkItemID           string              `json:"work_item_id"`
	ParentWorkItemID     string              `json:"parent_work_item_id,omitempty"`
}

// Normalize trims fields and enforces the minimal required match key: a valid
// shape, a milestone path, a provider, and a work item id. Task shapes
// additionally require a parent work item id so a Task is never mistaken for a
// top-level Milestone. Missing required fields are a hard error rather than a
// silently accepted partial identity.
func (i Identity) Normalize() (Identity, error) {
	shape := Shape(strings.TrimSpace(string(i.Shape)))
	if shape == "" {
		shape = ShapeMilestone
	}
	if !shape.Valid() {
		return Identity{}, ErrInvalidIdentity
	}

	normalized := Identity{
		Shape:                shape,
		RoadmapMilestonePath: strings.TrimSpace(i.RoadmapMilestonePath),
		RoadmapItemID:        strings.TrimSpace(i.RoadmapItemID),
		Provider:             workitem.ProviderID(strings.TrimSpace(string(i.Provider))),
		Tenant:               strings.TrimSpace(i.Tenant),
		Project:              strings.TrimSpace(i.Project),
		WorkItemID:           strings.TrimSpace(i.WorkItemID),
		ParentWorkItemID:     strings.TrimSpace(i.ParentWorkItemID),
	}

	if normalized.RoadmapMilestonePath == "" ||
		normalized.Provider == "" ||
		normalized.WorkItemID == "" {
		return Identity{}, ErrInvalidIdentity
	}
	if normalized.Shape == ShapeTask && normalized.ParentWorkItemID == "" {
		return Identity{}, ErrInvalidIdentity
	}
	return normalized, nil
}

// Matches reports whether two identities refer to the same Milestone item. The
// comparison is on the normalized provider + work item id + milestone path, so
// callers must normalize first; an un-normalizable identity never matches.
func (i Identity) Matches(other Identity) bool {
	left, err := i.Normalize()
	if err != nil {
		return false
	}
	right, err := other.Normalize()
	if err != nil {
		return false
	}
	return left.Provider == right.Provider &&
		left.WorkItemID == right.WorkItemID &&
		left.RoadmapMilestonePath == right.RoadmapMilestonePath
}

// MatchesStrict reports whether two identities refer to the same item using the
// full provider-side identity key persisted by the identity map: provider,
// tenant, project, work item id, plus the Milestone path. Unlike Matches it
// also compares tenant and project, so a re-process in a different Plane
// workspace/project is never treated as the same identity. The retry decision
// uses this so it cannot reuse an identity across project-scoped sync targets.
// Callers must be able to normalize both sides; an un-normalizable identity
// never matches.
func (i Identity) MatchesStrict(other Identity) bool {
	left, err := i.Normalize()
	if err != nil {
		return false
	}
	right, err := other.Normalize()
	if err != nil {
		return false
	}
	return left.Provider == right.Provider &&
		left.Tenant == right.Tenant &&
		left.Project == right.Project &&
		left.WorkItemID == right.WorkItemID &&
		left.RoadmapMilestonePath == right.RoadmapMilestonePath
}

// Revision captures the branch and revision context that a scan observed, so a
// projection decision can record what develop state it was based on. Milestone
// sync defaults Branch to develop per the contract note.
type Revision struct {
	Branch   string `json:"roadmap_branch"`
	Revision string `json:"roadmap_revision,omitempty"`
}

// DefaultBranch is the source-of-truth branch for Milestone sync.
const DefaultBranch = "develop"

// Normalize defaults an empty branch to develop and trims the revision.
func (r Revision) Normalize() Revision {
	branch := strings.TrimSpace(r.Branch)
	if branch == "" {
		branch = DefaultBranch
	}
	return Revision{
		Branch:   branch,
		Revision: strings.TrimSpace(r.Revision),
	}
}

// OnDevelop reports whether the revision is anchored on the develop branch.
func (r Revision) OnDevelop() bool {
	return r.Normalize().Branch == DefaultBranch
}

// ScannedMilestone pairs a changed Milestone file path with the provider
// identity parsed out of that file. The develop gate matches the parsed
// identity against the expected work item identity, so a path change alone is
// never enough to project.
type ScannedMilestone struct {
	Path     string   `json:"path"`
	Identity Identity `json:"identity"`
}

// ScanResult is the input describing what a develop scan observed: the revision
// it looked at, the set of roadmap-relevant files that changed, and the
// Milestone files whose provider identity was parsed. ChangedFiles are
// repository-relative paths.
type ScanResult struct {
	Revision          Revision           `json:"revision"`
	ChangedFiles      []string           `json:"changed_files,omitempty"`
	ScannedMilestones []ScannedMilestone `json:"scanned_milestones,omitempty"`
}

// ProjectionCandidate is the typed result of matching a scan against an
// expected work item identity. Ready is only true when a pushed develop
// Milestone change matched the identity; Reason always explains the decision so
// a not-ready result is never silent.
type ProjectionCandidate struct {
	Identity Identity `json:"identity"`
	Revision Revision `json:"revision"`
	Ready    bool     `json:"ready"`
	Reason   string   `json:"reason"`
}
