// Package gitosync bridges Gito `branch.updated` wakeup events to the Core
// Plane-origin Milestone creation sync. It is the layer that turns a wakeup
// signal into a verified develop scan and an EnqueueRoadmapCreationSync call:
// the scanner re-verifies the target branch revision from a local develop
// checkout (never trusting the event's changed_files alone), and the bridge
// matches each scanned Milestone to its Plane work item and enqueues a job.
package gitosync

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/nomadcode/nomadcode-core/internal/gitoevents"
	"github.com/nomadcode/nomadcode-core/internal/roadmapsync"
)

// ErrNotReady is returned by the scanner when the local develop checkout's
// remote revision does not yet match the event's `after`, so the pushed state
// the event announced is not observable locally and nothing should be enqueued.
var ErrNotReady = errors.New("gitosync: develop revision not ready")

// ErrDuplicateActiveSlug is returned when the scan detects duplicate active
// milestones for the same milestone ID (filename slug).
var ErrDuplicateActiveSlug = errors.New("gitosync: duplicate active milestone slugs")

// CommandRunner runs a git command and returns its stdout. It is the test seam
// that keeps the scanner unit-testable without a real git checkout; production
// wraps os/exec. trimming of the trailing newline is the caller's job.
type CommandRunner interface {
	Run(ctx context.Context, dir string, name string, args ...string) (string, error)
}

// ScannerConfig configures a BranchRevisionScanner. DevelopRepoPath is the
// local develop checkout the scan runs against; RemoteName is the git remote to
// fetch and resolve (default origin); Branch is the develop branch name.
type ScannerConfig struct {
	DevelopRepoPath string
	RemoteName      string
	Branch          string
}

func (c ScannerConfig) remote() string {
	if strings.TrimSpace(c.RemoteName) == "" {
		return "origin"
	}
	return c.RemoteName
}

func (c ScannerConfig) branch() string {
	if strings.TrimSpace(c.Branch) == "" {
		return gitoevents.DefaultBranch
	}
	return c.Branch
}

// ScannedMilestoneDoc pairs a scanned Milestone with the Markdown the scanner
// read at the target revision, so the bridge can both match the provider
// identity and project the develop content without re-reading git.
type ScannedMilestoneDoc struct {
	Path        string
	MilestoneID string
	Markdown    string
	HasIdentity bool
	Identity    roadmapsync.Identity
}

// ArchivedMilestoneCandidate records an archive Milestone path that shares the
// same filename-slug lookup space but must not be reconciled as an active item.
type ArchivedMilestoneCandidate struct {
	Path        string
	MilestoneID string
}

// ScanOutput is the verified result of a develop scan: the provider-neutral
// ScanResult the develop match gate consumes, the per-path active Milestone
// Markdown the bridge projects, and archive Milestone candidates kept only for
// future slug lookup/history diagnostics.
type ScanOutput struct {
	Result            roadmapsync.ScanResult
	Docs              []ScannedMilestoneDoc
	ArchiveCandidates []ArchivedMilestoneCandidate
}

// BranchRevisionScanner re-verifies a Gito branch event against the local
// develop checkout and produces a verified ScanResult. It never trusts the
// event's changed_files as the final set; those are only a wakeup hint.
type BranchRevisionScanner struct {
	runner CommandRunner
	cfg    ScannerConfig
}

// NewBranchRevisionScanner builds a scanner. The runner and a non-empty develop
// repo path are required.
func NewBranchRevisionScanner(runner CommandRunner, cfg ScannerConfig) (*BranchRevisionScanner, error) {
	if runner == nil {
		return nil, errors.New("gitosync: command runner is nil")
	}
	if strings.TrimSpace(cfg.DevelopRepoPath) == "" {
		return nil, errors.New("gitosync: develop repo path is required")
	}
	return &BranchRevisionScanner{runner: runner, cfg: cfg}, nil
}

// Scan fetches the remote develop branch, verifies its revision matches the
// event's `after`, indexes every active Milestone at that revision, and
// reads/parses each active Milestone. It returns ErrNotReady when the local
// remote revision does not yet match the event so a caller skips the enqueue
// instead of acting on stale state. Active Milestone files without a provider
// identity stay in Docs as missing-create candidates and remain in ChangedFiles
// so the match gate's reason stays accurate.
func (s *BranchRevisionScanner) Scan(ctx context.Context, ev gitoevents.BranchUpdatedEvent) (ScanOutput, error) {
	dir := s.cfg.DevelopRepoPath
	remote := s.cfg.remote()
	branch := s.cfg.branch()

	if _, err := s.runner.Run(ctx, dir, "git", "fetch", "--prune", remote, branch); err != nil {
		return ScanOutput{}, fmt.Errorf("gitosync: fetch failed: %w", err)
	}

	remoteRev, err := s.runner.Run(ctx, dir, "git", "rev-parse", remoteRef(remote, branch))
	if err != nil {
		return ScanOutput{}, fmt.Errorf("gitosync: rev-parse failed: %w", err)
	}
	remoteRev = strings.TrimSpace(remoteRev)
	if remoteRev == "" || remoteRev != strings.TrimSpace(ev.After) {
		// The local remote-tracking ref has not caught up to the revision the
		// event announced; acting now would scan stale content.
		return ScanOutput{}, ErrNotReady
	}

	out, err := s.runner.Run(ctx, dir, "git", "ls-tree", "-r", "--name-only", remoteRev, "--", "agent-roadmap/phase", "agent-roadmap/archive")
	if err != nil {
		return ScanOutput{}, fmt.Errorf("gitosync: active milestone index failed: %w", err)
	}

	changed := milestonePaths(out)
	archiveCandidates := archiveMilestoneCandidates(out)

	// Verify that there are no duplicate active milestone slugs.
	slugToPaths := make(map[string][]string)
	for _, path := range changed {
		slug := roadmapsync.MilestoneSlug(path)
		if slug == "" {
			continue
		}
		slugToPaths[slug] = append(slugToPaths[slug], path)
	}

	var dups []string
	for slug, paths := range slugToPaths {
		if len(paths) > 1 {
			dups = append(dups, fmt.Sprintf("%s (%s)", slug, strings.Join(paths, ", ")))
		}
	}
	if len(dups) > 0 {
		return ScanOutput{}, fmt.Errorf("%w: %s", ErrDuplicateActiveSlug, strings.Join(dups, "; "))
	}

	result := roadmapsync.ScanResult{
		Revision: roadmapsync.Revision{
			Branch:   branch,
			Revision: remoteRev,
		},
		ChangedFiles: changed,
	}

	var docs []ScannedMilestoneDoc
	for _, path := range changed {
		markdown, err := s.runner.Run(ctx, dir, "git", "show", ev.After+":"+path)
		if err != nil {
			return ScanOutput{}, fmt.Errorf("gitosync: show %q failed: %w", path, err)
		}
		identity, err := roadmapsync.ParseMilestoneIdentity(path, markdown)
		var hasIdentity bool
		var milestoneID string
		if err != nil {
			hasIdentity = false
			milestoneID = roadmapsync.MilestoneSlug(path)
		} else {
			hasIdentity = true
			milestoneID = identity.MilestoneID()
			scanned := roadmapsync.ScannedMilestone{Path: path, Identity: identity}
			result.ScannedMilestones = append(result.ScannedMilestones, scanned)
		}
		docs = append(docs, ScannedMilestoneDoc{
			Path:        path,
			MilestoneID: milestoneID,
			Markdown:    markdown,
			HasIdentity: hasIdentity,
			Identity:    identity,
		})
	}

	return ScanOutput{Result: result, Docs: docs, ArchiveCandidates: archiveCandidates}, nil
}

func remoteRef(remote, branch string) string {
	return fmt.Sprintf("refs/remotes/%s/%s", remote, branch)
}

// milestonePaths keeps only the lines that look like roadmap Milestone
// documents, normalizing each to a cleaned repo-relative path.
func milestonePaths(diffOutput string) []string {
	var out []string
	for _, line := range strings.Split(diffOutput, "\n") {
		path := strings.TrimSpace(line)
		if path == "" {
			continue
		}
		if isMilestonePath(path) {
			out = append(out, path)
		}
	}
	return out
}

func archiveMilestoneCandidates(treeOutput string) []ArchivedMilestoneCandidate {
	var out []ArchivedMilestoneCandidate
	for _, line := range strings.Split(treeOutput, "\n") {
		path := strings.TrimSpace(line)
		if path == "" || !isArchiveMilestonePath(path) {
			continue
		}
		id := roadmapsync.MilestoneSlug(path)
		if id == "" {
			continue
		}
		out = append(out, ArchivedMilestoneCandidate{Path: path, MilestoneID: id})
	}
	return out
}

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

// isArchiveMilestonePath reports whether a path matches an archived Milestone
// snapshot under agent-roadmap/archive/phase/<phase>/milestones/<file>.md.
func isArchiveMilestonePath(p string) bool {
	if !strings.HasSuffix(p, ".md") {
		return false
	}
	parts := strings.Split(strings.TrimPrefix(p, "./"), "/")
	if len(parts) != 6 {
		return false
	}
	return parts[0] == "agent-roadmap" &&
		parts[1] == "archive" &&
		parts[2] == "phase" &&
		parts[4] == "milestones"
}
