// Package synth generates gnark circuit source code from a metamodel.Schema.
//
// It emits Go source as plain text; imports must be stdlib + metamodel only so
// the synthesizer has no dependency on gnark itself (gnark lives in the
// generated code, not here).
//
// Output is always deterministic: actions are processed in schema order,
// fields are emitted in canonical order, map iteration is sorted. This is
// required for the `make gen-circuits && git diff --exit-code` CI gate.
package synth

import (
	"fmt"
	"go/format"
	"strings"

	"github.com/stackdump/bitwrap-io/internal/metamodel"
)

// Generate emits Go source for every action in the schema that has a circuit
// generator registered. The returned string is a complete, go-formatted file
// beginning with `package <pkg>`.
//
// Actions without a generator are silently skipped — that's expected during
// the phased rollout (slice 2.1 ships Mint only; later slices add more).
func Generate(schema *metamodel.Schema, pkg string) (string, error) {
	if schema == nil {
		return "", fmt.Errorf("synth: nil schema")
	}
	if pkg == "" {
		return "", fmt.Errorf("synth: package name required")
	}

	var body strings.Builder
	imports := map[string]bool{
		"github.com/consensys/gnark/frontend": true,
	}

	generated := 0
	for i := range schema.Actions {
		action := &schema.Actions[i]
		gen := registry[action.ID]
		if gen == nil {
			continue
		}
		if !supportsSchema(action.ID, schema) {
			// Action-name collision across schemas (e.g. ERC-20 "transfer"
			// on balances vs ERC-5725 "transfer" on vesting NFTs). Silently
			// skip when the shape doesn't match.
			continue
		}
		if err := gen(&body, schema, action, imports); err != nil {
			return "", fmt.Errorf("synth: generating %q: %w", action.ID, err)
		}
		generated++
	}

	if generated == 0 {
		return "", fmt.Errorf("synth: schema has no synthesizable actions")
	}

	var out strings.Builder
	out.WriteString("// Code generated by bitwrap synth; DO NOT EDIT.\n")
	out.WriteString(fmt.Sprintf("// Source schema: %s %s\n\n", schema.Name, schema.Version))
	out.WriteString(fmt.Sprintf("package %s\n\n", pkg))

	if len(imports) > 0 {
		out.WriteString("import (\n")
		for _, imp := range sortedKeys(imports) {
			out.WriteString(fmt.Sprintf("\t%q\n", imp))
		}
		out.WriteString(")\n\n")
	}

	out.WriteString(body.String())

	// Format through go/format so output is gofmt-stable — the Makefile's
	// `gen-circuits && git diff --exit-code` gate requires byte-identical
	// output on every regeneration.
	formatted, err := format.Source([]byte(out.String()))
	if err != nil {
		return "", fmt.Errorf("synth: gofmt on generated source: %w", err)
	}
	return string(formatted), nil
}

// generator is the per-action code-emitting function signature. It writes the
// struct + Define() method for one action into `body` and adds any extra
// imports to `imports`.
type generator func(body *strings.Builder, schema *metamodel.Schema, action *metamodel.Action, imports map[string]bool) error

// registry maps action IDs to their generators. Populated by package init()
// in the per-action generator files (mint.go, etc.) via register().
var registry = map[string]generator{}

func register(actionID string, gen generator) {
	if _, dup := registry[actionID]; dup {
		panic(fmt.Sprintf("synth: duplicate generator for action %q", actionID))
	}
	registry[actionID] = gen
}

// supportsSchema returns true if the named action's generator can handle the
// given schema. Used to skip action-name collisions between template families.
// State lookup is case-insensitive so schemas from both Go-built templates
// (lowercase `balances`, `allowances`) and the .btw DSL (uppercase `BALANCES`,
// `ALLOWANCES`) work without a separate canonicalization step.
func supportsSchema(actionID string, schema *metamodel.Schema) bool {
	switch actionID {
	case "mint", "burn", "transfer", "transferFrom":
		return stateByIDCI(schema, "balances") != nil
	case "approve":
		return stateByIDCI(schema, "allowances") != nil
	case "claim":
		return stateByIDCI(schema, "schedules") != nil && stateByIDCI(schema, "owners") != nil
	default:
		return true
	}
}

// stateByIDCI is a case-insensitive variant of Schema.StateByID. Shared
// between supportsSchema and the per-action generators so they all accept
// the same inputs.
func stateByIDCI(schema *metamodel.Schema, name string) *metamodel.State {
	for i := range schema.States {
		if strings.EqualFold(schema.States[i].ID, name) {
			return &schema.States[i]
		}
	}
	return nil
}

func sortedKeys(m map[string]bool) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	// Sort for deterministic output.
	for i := 1; i < len(keys); i++ {
		for j := i; j > 0 && keys[j-1] > keys[j]; j-- {
			keys[j-1], keys[j] = keys[j], keys[j-1]
		}
	}
	return keys
}
