// Command task-channel-authority-inventory.go is the structural production
// inventory for every non-test SQL expression that references scope_memberships.
// It uses Go AST plus constant folding (including concatenated string literals)
// to enumerate each callsite, normalizes the SQL (lowercasing, whitespace
// collapsing, and identifier substitution so alias renames cannot bypass the
// check), and classifies each digest against a committed inventory. Any new,
// changed, or unclassified occurrence fails by default.
//
// Classification categories:
//
//   - authoritative_path: the authoritative Task->board Channel resolver and the
//     active Agent channel-membership ACL in internal/store/access/authorizer.go.
//   - state_only_task: the exact state-only Task allowlist (watch/notification/
//     unread/read-cursor/participant projection materialization) documented in
//     the guardrail README.
//   - reviewed_unrelated: reviewed unrelated scope use (channel/dm/thread
//     membership for delivery, notification, rewind, team, service-account, and
//     other non-Task surfaces) that is not a Task authorization gate.
//
// Every digest below is the sha256 of the normalized SQL fragment. The
// committed inventory is exact: an occurrence whose (file, function) is new or
// whose normalized digest is missing from the committed set fails the guardrail.
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"flag"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
)

type inventorySite struct {
	file string
	fn   string
	line int
}

// inventoryEntry is one committed scope_memberships SQL occurrence with its
// classification and callsite metadata. The digest key preserves the security
// fact; the classification names whether the site is authoritative_path,
// task_state_only (exact state-only Task allowlist), or reviewed_unrelated.
type inventoryEntry struct {
	Classification string `json:"classification"`
	File           string `json:"file"`
	Function       string `json:"function"`
	Line           int    `json:"line"`
}

// InventoryClassification enumerates the committed classification categories.
const (
	ClassificationAuthoritativePath = "authoritative_path"
	ClassificationTaskStateOnly      = "task_state_only"
	ClassificationReviewedUnrelated  = "reviewed_unrelated"
)

type checker struct {
	root      string
	fset      *token.FileSet
	failed    bool
	fails     []string
	inventory map[string]inventoryEntry // committed digests with classification metadata
	policy    map[string]string         // reviewed policy: per-occurrence signature -> reviewed classification
}

func newChecker(root string) *checker {
	c := &checker{
		root:      root,
		fset:      token.NewFileSet(),
		inventory: committedInventory(),
		policy:    reviewedPolicy(),
	}
	return c
}

func (c *checker) fail(format string, args ...any) {
	c.failed = true
	c.fails = append(c.fails, fmt.Sprintf(format, args...))
}

// occurrenceSignature is the stable per-exact-AST-callsite signature. The
// security-preserving digest is part of the identity, so a classification or
// digest change at the same file::function::line is a NEW signature and never
// matches a committed record. Multiple differently-classified callsites inside
// one function have distinct line::digest signatures and are independently
// policy-checked.
func occurrenceSignature(rel, fn string, line int, d string) string {
	return rel + "::" + fn + "::" + fmt.Sprintf("%d", line) + "::" + d
}

// sqlIdentRe matches SQL identifiers (table names, column names, aliases) so the
// normalized form replaces them with a placeholder. SQL keywords and the table
// name scope_memberships are preserved as structural tokens.
var sqlIdentRe = regexp.MustCompile(`\b[a-zA-Z_][a-zA-Z0-9_]*\b`)

var sqlKeywordSet = map[string]bool{
	"select": true, "from": true, "where": true, "join": true, "left": true,
	"right": true, "inner": true, "outer": true, "on": true, "and": true,
	"or": true, "not": true, "in": true, "is": true, "null": true, "between": true,
	"like": true, "exists": true, "case": true, "when": true, "then": true,
	"else": true, "end": true, "as": true, "insert": true, "into": true,
	"values": true, "update": true, "set": true, "delete": true, "create": true,
	"table": true, "drop": true, "alter": true, "order": true, "by": true,
	"group": true, "having": true, "limit": true, "offset": true, "union": true,
	"all": true, "distinct": true, "with": true, "for": true,
	"asc": true, "desc": true, "true": true, "false": true, "using": true,
	"cast": true, "coalesce": true, "greatest": true, "least": true,
	"concat": true, "substring": true, "nullif": true, "count": true,
	"sum": true, "min": true, "max": true, "avg": true, "abs": true,
	"now": true, "current": true, "timestamp": true, "date": true,
	"if": true, "any": true, "some": true, "unknown": true,
}

// securityRelevantTokens are the topology tables and columns whose presence is
// the security fact for Task authorization. The structural digest preserves
// these exact tokens so a Channel query changed to a direct Task membership in
// the same file/function produces a different digest and fails the inventory.
// Generic aliases (sm, tb, m, ...) and unrelated columns are normalized.
var securityRelevantTokens = map[string]bool{
	"scope_memberships":  true,
	"scope_membership":   true,
	"scope_id":           true,
	"member_id":          true,
	"task_scope_id":      true,
	"channel_id":         true,
	"board_id":           true,
	"task_id":            true,
	"tasks":              true,
	"task_boards":        true,
	"task_scope":         true,
	"channel_scope":      true,
	"source_scope":       true,
	"task_board":         true,
	"task_read_source":   true,
	"task_read_board":    true,
	"parent_scope_id":    true,
	"owner_member_id":    true,
	"effective_to_seq":   true,
	"effective_from_seq": true,
	"removed_at":         true,
	"archived_at":        true,
	"visibility":         true,
	"status":             true,
	"type":               true,
	"scope_type":         true,
}

// normalizeSQL lowercases, collapses whitespace, and normalizes generic
// identifiers (SQL keywords and security-relevant topology tokens are
// preserved verbatim; unrelated identifiers become a placeholder). The digest
// therefore retains the exact security fact (which table/column carries the
// membership) while still being invariant under generic alias renames.
func normalizeSQL(raw string) string {
	lower := strings.ToLower(raw)
	lower = strings.ReplaceAll(lower, "`", "")
	lower = regexp.MustCompile(`\s+`).ReplaceAllString(lower, " ")
	return sqlIdentRe.ReplaceAllStringFunc(lower, func(word string) string {
		if securityRelevantTokens[word] {
			return word
		}
		if sqlKeywordSet[word] {
			return word
		}
		return "?i"
	})
}

func digest(sql string) string {
	sum := sha256.Sum256([]byte(normalizeSQL(sql)))
	return hex.EncodeToString(sum[:])
}

// foldExpr constant-folds string expressions: basic string literals and '+' of
// strings (including backtick raw strings). It returns the folded literal value
// with surrounding quotes stripped.
func foldExpr(expr ast.Expr) string {
	switch e := expr.(type) {
	case *ast.BasicLit:
		if e.Kind == token.STRING {
			return e.Value
		}
	case *ast.BinaryExpr:
		if e.Op == token.ADD {
			left := foldExpr(e.X)
			right := foldExpr(e.Y)
			if left != "" && right != "" {
				return left + right
			}
		}
	case *ast.ParenExpr:
		return foldExpr(e.X)
	}
	return ""
}

// stripQuotes removes Go string delimiters (backticks or double quotes) around a
// folded literal value.
func stripQuotes(v string) string {
	v = strings.TrimSpace(v)
	if len(v) >= 2 {
		if (v[0] == '`' && v[len(v)-1] == '`') || (v[0] == '"' && v[len(v)-1] == '"') {
			return v[1 : len(v)-1]
		}
	}
	return v
}

func (c *checker) walkProductionGo(fn func(rel string, file *ast.File)) {
	roots := []string{"internal", "cmd", "desktop"}
	for _, relativeRoot := range roots {
		walkRoot := filepath.Join(c.root, relativeRoot)
		if _, err := os.Stat(walkRoot); err != nil {
			if !os.IsNotExist(err) {
				c.fail("inspect %s: %v", relativeRoot, err)
			}
			continue
		}
		err := filepath.WalkDir(walkRoot, func(path string, entry os.DirEntry, walkErr error) error {
			if walkErr != nil {
				return walkErr
			}
			if entry.IsDir() {
				switch entry.Name() {
				case ".git", "node_modules", "vendor":
					return filepath.SkipDir
				}
				return nil
			}
			if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
				return nil
			}
			parsed, err := parser.ParseFile(c.fset, path, nil, parser.SkipObjectResolution)
			if err != nil {
				return fmt.Errorf("parse %s: %w", path, err)
			}
			rel, err := filepath.Rel(c.root, path)
			if err != nil {
				return err
			}
			fn(filepath.ToSlash(rel), parsed)
			return nil
		})
		if err != nil {
			c.fail("walk production Go under %s: %v", relativeRoot, err)
		}
	}
}

// extractSQLStrings returns every constant-folded string expression in a file
// that references scope_memberships, with the enclosing function name.
func (c *checker) extractSQLStrings(file *ast.File) []struct {
	fn   string
	line int
	sql  string
} {
	var out []struct {
		fn   string
		line int
		sql  string
	}
	var currentFn string
	ast.Inspect(file, func(n ast.Node) bool {
		switch node := n.(type) {
		case *ast.FuncDecl:
			currentFn = node.Name.Name
		case *ast.CallExpr:
			// Query/Exec string arguments are usually the first argument.
			for _, arg := range node.Args {
				val := stripQuotes(foldExpr(arg))
				if val != "" && strings.Contains(val, "scope_memberships") {
					out = append(out, struct {
						fn   string
						line int
						sql  string
					}{fn: currentFn, line: c.lineOf(arg), sql: val})
				}
			}
		case *ast.AssignStmt:
			for _, rhs := range node.Rhs {
				val := stripQuotes(foldExpr(rhs))
				if val != "" && strings.Contains(val, "scope_memberships") {
					out = append(out, struct {
						fn   string
						line int
						sql  string
					}{fn: currentFn, line: c.lineOf(rhs), sql: val})
				}
			}
		case *ast.ReturnStmt:
			for _, res := range node.Results {
				val := stripQuotes(foldExpr(res))
				if val != "" && strings.Contains(val, "scope_memberships") {
					out = append(out, struct {
						fn   string
						line int
						sql  string
					}{fn: currentFn, line: c.lineOf(res), sql: val})
				}
			}
		}
		return true
	})
	return out
}

func (c *checker) lineOf(n ast.Node) int {
	return c.fset.Position(n.Pos()).Line
}

func (c *checker) check() {
	c.walkProductionGo(func(rel string, file *ast.File) {
		for _, site := range c.extractSQLStrings(file) {
			d := digest(site.sql)
			sig := occurrenceSignature(rel, site.fn, site.line, d)
			c.checkOccurrence(rel, site.fn, site.line, d, sig)
		}
	})
	c.checkInventoryBijection()
	c.checkPolicyBijection()
}

// checkWithSets is the testable core of the fail-closed three-way bijection. It
// checks every supplied actual occurrence against the committed inventory and
// policy maps, then checks both reverse directions (inventory->actual and
// policy->actual). The self-tests construct/remove/add/misclassify these sets
// and assert the exact failure modes; the production check() builds the actual
// set from the AST walk and delegates here.
func (c *checker) checkWithSets(actual []occurrence, inventory map[string]inventoryEntry, policy map[string]string) {
	actualBySig := make(map[string]occurrence, len(actual))
	for _, occ := range actual {
		if _, dup := actualBySig[occ.signature]; dup {
			c.fail("%s:%d %s: duplicate actual occurrence signature %s", occ.rel, occ.line, occ.fn, occ.signature)
			continue
		}
		actualBySig[occ.signature] = occ
	}

	// Every actual occurrence must have exactly one committed inventory record
	// and exactly one independent policy decision; all three must agree.
	for _, occ := range actual {
		sig := occ.signature
		entry, committed := inventory[sig]
		if !committed {
			c.fail("%s:%d %s: actual occurrence has no committed inventory record (digest %s)", occ.rel, occ.line, occ.fn, occ.digest)
			continue
		}
		if entry.Function != occ.fn {
			c.fail("%s:%d %s: committed inventory function %q does not match actual callsite function %q", occ.rel, occ.line, occ.fn, entry.Function, occ.fn)
		}
		if entry.Line != occ.line {
			c.fail("%s:%d %s: committed inventory line %d does not match actual callsite line %d", occ.rel, occ.line, occ.fn, entry.Line, occ.line)
		}
		reviewed, hasPolicy := policy[sig]
		if !hasPolicy {
			// Never accept an inventory occurrence merely because policy lookup is
			// absent: an un-reviewed direct-membership callsite must fail closed.
			c.fail("%s:%d %s: committed inventory occurrence has no independent policy decision", occ.rel, occ.line, occ.fn)
			continue
		}
		if reviewed != entry.Classification {
			c.fail("%s:%d %s: generated inventory classification %q does not match reviewed policy %q", occ.rel, occ.line, occ.fn, entry.Classification, reviewed)
		}
	}

	// Every committed inventory record must correspond to an actual occurrence
	// (stale inventory fails). The per-occurrence signature already embeds the
	// digest, so a record keyed at the actual signature is by construction the
	// same security fact; a record keyed elsewhere is stale and fails below.
	for sig, entry := range inventory {
		if _, ok := actualBySig[sig]; !ok {
			c.fail("%s:%d %s: stale inventory record has no actual occurrence (digest %s)", entry.File, entry.Line, entry.Function, sig)
		}
	}

	// Every policy decision must correspond to an actual occurrence and a
	// committed inventory record (stale policy fails; duplicate/sibling policy
	// with no inventory fails).
	for sig, reviewed := range policy {
		if _, ok := actualBySig[sig]; !ok {
			c.fail("stale policy decision %q has no actual occurrence (classification %q)", sig, reviewed)
			continue
		}
		entry, committed := inventory[sig]
		if !committed {
			c.fail("policy decision %q has no committed inventory record (classification %q)", sig, reviewed)
			continue
		}
		if reviewed != entry.Classification {
			c.fail("policy decision %q classification %q does not match committed inventory %q", sig, reviewed, entry.Classification)
		}
	}
}

// checkOccurrence validates one actual occurrence against the committed
// inventory and policy (forward direction).
func (c *checker) checkOccurrence(rel, fn string, line int, d string, sig string) {
	entry, committed := c.inventory[sig]
	if !committed {
		c.fail("%s:%d %s: unclassified scope_memberships SQL callsite (digest %s)", rel, line, fn, d)
		return
	}
	if entry.Function != fn {
		c.fail("%s:%d %s: committed inventory function %q does not match actual callsite function %q", rel, line, fn, entry.Function, fn)
	}
	if entry.Line != line {
		c.fail("%s:%d %s: committed inventory line %d does not match actual callsite line %d", rel, line, fn, entry.Line, line)
	}
	reviewed, hasPolicy := c.policy[sig]
	if !hasPolicy {
		c.fail("%s:%d %s: committed inventory occurrence has no independent policy decision", rel, line, fn)
		return
	}
	if reviewed != entry.Classification {
		c.fail("%s:%d %s: generated inventory classification %q does not match reviewed policy %q", rel, line, fn, entry.Classification, reviewed)
	}
}

// checkInventoryBijection asserts every committed inventory record corresponds
// to an actual occurrence (stale inventory fails) and its digest matches.
func (c *checker) checkInventoryBijection() {
	actual := c.collectActual()
	actualBySig := make(map[string]bool, len(actual))
	for _, occ := range actual {
		actualBySig[occ.signature] = true
	}
	for sig, entry := range c.inventory {
		if !actualBySig[sig] {
			c.fail("%s:%d %s: stale inventory record has no actual occurrence (digest %s)", entry.File, entry.Line, entry.Function, sig)
		}
	}
}

// checkPolicyBijection asserts every policy decision corresponds to an actual
// occurrence and a committed inventory record (stale/duplicate policy fails).
func (c *checker) checkPolicyBijection() {
	actual := c.collectActual()
	actualBySig := make(map[string]bool, len(actual))
	for _, occ := range actual {
		actualBySig[occ.signature] = true
	}
	for sig, reviewed := range c.policy {
		if !actualBySig[sig] {
			c.fail("stale policy decision %q has no actual occurrence (classification %q)", sig, reviewed)
			continue
		}
		entry, committed := c.inventory[sig]
		if !committed {
			c.fail("policy decision %q has no committed inventory record (classification %q)", sig, reviewed)
			continue
		}
		if reviewed != entry.Classification {
			c.fail("policy decision %q classification %q does not match committed inventory %q", sig, reviewed, entry.Classification)
		}
	}
}

// occurrence is one actual scope_memberships SQL occurrence discovered by the
// AST walk, with its stable per-callsite signature.
type occurrence struct {
	rel       string
	fn        string
	line      int
	digest    string
	signature string
}

// collectActual walks production Go and returns every actual occurrence with its
// per-callsite signature.
func (c *checker) collectActual() []occurrence {
	var actual []occurrence
	c.walkProductionGo(func(rel string, file *ast.File) {
		for _, site := range c.extractSQLStrings(file) {
			d := digest(site.sql)
			sig := occurrenceSignature(rel, site.fn, site.line, d)
			actual = append(actual, occurrence{
				rel: rel, fn: site.fn, line: site.line, digest: d, signature: sig,
			})
		}
	})
	return actual
}

func (c *checker) report() int {
	if !c.failed {
		fmt.Println("task-channel-authority inventory guardrail passed.")
		return 0
	}
	sort.Strings(c.fails)
	for _, fail := range c.fails {
		fmt.Fprintln(os.Stderr, "FAIL:", fail)
	}
	fmt.Fprintf(os.Stderr, "\ntask-channel-authority inventory guardrail failed with %d issue(s).\n", len(c.fails))
	return 1
}

func main() {
	root := flag.String("root", ".", "repository root")
	flag.Parse()
	code := 0

	c := newChecker(*root)
	c.check()
	if c.report() != 0 {
		code = 1
	}

	// Per-mutator/callsite closure: every Task mutation entry must reach its
	// declared fence before any durable write; a new mutator without a fence,
	// a sibling relying on an ordinary membership read, a stale/duplicate/
	// misclassified entry, or a fence after a side effect fails closed.
	mc := newMutatorChecker(*root)
	mc.check()
	if mc.failed {
		sort.Strings(mc.fails)
		for _, fail := range mc.fails {
			fmt.Fprintln(os.Stderr, "FAIL:", fail)
		}
		fmt.Fprintf(os.Stderr, "\ntask-channel-authority mutator closure guardrail failed with %d issue(s).\n", len(mc.fails))
		code = 1
	} else {
		fmt.Println("task-channel-authority mutator closure guardrail passed.")
	}

	os.Exit(code)
}
