// Command boundarygate is a stdlib-only CI gate that enforces module-boundary
// invariants for the NexusCRM Go workspace:
//
//   - builds the first-party module dependency graph from every go.mod;
//   - detects strongly connected components (Tarjan) and fails on any NEW SCC
//     or new member of an allowlisted SCC;
//   - enforces a forbidden-edge policy (contract is a sink; api-host must not
//     pull backend/controlplane/engine; controlplane must not pull backend),
//     failing only on regressions beyond a recorded baseline;
//   - keeps published external L0 module versions synchronized between the
//     backend pin owner and the release-shaped host consumer modules;
//   - scans contract package source for database/sql / net/http leaks and L0
//     module source for product-dialect tokens, failing only on NEW leaks
//     beyond their baselines.
//
// Usage:
//
//	go run ./tools/boundarygate            # gate mode (CI): exit non-zero on a NEW violation
//	go run ./tools/boundarygate -update    # regenerate baselines + allowlist + ledger
//	go run ./tools/boundarygate -root DIR  # point at a different repo root
//
// The tool has its own module (github.com/nexuscrm/boundarygate) so it does not
// pollute the backend dependency graph, and uses only the Go standard library.
package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"sort"
)

const (
	acceptedSCCFile       = "accepted_sccs.txt"
	forbiddenBaselineFile = "forbidden_edge_baseline.txt"
	contractLeakFile      = "contract_runtime_leak_baseline.txt"
	l0DialectFile         = "l0_dialect_baseline.txt"
	ledgerRel             = "docs/architecture/boundary-ledger.md"
)

func main() {
	var (
		root   string
		update bool
	)
	flag.StringVar(&root, "root", "", "repo root (default: auto-detect from this tool's location)")
	flag.BoolVar(&update, "update", false, "regenerate baselines, accepted-SCC allowlist, and the ledger doc")
	flag.Parse()

	if root == "" {
		r, err := detectRepoRoot()
		if err != nil {
			fmt.Fprintln(os.Stderr, "boundarygate: cannot locate repo root:", err)
			os.Exit(2)
		}
		root = r
	}
	root, _ = filepath.Abs(root)

	toolDir := filepath.Join(root, "tools", "boundarygate")

	modules, warnings, err := discoverModules(root)
	if err != nil {
		fmt.Fprintln(os.Stderr, "boundarygate: discovery failed:", err)
		os.Exit(2)
	}

	// LOC per module (exclude nested module subtrees).
	var allDirs []string
	for _, m := range modules {
		if !m.Missing {
			allDirs = append(allDirs, m.Dir)
		}
	}
	for _, m := range modules {
		m.LOC = countLOC(m, allDirs)
	}

	sccs := tarjanSCC(modules)
	nonTrivial := nontrivialSCCs(sccs)
	edgeViolations := evaluateForbiddenEdges(modules)
	externalPinMismatches := evaluateExternalPinMismatches(modules)
	contractLeaks := scanContractRuntimeLeak(root, modules)
	dialectHits := scanL0Dialect(root, modules)

	if update {
		if err := writeBaselines(toolDir, nonTrivial, edgeViolations, contractLeaks, dialectHits); err != nil {
			fmt.Fprintln(os.Stderr, "boundarygate: writing baselines failed:", err)
			os.Exit(2)
		}
		if err := writeLedger(root, modules, sccs, nonTrivial, edgeViolations, contractLeaks, dialectHits); err != nil {
			fmt.Fprintln(os.Stderr, "boundarygate: writing ledger failed:", err)
			os.Exit(2)
		}
		fmt.Println("boundarygate: baselines + ledger regenerated.")
		return
	}

	// ---- Gate mode: compare against baselines, fail only on NEW violations. ----
	accepted, err := loadBaseline(filepath.Join(toolDir, acceptedSCCFile))
	must(err)
	edgeBaseline, err := loadBaseline(filepath.Join(toolDir, forbiddenBaselineFile))
	must(err)
	leakBaseline, err := loadBaseline(filepath.Join(toolDir, contractLeakFile))
	must(err)
	dialectBaseline, err := loadDialectBaseline(filepath.Join(toolDir, l0DialectFile))
	must(err)

	// Build the set of allowlisted SCC members for "new member" detection.
	acceptedMembers := map[string]bool{}
	for key := range accepted {
		for _, mem := range splitSCCKey(key) {
			acceptedMembers[mem] = true
		}
	}

	var failures []string

	// SCC checks.
	for _, s := range nonTrivial {
		if accepted[s.Key()] {
			continue
		}
		// Either a brand-new SCC, or an accepted SCC that gained/lost a member.
		var newMembers []string
		for _, mem := range s.Members {
			if !acceptedMembers[mem] {
				newMembers = append(newMembers, mem)
			}
		}
		if len(newMembers) == 0 {
			// Same members as some accepted SCC(s) but key differs (merge/split).
			failures = append(failures, fmt.Sprintf(
				"SCC changed shape (not allowlisted): {%s}", join(s.Members, ", ")))
		} else {
			failures = append(failures, fmt.Sprintf(
				"NEW cyclic SCC or new member(s) %v in component {%s}", newMembers, join(s.Members, ", ")))
		}
	}

	// Forbidden-edge checks.
	for _, v := range edgeViolations {
		if !edgeBaseline[v.Key()] {
			failures = append(failures, fmt.Sprintf("NEW forbidden edge: %s (%s)", v.Key(), v.Rule))
		}
	}

	for _, mismatch := range externalPinMismatches {
		failures = append(failures, "external module pin mismatch: "+mismatch.String())
	}

	// Contract runtime-leak checks.
	for _, f := range contractLeaks {
		if !leakBaseline[f] {
			failures = append(failures, fmt.Sprintf("NEW contract runtime leak (database/sql or net/http): %s", f))
		}
	}

	// L0 dialect-token checks (token-aware: a file frozen with a token set still
	// fails when a NEW dialect token appears in it; a bare-path legacy entry
	// waives the whole file).
	for _, h := range dialectHits {
		newToks := dialectNewTokens(h, dialectBaseline)
		if len(newToks) == 0 {
			continue
		}
		if _, frozen := dialectBaseline[h.File]; frozen {
			failures = append(failures, fmt.Sprintf("NEW L0 dialect-token %v in frozen file (baseline waives only its listed tokens): %s", newToks, h.File))
		} else {
			failures = append(failures, fmt.Sprintf("NEW L0 dialect-token hit %v: %s", newToks, h.File))
		}
	}

	printSummary(modules, nonTrivial, accepted, edgeViolations, edgeBaseline, contractLeaks, leakBaseline, dialectHits, dialectBaseline, warnings)
	fmt.Printf("\nExternal published-module pin mismatches: %d\n", len(externalPinMismatches))
	for _, mismatch := range externalPinMismatches {
		fmt.Println("  [NEW] " + mismatch.String())
	}

	if len(failures) > 0 {
		fmt.Println()
		fmt.Println("BOUNDARY GATE: FAIL")
		sort.Strings(failures)
		for _, f := range failures {
			fmt.Println("  ✗ " + f)
		}
		fmt.Printf("\n%d new (non-baselined) violation(s). Run `go run ./tools/boundarygate -update` only to ACCEPT an intended boundary change.\n", len(failures))
		os.Exit(1)
	}
	fmt.Println()
	fmt.Println("BOUNDARY GATE: PASS (only baselined violations remain)")
}

func must(err error) {
	if err != nil {
		fmt.Fprintln(os.Stderr, "boundarygate:", err)
		os.Exit(2)
	}
}

// detectRepoRoot walks up from the tool's own directory (or cwd) looking for a
// go.work file, which marks the workspace root.
func detectRepoRoot() (string, error) {
	// Prefer the executable-independent path: start from this source file's dir
	// via cwd, since `go run` sets cwd to the invocation dir.
	start, err := os.Getwd()
	if err != nil {
		return "", err
	}
	dir := start
	for {
		if _, err := os.Stat(filepath.Join(dir, "go.work")); err == nil {
			return dir, nil
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return "", fmt.Errorf("no go.work found walking up from %s", start)
		}
		dir = parent
	}
}

func writeBaselines(toolDir string, sccs []SCC, edges []EdgeViolation, leaks []string, dialect []DialectHit) error {
	// Accepted SCCs.
	var sccLines []string
	for _, s := range sccs {
		sccLines = append(sccLines, s.Key())
	}
	if err := writeLines(filepath.Join(toolDir, acceptedSCCFile),
		"# Accepted strongly-connected components (Tarjan over first-party modules).\n"+
			"# One SCC per line; members joined by '|'. The gate FAILS on any NEW SCC or\n"+
			"# a new member joining an existing one. Regenerate only to ACCEPT an\n"+
			"# intended cycle: `go run ./tools/boundarygate -update`.",
		sccLines); err != nil {
		return err
	}

	// Forbidden-edge baseline.
	var edgeLines []string
	for _, v := range edges {
		edgeLines = append(edgeLines, v.Key())
	}
	if err := writeLines(filepath.Join(toolDir, forbiddenBaselineFile),
		"# Baselined forbidden edges that exist TODAY (from -> to). The gate fails\n"+
			"# only on NEW forbidden edges beyond this list. Shrink this file as the\n"+
			"# offending dependencies are cut.",
		edgeLines); err != nil {
		return err
	}

	// Contract runtime leak baseline.
	if err := writeLines(filepath.Join(toolDir, contractLeakFile),
		"# Contract package files that currently import database/sql or net/http.\n"+
			"# The contract module is the neutral sink and must not carry runtime deps.\n"+
			"# The gate fails on NEW leak files; PR1 will SHRINK this baseline to empty.",
		leaks); err != nil {
		return err
	}

	// L0 dialect baseline. Format: "<file>\t<token,token>" — the gate waives ONLY
	// the listed tokens, so a NEW token in an already-frozen file still fails.
	var dialectLines []string
	for _, h := range dialect {
		dialectLines = append(dialectLines, dialectBaselineLine(h))
	}
	if err := writeLines(filepath.Join(toolDir, l0DialectFile),
		"# L0 module source files that currently contain a product-dialect token\n"+
			"# (salesforce|soql|sosl|sobject|hubspot|opportunity|tooling, case-insensitive).\n"+
			"# Format: \"<file>\\t<token,token>\" — the gate waives ONLY the listed tokens,\n"+
			"# so a NEW token added to a frozen file still fails. A bare \"<file>\" line\n"+
			"# (legacy) waives the whole file. This FREEZES the known dialect leak so it\n"+
			"# can't grow while the Phase-2 carve happens; the baseline may only shrink.",
		dialectLines); err != nil {
		return err
	}
	return nil
}

func splitSCCKey(key string) []string {
	return splitNonEmpty(key, '|')
}
