package lib

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"strings"

	"github.com/devrites/devrites/internal/devritespaths"
	"github.com/devrites/devrites/internal/markdowntext"
	"github.com/devrites/devrites/internal/parallel"
	"github.com/devrites/devrites/internal/state"
	"github.com/devrites/devrites/internal/workflow"
)

// RunNext prints the minimal remaining lifecycle path using artifact presence
// where it proves completion, retaining phases that add no required artifact.
// Advisory only — the rite still owns the decision to advance — except the
// root-edit tripwire, which exits 3 while an unattributed product edit is in the
// tree. Observation never records lifecycle metrics.
func RunNext(root string, args []string, stdout, stderr io.Writer) int {
	slug, code, err := ActiveSlug(root, args)
	if err != nil {
		fmt.Fprintf(stderr, "next: %v\n", err)
		if code == 0 {
			code = 2
		}
		return code
	}
	featureDir, err := devritespaths.ExistingFeatureDirChecked(root, slug)
	if err != nil {
		fmt.Fprintf(stderr, "next: %v\n", err)
		return 2
	}
	report, err := state.Status(root, slug)
	if err != nil {
		fmt.Fprintf(stderr, "next: %v\n", err)
		return 2
	}
	openQuestions := countOpenQuestions(filepath.Join(featureDir, "questions.md"))

	policies := state.PhasePolicies()
	currentIdx := -1
	for i, policy := range policies {
		if policy.Target == report.Phase {
			currentIdx = i
			break
		}
	}
	if currentIdx < 0 {
		fmt.Fprintf(stderr, "next: unknown phase %q in state.md\n", report.Phase)
		return 2
	}

	fmt.Fprintf(stdout, "source: %s\n", slug)
	fmt.Fprintf(stdout, "current: %s\n", report.Phase)
	if report.Complete() {
		fmt.Fprintln(stdout, "current-status: complete")
	} else {
		fmt.Fprintln(stdout, "current-status: incomplete")
	}
	fmt.Fprintf(stdout, "open-questions: %d\n", openQuestions)
	// The root-edit tripwire (ADR-0038) leads the output and exits 3; the rest
	// still prints so the root sees where it resumes once the edit is routed.
	exit := 0
	if flagged, err := unattributedEdits(root, slug); err != nil {
		fmt.Fprintf(stdout, "blocked: tree baseline: %v\n", err)
		exit = 3
	} else if len(flagged) > 0 {
		fmt.Fprintf(stdout, "blocked: %s\n", unattributedMessage(slug, flagged))
		exit = 3
	}
	if afkPresent(root) {
		lines, err := FrontierLines(root, slug)
		if err != nil {
			fmt.Fprintf(stderr, "next: frontier: %v\n", err)
			return 2
		}
		for _, line := range lines {
			fmt.Fprintln(stdout, line)
		}
		mode, err := autocompleteMode(root)
		if err != nil {
			fmt.Fprintf(stderr, "next: %v\n", err)
			return 2
		}
		if mode && frontierGlobalPriority(slug, lines) {
			writeFrontierBatch(stdout, lines)
			fmt.Fprintln(stdout, "next-command: /rite-autocomplete")
			return exit
		}
		if mode && frontierSourcePreparationWait(slug, lines) {
			return exit
		}
	}
	if report.Phase == state.PhaseBuild {
		printPlanBatch(stdout, root, slug)
	}

	if report.Phase == state.PhaseDone {
		fmt.Fprintln(stdout, "next-phase: done")
		return exit
	}

	// Walk from the current phase to the first incomplete one. Vacuous phases
	// are reported as advisory skips, not silently dropped.
	var skips []string
	for i := currentIdx; i < len(policies); i++ {
		policy := policies[i]
		if policy.Target == state.PhaseDone {
			fmt.Fprintln(stdout, "next-phase: done")
			printSkips(stdout, skips)
			return exit
		}
		var missing []string
		if i == currentIdx {
			if report.Complete() && (currentIdx == 0 || addsArtifact(policies[currentIdx-1], policy) || policy.Target == state.PhaseConverge) {
				continue
			}
			missing = report.MissingFiles
		} else {
			missing = missingArtifacts(featureDir, policy.RequiredArtifacts)
			if policy.Target == state.PhaseClarify && openQuestions == 0 && len(missing) == 0 {
				skips = append(skips, "clarify (0 open questions)")
				continue
			}
			// A phase that adds no required artifact over the previous one
			// (build after vet, polish after prove) cannot be proven done by
			// artifact presence, so it is never skipped. Converge is the
			// exception: a conditional recovery pass that build hands past.
			if len(missing) == 0 && (addsArtifact(policies[i-1], policy) || policy.Target == state.PhaseConverge) {
				skips = append(skips, fmt.Sprintf("%s (artifacts complete)", policy.Target))
				continue
			}
		}
		fmt.Fprintf(stdout, "next-phase: %s\n", policy.Target)
		fmt.Fprintf(stdout, "next-command: %s\n", renderNextCommand(policy, slug))
		if policy.BlocksOpenQuestions && openQuestions > 0 && policy.Target != state.PhaseClarify {
			fmt.Fprintf(stdout, "blocked: %d open question(s) gate this phase\n", openQuestions)
		}
		if len(missing) > 0 {
			fmt.Fprintf(stdout, "missing: %s\n", strings.Join(missing, ","))
		}
		printSkips(stdout, skips)
		return exit
	}
	fmt.Fprintln(stdout, "next-phase: done")
	printSkips(stdout, skips)
	return exit
}

// printPlanBatch prints the slices runnable now and the batch `parallel select
// --from-plan` picks at the AFK max_parallel cap (else the maximum), with the
// command itself. Advisory: an unreadable plan prints why, never an exit code.
func printPlanBatch(stdout io.Writer, root, slug string) {
	capN := afkMaxParallel(root)
	if capN < 1 || capN > parallel.MaxParallelSlices {
		capN = parallel.MaxParallelSlices
	}
	eligible, batch, err := planBatch(root, slug, capN)
	if err != nil {
		fmt.Fprintf(stdout, "batch: unavailable (%v)\n", err)
		return
	}
	list := func(ids []string) string {
		if len(ids) == 0 {
			return "none"
		}
		return strings.Join(ids, ",")
	}
	fmt.Fprintf(stdout, "eligible_now: %s\n", list(eligible))
	fmt.Fprintf(stdout, "batch: %s\n", list(batch))
	fmt.Fprintf(stdout, "select-command: %s\n", planSelectCommand(filepath.Dir(root), slug, capN))
}

func renderNextCommand(policy state.PhasePolicy, slug string) string {
	verb := policy.ResumeVerb
	if verb == "" {
		return "(no resume command)"
	}
	return workflow.ForVerb(verb).SourceInvocation(slug)
}

func printSkips(stdout io.Writer, skips []string) {
	for _, skip := range skips {
		fmt.Fprintf(stdout, "skip: %s\n", skip)
	}
}

func addsArtifact(prev, policy state.PhasePolicy) bool {
	for _, artifact := range policy.RequiredArtifacts {
		if !slices.Contains(prev.RequiredArtifacts, artifact) {
			return true
		}
	}
	return false
}

func missingArtifacts(featureDir string, required []state.ArtifactPath) []string {
	var missing []string
	for _, artifact := range required {
		path := filepath.Join(featureDir, filepath.FromSlash(string(artifact)))
		info, err := os.Stat(path)
		if err != nil || info.Size() == 0 {
			missing = append(missing, string(artifact))
		}
	}
	return missing
}

var (
	openStatusLine = regexp.MustCompile(`(?i)^\s*status:\s*open\s*$`)
	openTableCell  = regexp.MustCompile(`(?i)\|\s*open\s*\|`)
)

// countOpenQuestions tolerates both the block form ("status: open") and the
// register table form ("| Q-001 | open | ... |"), outside fenced blocks.
func countOpenQuestions(path string) int {
	data, err := os.ReadFile(path) // #nosec G304 -- path is a validated workspace artifact
	if err != nil {
		return 0
	}
	// Fenced examples are not live questions. Text Structural rejects is
	// counted raw, so an unreadable register over-counts rather than hides.
	if visible, err := markdowntext.Structural(data); err == nil {
		data = visible
	}
	count := 0
	for _, line := range strings.Split(string(data), "\n") {
		if openStatusLine.MatchString(line) || openTableCell.MatchString(line) {
			count++
		}
	}
	return count
}
