//go:build ags_inventory

// Command check-authority-aware-inventory generates the AGS migration inventory.
// It deliberately inventories the seams (route, client, token, collaborator,
// event) rather than treating every textual "ags" mention as an owner. Each
// occurrence carries an authority classification. New or changed occurrences
// fail closed until explicitly reviewed with --update; this prevents a new
// AGS caller from silently becoming production authority.
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
)

type occurrence struct {
	Category       string `json:"category"`
	File           string `json:"file"`
	Line           int    `json:"line"`
	Symbol         string `json:"symbol"`
	Authority      string `json:"authority"`
	Classification string `json:"classification"`
	Digest         string `json:"digest"`
}

type report struct {
	Version             int    `json:"version"`
	GeneratedBy         string `json:"generatedBy"`
	OccurrenceCount     int    `json:"occurrenceCount"`
	OccurrencesDigest   string `json:"occurrencesDigest"`
	ActiveAGSOwnerCount int    `json:"activeAGSOwnerCount"`
}

var patterns = []struct {
	name string
	re   *regexp.Regexp
}{
	{"route", regexp.MustCompile(`(?i)(/api/(?:v3|graphql|ext/v1)|workspaceAGS|agsserver|repositoryroute|git-smart-http)`)},
	{"client", regexp.MustCompile(`(?i)(workspaceAGSPath|buildMeshXPath\([^\n]*?/api/v3|/meshx/api/(?:repos|issues|pulls|releases)|AGSHTTPRepositoryStore)`)},
	{"token", regexp.MustCompile(`(?i)(AGSToken|ags_rat_|repository[_ -]?access[_ -]?token|MESHX_AGS_.*TOKEN)`)},
	{"collaborator", regexp.MustCompile(`(?i)(/collaborators|AGS.*collaborat|collaborat.*AGS)`)},
	{"event", regexp.MustCompile(`(?i)(AGS.*event|event.*AGS|AGS.*webhook|webhook.*AGS)`)},
}
var funcRE = regexp.MustCompile(`^\s*func\s+(?:\([^)]*\)\s*)?([A-Za-z_][A-Za-z0-9_]*)`)

func digest(s string) string {
	h := sha256.Sum256([]byte(strings.TrimSpace(s)))
	return hex.EncodeToString(h[:])
}
func key(o occurrence) string {
	// Line numbers are intentionally excluded: adding an unrelated import or
	// wiring statement must not turn every later occurrence into drift. The
	// source file, category, containing symbol, and normalized line digest still
	// identify the authority seam and detect edits/new callers.
	return fmt.Sprintf("%s:%s:%s:%s", o.Category, o.File, o.Symbol, o.Digest)
}

func classify(rel, line string) (authority, classification string) {
	// Existing compatibility routes/clients/tokens are retained solely for the
	// migration read/audit window. They are never an active native writer.
	// A future source occurrence is still rejected because it is not in the
	// committed generated inventory.
	_ = rel
	// Only an explicit native-owner/AGS-owner co-reference is ambiguous. Names
	// such as nativeRequest are ordinary request variables and are not enough
	// to claim two authorities.
	lower := strings.ToLower(line)
	if strings.Contains(lower, "contentstore") && strings.Contains(lower, "ags") {
		return "ambiguous", "ambiguous"
	}
	return "legacy_ags", "migration_only"
}

func scan(root string) ([]occurrence, error) {
	var out []occurrence
	roots := []string{"internal", "cmd", "desktop/internal", "web/src"}
	for _, rr := range roots {
		base := filepath.Join(root, rr)
		if _, err := os.Stat(base); err != nil {
			continue
		}
		err := filepath.Walk(base, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if info.IsDir() {
				if info.Name() == "node_modules" || info.Name() == "vendor" {
					return filepath.SkipDir
				}
				return nil
			}
			ext := filepath.Ext(path)
			if ext != ".go" && ext != ".ts" && ext != ".tsx" && ext != ".js" && ext != ".jsx" {
				return nil
			}
			if strings.HasSuffix(path, "_test.go") || strings.Contains(filepath.Base(path), ".test.") || strings.Contains(path, "/testdata/") {
				return nil
			}
			b, e := os.ReadFile(path)
			if e != nil {
				return e
			}
			lines := strings.Split(string(b), "\n")
			symbol := ""
			rel, _ := filepath.Rel(root, path)
			rel = filepath.ToSlash(rel)
			for i, line := range lines {
				if m := funcRE.FindStringSubmatch(line); len(m) > 1 {
					symbol = m[1]
				}
				for _, p := range patterns {
					if !p.re.MatchString(line) {
						continue
					}
					a, c := classify(rel, line)
					out = append(out, occurrence{Category: p.name, File: rel, Line: i + 1, Symbol: symbol, Authority: a, Classification: c, Digest: digest(line)})
				}
			}
			return nil
		})
		if err != nil {
			return nil, err
		}
	}
	sort.Slice(out, func(i, j int) bool {
		a, b := out[i], out[j]
		if a.File != b.File {
			return a.File < b.File
		}
		if a.Line != b.Line {
			return a.Line < b.Line
		}
		return a.Category < b.Category
	})
	return out, nil
}

func load(path string) (report, error) {
	b, e := os.ReadFile(path)
	if os.IsNotExist(e) {
		return report{Version: 1}, nil
	}
	if e != nil {
		return report{}, e
	}
	var r report
	e = json.Unmarshal(b, &r)
	return r, e
}
func write(path string, r report) error {
	b, e := json.MarshalIndent(r, "", "  ")
	if e != nil {
		return e
	}
	b = append(b, '\n')
	return os.WriteFile(path, b, 0644)
}

func occurrencesDigest(items []occurrence) string {
	keys := make([]string, 0, len(items))
	for _, item := range items {
		keys = append(keys, key(item))
	}
	sort.Strings(keys)
	h := sha256.Sum256([]byte(strings.Join(keys, "\n")))
	return hex.EncodeToString(h[:])
}

func runClassifierSelfTest() error {
	a, c := classify("fixture.go", "contentStore AGS owner")
	if a != "ambiguous" || c != "ambiguous" {
		return fmt.Errorf("ambiguous authority was not rejected")
	}
	a, c = classify("fixture.go", "legacy AGS token")
	if a != "legacy_ags" || c != "migration_only" {
		return fmt.Errorf("migration-only classification changed")
	}
	return nil
}

func main() {
	root := flag.String("root", ".", "repository root")
	update := flag.Bool("update", false, "rewrite generated inventory")
	selfTest := flag.Bool("self-test", false, "run fail-closed classifier self-test")
	flag.Parse()
	if *selfTest {
		if err := runClassifierSelfTest(); err != nil {
			fmt.Fprintf(os.Stderr, "self-test: %v\n", err)
			os.Exit(1)
		}
		fmt.Println("authority-aware AGS inventory self-test passed")
		return
	}
	abs, _ := filepath.Abs(*root)
	if err := runClassifierSelfTest(); err != nil {
		fmt.Fprintf(os.Stderr, "self-test: %v\n", err)
		os.Exit(1)
	}
	path := filepath.Join(abs, "guardrails", "ags", "authority-aware-inventory.json")
	got, err := scan(abs)
	if err != nil {
		fmt.Fprintf(os.Stderr, "inventory scan: %v\n", err)
		os.Exit(1)
	}
	active := 0
	for _, o := range got {
		if o.Classification == "active" {
			active++
		}
	}
	want := report{Version: 1, GeneratedBy: "guardrails/ags/check-authority-aware-inventory.go", OccurrenceCount: len(got), OccurrencesDigest: occurrencesDigest(got), ActiveAGSOwnerCount: active}
	if *update {
		if err := write(path, want); err != nil {
			fmt.Fprintf(os.Stderr, "write inventory: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("authority-aware AGS inventory updated: occurrences=%d active_ags_owner=%d\n", len(got), active)
		return
	}
	have, err := load(path)
	if err != nil {
		fmt.Fprintf(os.Stderr, "read inventory: %v\n", err)
		os.Exit(1)
	}
	// Compare stable keys and classifications. Unknown/ambiguous callers fail
	// closed; a changed digest is also a new occurrence.
	gotDigest := occurrencesDigest(got)
	if have.OccurrenceCount != len(got) || have.OccurrencesDigest != gotDigest {
		fmt.Fprintf(os.Stderr, "authority-aware AGS inventory drift (fail-closed): expected count=%d digest=%s, got count=%d digest=%s\n", have.OccurrenceCount, have.OccurrencesDigest, len(got), gotDigest)
		os.Exit(1)
	}
	for _, o := range got {
		if o.Authority == "ambiguous" || o.Classification == "ambiguous" || o.Authority == "unknown" {
			fmt.Fprintf(os.Stderr, "authority-aware AGS inventory is not fail-closed at %s:%d (%s)\n", o.File, o.Line, o.Category)
			os.Exit(1)
		}
	}
	if have.ActiveAGSOwnerCount != 0 || active != 0 {
		fmt.Fprintf(os.Stderr, "active AGS owner count must be zero: generated=%d committed=%d\n", active, have.ActiveAGSOwnerCount)
		os.Exit(1)
	}
	fmt.Printf("authority-aware AGS inventory passed: occurrences=%d active_ags_owner=%d\n", len(got), active)
}
