// Command autonumber_seeds generates
// backend/internal/controlplane/packs/platform_config_autonumber_formats.go
// from data-specs/2026-04-18-autonumber-v2-seed-public.md.
//
// The markdown spec is the single canonical source (PRD
// docs/design/autonumber-engine-v2.md §8.1 C#5). This generator parses the
// Section 1+2 format tables, the Section 4 legacy-source table, and the
// Section 3 RecordType code table, then emits a Go file consumed by
// platform_config.go applyAutoNumberFormats().
//
// Usage (from repo root):
//
//	go run ./scripts/gen autonumber-seeds
//	# or via make generate (wired in Makefile)
//
// Hooked into `make verify-generated` so CI catches drift between the spec
// markdown and the generated Go file (Round-2 fixpack F17).
package main

import (
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

type autoNumberFormatSeed struct {
	ObjectName    string
	FieldAPIName  string
	Format        string
	Scope         string
	Start         int64
	LegacySource  string
	TokenRefsJSON string
}

// tokenRefsOverrides contains fields whose `{Object.Field}` tokens
// require an explicit multi-chain resolution that the parser cannot
// derive from the format string alone. Keys are "Object.FieldApiName";
// values are canonicalized JSON slices of CrossRecordRef (matching
// `autonumber.CrossRecordRef` wire format).
//
// Round-5 Codex V2-Codex-H6: Quote.Name needs the two-path fallback
// from `OpportunityId → Opportunity.AccountId → Account.BillingCountryCode`
// (primary) to `AccountId → Account.BillingCountryCode` (direct)
// because quotes in the wild are often attached to an Opportunity
// whose owning Account is different from (or substitutes for) any
// AccountId directly set on the Quote. The parser cannot infer the
// Opportunity hop from the format string alone — that hop is an
// explicit business convention inherited from the legacy
// CompoundAutoNumberListener.
//
// Contract.Name and Order.Name are omitted because their single
// implicit chain (`AccountId → Account.BillingCountryCode`) matches
// the parser-derived default — the engine falls back to the parser
// refs when TokenRefs is empty.
//
// Keep this map small. If the business rule grows, add a Section 2a
// "TokenRefs" table to the data-spec and drive this map from the
// markdown table.
// scopeOverrides sets the `_System_Field.auto_number_scope` value for
// fields whose counter must reset per-partition rather than running
// monotonically. Keys are "Object.FieldApiName".
//
// Rationale: NexusCRM's historical Apex triggers generated
// Quote.Name / Order.Name / Contract.Name / Account.Account_ID__c as
// `{partition}-{NN}` where the counter resets per unique partition
// (country + static prefix + YYMMDD, or IBG-B-YYYYMMDD). v2 honors
// that by assigning `auto_number_scope = 'per_prefix'` on these four
// fields; the engine derives the scope_key from the rendered non-
// counter portion of the format at reservation time (see
// autonumber.RenderScopePrefix).
//
// Other ~128 seeded fields remain `global` — their formats have no
// partitioning dimension and a single monotonic counter matches the
// Salesforce standard-autonumber contract.
var scopeOverrides = map[string]string{
	"Quote.Name":            "per_prefix",
	"Order.Name":            "per_prefix",
	"Contract.Name":         "per_prefix",
	"Account.Account_ID__c": "per_prefix",
}

var tokenRefsOverrides = map[string]string{
	"Quote.Name": `[` +
		`{` +
		`"directField":"Account_Country_Code__c",` +
		`"fkChains":[` +
		`[{"fk":"OpportunityId","targetField":"AccountId","targetObject":"Opportunity"},` +
		`{"fk":"AccountId","targetField":"BillingCountryCode","targetObject":"Account"}],` +
		`[{"fk":"AccountId","targetField":"BillingCountryCode","targetObject":"Account"}]` +
		`],` +
		`"readStrategy":"caller_txn",` +
		`"targetField":"BillingCountryCode",` +
		`"targetObject":"Account",` +
		`"tokenLiteral":"{Account.BillingCountryCode}"` +
		`}` +
		`]`,
}

type recordTypeCodeSeed struct {
	DeveloperName string
	Code          string
}

// main entrypoint. Subcommand argument is optional and exists for future
// expansion (e.g. additional spec kinds); `autonumber-seeds` is the default
// and only supported subcommand today.
func main() {
	specPath := flag.String("spec",
		"data-specs/2026-04-18-autonumber-v2-seed-public.md",
		"path to the autonumber seed data-spec markdown")
	outPath := flag.String("out",
		"backend/internal/controlplane/packs/platform_config_autonumber_formats.go",
		"path to write the generated Go file")
	flag.Parse()

	// Allow running from any directory: if the path is relative and not found
	// from CWD, try resolving relative to the repo root via git rev-parse.
	specAbs := resolveRepoPath(*specPath)
	outAbs := resolveRepoPath(*outPath)

	formats, rtCodes, err := parseSpec(specAbs)
	if err != nil {
		fmt.Fprintf(os.Stderr, "parse spec %s: %v\n", specAbs, err)
		os.Exit(1)
	}

	if len(formats) == 0 {
		fmt.Fprintln(os.Stderr, "no format rows parsed — refusing to emit empty seed file")
		os.Exit(1)
	}
	if err := writeGeneratedFile(outAbs, *specPath, formats, rtCodes); err != nil {
		fmt.Fprintf(os.Stderr, "write %s: %v\n", outAbs, err)
		os.Exit(1)
	}

	fmt.Printf("generated %s: %d format seeds, %d RecordType code seeds\n",
		outAbs, len(formats), len(rtCodes))
}

// resolveRepoPath returns an absolute path for p. If p is relative and not
// present relative to CWD, tries walking up looking for go.work or .git to
// anchor the repo root.
func resolveRepoPath(p string) string {
	if filepath.IsAbs(p) {
		return p
	}
	if _, err := os.Stat(p); err == nil {
		abs, _ := filepath.Abs(p)
		return abs
	}
	cwd, err := os.Getwd()
	if err != nil {
		return p
	}
	for dir := cwd; dir != "/" && dir != "."; dir = filepath.Dir(dir) {
		candidate := filepath.Join(dir, p)
		if _, err := os.Stat(candidate); err == nil {
			return candidate
		}
		// anchor on repo-root markers
		for _, marker := range []string{"go.work", ".git"} {
			if _, err := os.Stat(filepath.Join(dir, marker)); err == nil {
				return filepath.Join(dir, p)
			}
		}
	}
	return p
}

// parseSpec parses the data-spec markdown and returns the format + RecordType
// seed lists. The parser is intentionally strict about section headings to
// fail loudly when the spec is edited inconsistently.
func parseSpec(path string) ([]autoNumberFormatSeed, []recordTypeCodeSeed, error) {
	raw, err := os.ReadFile(path)
	if err != nil {
		return nil, nil, err
	}
	text := string(raw)

	// Extract tables by section heading.
	s1 := extractSection(text, "### 1. Seed", "### 2.")
	s2 := extractSection(text, "### 2. Seed", "### 3.")
	s3 := extractSection(text, "### 3. Seed", "### 4.")
	s4 := extractSection(text, "### 4. Per-field", "### 5.")

	if s1 == "" || s2 == "" || s3 == "" || s4 == "" {
		return nil, nil, fmt.Errorf("could not locate all four seed sections (1/2/3/4) in %s", path)
	}

	fmtRows, err := parseFormatTable(s1 + "\n" + s2)
	if err != nil {
		return nil, nil, fmt.Errorf("format table: %w", err)
	}
	rtRows, err := parseRecordTypeTable(s3)
	if err != nil {
		return nil, nil, fmt.Errorf("recordtype table: %w", err)
	}
	legacyMap, err := parseLegacySourceTable(s4)
	if err != nil {
		return nil, nil, fmt.Errorf("legacy-source table: %w", err)
	}

	// Join legacy source + token_refs overrides onto formats.
	for i := range fmtRows {
		key := fmtRows[i].ObjectName + "." + fmtRows[i].FieldAPIName
		src, ok := legacyMap[key]
		if !ok {
			return nil, nil, fmt.Errorf("field %s missing from Section 4 legacy-source map", key)
		}
		fmtRows[i].LegacySource = src
		if override, ok := scopeOverrides[key]; ok {
			fmtRows[i].Scope = override
		} else {
			fmtRows[i].Scope = "global"
		}
		if refs, ok := tokenRefsOverrides[key]; ok {
			fmtRows[i].TokenRefsJSON = refs
		}
	}

	// Stable order: object then field (already the data-spec order, but
	// enforce explicitly so output is deterministic regardless of table
	// reordering in the spec).
	sort.SliceStable(fmtRows, func(i, j int) bool {
		if fmtRows[i].ObjectName != fmtRows[j].ObjectName {
			return fmtRows[i].ObjectName < fmtRows[j].ObjectName
		}
		return fmtRows[i].FieldAPIName < fmtRows[j].FieldAPIName
	})

	return fmtRows, rtRows, nil
}

// extractSection returns the text between the first occurrence of `start` and
// the next occurrence of `end`, exclusive of both boundaries. Returns "" if
// either marker is absent.
func extractSection(text, start, end string) string {
	i := strings.Index(text, start)
	if i < 0 {
		return ""
	}
	rest := text[i+len(start):]
	j := strings.Index(rest, end)
	if j < 0 {
		return rest
	}
	return rest[:j]
}

// markdown table row of the shape:
//
//	| `value1` | `value2` | `value3` | number |
//
// tolerates whitespace and backticks around cells.
var tableRowRE = regexp.MustCompile(`^\|(.+)\|$`)

func splitTableRow(line string) []string {
	m := tableRowRE.FindStringSubmatch(strings.TrimSpace(line))
	if m == nil {
		return nil
	}
	parts := strings.Split(m[1], "|")
	out := make([]string, len(parts))
	for i, p := range parts {
		out[i] = strings.Trim(strings.TrimSpace(p), "`")
	}
	return out
}

// parseFormatTable extracts rows with columns (Object, Field, Format, Start).
// Skips header/separator rows (first two) and non-matching lines.
func parseFormatTable(text string) ([]autoNumberFormatSeed, error) {
	var out []autoNumberFormatSeed
	seen := make(map[string]bool)
	inTable := false
	separatorSeen := false
	for _, line := range strings.Split(text, "\n") {
		cols := splitTableRow(line)
		if cols == nil {
			if inTable && separatorSeen {
				// table ended; reset so later tables in same section don't
				// bleed in
				inTable = false
				separatorSeen = false
			}
			continue
		}
		if len(cols) != 4 {
			continue
		}
		// Header row contains "ObjectName" literally; separator row is all
		// dashes.
		if !inTable {
			if strings.EqualFold(cols[0], "ObjectName") {
				inTable = true
				continue
			}
			continue
		}
		if !separatorSeen {
			if strings.HasPrefix(cols[0], "---") {
				separatorSeen = true
				continue
			}
			// non-separator row after header — shouldn't happen, skip
			continue
		}
		// Real row
		start, err := strconv.ParseInt(cols[3], 10, 64)
		if err != nil {
			return nil, fmt.Errorf("row %q: parse start: %w", line, err)
		}
		key := cols[0] + "." + cols[1]
		if seen[key] {
			return nil, fmt.Errorf("duplicate field %s in format tables", key)
		}
		seen[key] = true
		out = append(out, autoNumberFormatSeed{
			ObjectName:   cols[0],
			FieldAPIName: cols[1],
			Format:       cols[2],
			Start:        start,
		})
	}
	return out, nil
}

// parseRecordTypeTable extracts rows with columns (DeveloperName, Code).
func parseRecordTypeTable(text string) ([]recordTypeCodeSeed, error) {
	var out []recordTypeCodeSeed
	seen := make(map[string]bool)
	inTable := false
	separatorSeen := false
	for _, line := range strings.Split(text, "\n") {
		cols := splitTableRow(line)
		if cols == nil {
			continue
		}
		if len(cols) != 2 {
			continue
		}
		if !inTable {
			// header row contains "DeveloperName"
			if strings.Contains(strings.ToLower(cols[0]), "developername") {
				inTable = true
				continue
			}
			continue
		}
		if !separatorSeen {
			if strings.HasPrefix(cols[0], "---") {
				separatorSeen = true
				continue
			}
			continue
		}
		if seen[cols[0]] {
			return nil, fmt.Errorf("duplicate RecordType DeveloperName %q", cols[0])
		}
		seen[cols[0]] = true
		out = append(out, recordTypeCodeSeed{
			DeveloperName: cols[0],
			Code:          cols[1],
		})
	}
	return out, nil
}

// parseLegacySourceTable extracts the Section 4 table into a map keyed by
// "Object.Field" with LegacySource as value.
func parseLegacySourceTable(text string) (map[string]string, error) {
	out := make(map[string]string)
	inTable := false
	separatorSeen := false
	for _, line := range strings.Split(text, "\n") {
		cols := splitTableRow(line)
		if cols == nil {
			continue
		}
		if len(cols) != 3 {
			continue
		}
		if !inTable {
			if strings.EqualFold(cols[0], "ObjectName") && strings.Contains(strings.ToLower(cols[2]), "legacysource") {
				inTable = true
				continue
			}
			continue
		}
		if !separatorSeen {
			if strings.HasPrefix(cols[0], "---") {
				separatorSeen = true
				continue
			}
			continue
		}
		key := cols[0] + "." + cols[1]
		if _, dup := out[key]; dup {
			return nil, fmt.Errorf("duplicate field %s in legacy-source table", key)
		}
		out[key] = cols[2]
	}
	return out, nil
}

// writeGeneratedFile emits the Go seed file.
func writeGeneratedFile(outPath, displaySpecPath string, formats []autoNumberFormatSeed, rtCodes []recordTypeCodeSeed) error {
	if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
		return err
	}
	f, err := os.Create(outPath)
	if err != nil {
		return err
	}
	defer f.Close()

	if err := renderGenerated(f, displaySpecPath, formats, rtCodes); err != nil {
		return err
	}
	return nil
}

func renderGenerated(w io.Writer, displaySpecPath string, formats []autoNumberFormatSeed, rtCodes []recordTypeCodeSeed) error {
	write := func(s string) error {
		_, err := io.WriteString(w, s)
		return err
	}

	if err := write(renderHeader(displaySpecPath)); err != nil {
		return err
	}

	if err := write("var AutoNumberFormatSeeds = []AutoNumberFormatSeed{\n"); err != nil {
		return err
	}
	for _, r := range formats {
		line := fmt.Sprintf("\t{ObjectName: %q, FieldApiName: %q, Format: %q, Scope: %q, Start: %d, LegacySource: %q, TokenRefsJSON: %q},\n",
			r.ObjectName, r.FieldAPIName, r.Format, r.Scope, r.Start, r.LegacySource, r.TokenRefsJSON)
		if err := write(line); err != nil {
			return err
		}
	}
	if err := write("}\n\n"); err != nil {
		return err
	}

	if err := write("var RecordTypeCodeSeeds = []RecordTypeCodeSeed{\n"); err != nil {
		return err
	}
	for _, r := range rtCodes {
		line := fmt.Sprintf("\t{Name: %q, Code: %q},\n", r.DeveloperName, r.Code)
		if err := write(line); err != nil {
			return err
		}
	}
	return write("}\n")
}

func renderHeader(displaySpecPath string) string {
	return fmt.Sprintf(`// Code generated by scripts/gen/autonumber_seeds.go; DO NOT EDIT.
//
// Source: %s (PRD §8.1 C#5).
// Regenerate via: make generate
// Drift is caught by: make verify-generated (Round-2 fixpack F17).

package packs

// AutoNumberFormatSeed configures a single autonumber field's format metadata
// for Lane C1 (Wave 1) seeding. Lane C2's SeedAutoNumberCounters uses
// LegacySource to compute scope-aware GREATEST for counter migration (Round-2
// fixpack F8).
//
// TokenRefsJSON (Round-5 Codex V2-Codex-H6) — if non-empty, applyAutoNumberFormats
// writes this canonical JSON into _System_Field.AutoNumberTokenRefs so the
// engine's MetadataSource picks up an explicit multi-chain that the parser
// cannot derive from the format string alone (e.g. Quote.Name's fallback
// through Opportunity.AccountId). Empty string leaves the column NULL; the
// engine then falls back to parser-derived refs.
type AutoNumberFormatSeed struct {
	ObjectName    string
	FieldApiName  string
	Format        string
	Scope         string
	Start         int64
	LegacySource  string
	TokenRefsJSON string
}

// RecordTypeCodeSeed seeds _System_RecordType.RecordTypeCode (Approach α per
// PRD §4.5). Ports contractTypeCodeForName keyword map from substring-scan
// form into exact-match column form.
//
// Name must match the "_System_RecordType.Name" value populated by
// ApplyRecordTypes from backend/internal/controlplane/packs/record_types.json.
// The column "DeveloperName" does not exist on "_System_RecordType"; SF-parity
// adds it as a follow-up. Using Name here keeps the seed minimum-change
// while still uniquely identifying each RecordType row.
type RecordTypeCodeSeed struct {
	Name string
	Code string
}

`, displaySpecPath)
}
