package roadmapsync

import (
	"path"
	"strings"
)

// MatchInput is the input to the develop projection gate: the scan result a
// sync layer observed on branches/develop or remote develop, plus the expected
// work item identity carried from the originating Plane ticket. The expected
// identity supplies the Milestone path and provider/work item id the pushed
// change must match.
type MatchInput struct {
	Scan     ScanResult
	Expected Identity
}

// Reasons explaining a not-ready projection decision. These are stable strings
// so a sync layer can branch on the cause without re-deriving it.
const (
	reasonReady             = "pushed develop milestone matched expected identity"
	reasonInvalidIdentity   = "expected identity is invalid or incomplete"
	reasonNotOnDevelop      = "scan revision is not on the develop branch"
	reasonNoRevision        = "scan has no pushed develop revision"
	reasonNoMilestoneFile   = "no changed agent-roadmap milestone file matched the expected milestone path"
	reasonNoScannedIdentity = "no scanned milestone provider identity for the expected milestone path"
	reasonIdentityMismatch  = "scanned milestone identity does not match the expected work item identity"
)

// MatchDevelopMilestone decides whether a pushed/scanned develop change is
// projection-ready for the expected Plane work item. It is ready only when:
//   - the expected identity normalizes (provider + work item id + milestone path),
//   - the scan revision is anchored on develop (not a slot-local or feature branch),
//   - the scan carries an actual pushed revision,
//   - one of the changed files is the expected Milestone file under
//     agent-roadmap/phase/*/milestones/*.md, and
//   - the scanned Milestone at that path carries a provider identity that
//     matches the expected work item identity.
//
// A slot-local path or push-failure scan supplies no develop revision and so is
// never ready. A path-only match whose Milestone has no provider identity, or a
// different work item id/provider, is not ready. The reason is always
// populated.
func MatchDevelopMilestone(in MatchInput) ProjectionCandidate {
	revision := in.Scan.Revision.Normalize()

	expected, err := in.Expected.Normalize()
	if err != nil {
		return notReady(in.Expected, revision, reasonInvalidIdentity)
	}

	if !revision.OnDevelop() {
		return notReady(expected, revision, reasonNotOnDevelop)
	}
	if revision.Revision == "" {
		return notReady(expected, revision, reasonNoRevision)
	}
	if !changedFilesContainMilestone(in.Scan.ChangedFiles, expected.RoadmapMilestonePath) {
		return notReady(expected, revision, reasonNoMilestoneFile)
	}

	scanned, ok := scannedIdentityForPath(in.Scan.ScannedMilestones, expected.RoadmapMilestonePath)
	if !ok {
		// The path changed but no parsed provider identity backs it, so the
		// pushed Milestone cannot be tied to the expected work item.
		return notReady(expected, revision, reasonNoScannedIdentity)
	}
	if !expected.Matches(scanned) {
		return notReady(expected, revision, reasonIdentityMismatch)
	}

	return ProjectionCandidate{
		Identity: expected,
		Revision: revision,
		Ready:    true,
		Reason:   reasonReady,
	}
}

// scannedIdentityForPath returns the normalized parsed identity of the scanned
// Milestone at the expected path. It reports ok=false when no scanned Milestone
// matches the path or its identity does not normalize, so a present-but-empty
// identity is treated as missing rather than silently matched.
func scannedIdentityForPath(scanned []ScannedMilestone, expectedPath string) (Identity, bool) {
	want := normalizeRepoPath(expectedPath)
	if want == "" {
		return Identity{}, false
	}
	for _, sm := range scanned {
		if normalizeRepoPath(sm.Path) != want {
			continue
		}
		identity, err := sm.Identity.Normalize()
		if err != nil {
			return Identity{}, false
		}
		return identity, true
	}
	return Identity{}, false
}

func notReady(identity Identity, revision Revision, reason string) ProjectionCandidate {
	return ProjectionCandidate{
		Identity: identity,
		Revision: revision,
		Ready:    false,
		Reason:   reason,
	}
}

// changedFilesContainMilestone reports whether any changed file is the expected
// Milestone path and sits under the agent-roadmap milestones layout. The
// expected path must itself be a milestone-shaped path; matching is exact on
// the cleaned repository-relative path so an unrelated edit elsewhere never
// counts.
func changedFilesContainMilestone(changedFiles []string, expectedPath string) bool {
	want := normalizeRepoPath(expectedPath)
	if want == "" || !isMilestonePath(want) {
		return false
	}
	for _, f := range changedFiles {
		if normalizeRepoPath(f) == want {
			return true
		}
	}
	return false
}

// isMilestonePath reports whether a cleaned repo-relative path matches
// agent-roadmap/phase/<phase>/milestones/<file>.md.
func isMilestonePath(p string) bool {
	if !strings.HasSuffix(p, ".md") {
		return false
	}
	parts := strings.Split(p, "/")
	if len(parts) != 5 {
		return false
	}
	return parts[0] == "agent-roadmap" &&
		parts[1] == "phase" &&
		parts[3] == "milestones"
}

func normalizeRepoPath(p string) string {
	trimmed := strings.TrimSpace(p)
	if trimmed == "" {
		return ""
	}
	cleaned := path.Clean(strings.TrimPrefix(trimmed, "./"))
	if cleaned == "." || strings.HasPrefix(cleaned, "..") {
		return ""
	}
	return cleaned
}
