// Command gen-upc-snapshot reads paliad's live deadline corpus and
// writes the UPC subset as JSON files under
// pkg/litigationplanner/embedded/upc/. The package's embedded
// catalog/holiday/court implementations then serve this data without
// any DB roundtrip — letting youpc.org (or any future consumer) run
// the litigationplanner engine against the canonical UPC rule set.
//
// Slice C (m/paliad#124 §19). See docs/design-litigation-planner-2026-05-26.md
// §19 for the full design.
//
// Usage:
//
//	DATABASE_URL=postgres://... go run ./cmd/gen-upc-snapshot \
//	    [-output ./pkg/litigationplanner/embedded/upc] \
//	    [-version 2026-05-26-1] \
//	    [-source-label paliad-dev-supabase]
//
// The generator applies migrations against DATABASE_URL before
// SELECTing (so the snapshot always matches schema HEAD). Idempotent —
// running twice with the same DB state produces the same JSON.
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"

	"mgit.msbls.de/m/paliad/internal/db"
	"mgit.msbls.de/m/paliad/pkg/litigationplanner"
)

const (
	defaultOutput      = "./pkg/litigationplanner/embedded/upc"
	defaultSourceLabel = ""
)

// Meta is the version block written to meta.json. The embedded sub-
// package re-defines this type so consumers can decode it without
// importing the cmd; the cmd holds the canonical write shape.
type Meta struct {
	Version           string    `json:"version"`
	GeneratedAt       time.Time `json:"generated_at"`
	PaliadCommit      string    `json:"paliad_commit,omitempty"`
	SourceDBLabel     string    `json:"source_db_label,omitempty"`
	RuleCount         int       `json:"rule_count"`
	ProceedingCount   int       `json:"proceeding_count"`
	TriggerEventCount int       `json:"trigger_event_count"`
	HolidayCount      int       `json:"holiday_count"`
	CourtCount        int       `json:"court_count"`
}

// EmbeddedHoliday is the holiday row shape the embedded snapshot
// stores. JSON tags mirror paliad.holidays so the generator's SELECT
// scans onto it directly + the embedded HolidayCalendar reads the
// same tag.
type EmbeddedHoliday struct {
	Date        string  `db:"date_iso" json:"date"`
	Name        string  `db:"name" json:"name"`
	Country     *string `db:"country" json:"country,omitempty"`
	Regime      *string `db:"regime" json:"regime,omitempty"`
	State       *string `db:"state" json:"state,omitempty"`
	HolidayType string  `db:"holiday_type" json:"holiday_type"`
}

// EmbeddedCourt is the court row shape the embedded snapshot stores.
type EmbeddedCourt struct {
	ID        string  `db:"id" json:"id"`
	Code      string  `db:"code" json:"code"`
	NameDE    string  `db:"name_de" json:"name_de"`
	NameEN    string  `db:"name_en" json:"name_en"`
	Country   string  `db:"country" json:"country"`
	Regime    *string `db:"regime" json:"regime,omitempty"`
	CourtType string  `db:"court_type" json:"court_type"`
	ParentID  *string `db:"parent_id" json:"parent_id,omitempty"`
	SortOrder int     `db:"sort_order" json:"sort_order"`
}

func main() {
	output := flag.String("output", defaultOutput, "directory to write JSON files into")
	version := flag.String("version", "", "explicit snapshot version (auto-derived if empty)")
	sourceLabel := flag.String("source-label", defaultSourceLabel, "label for source_db in meta.json")
	flag.Parse()

	url := os.Getenv("DATABASE_URL")
	if url == "" {
		log.Fatal("DATABASE_URL must be set")
	}

	if err := db.ApplyMigrations(url); err != nil {
		log.Fatalf("apply migrations: %v", err)
	}

	pool, err := sqlx.Connect("postgres", url)
	if err != nil {
		log.Fatalf("connect: %v", err)
	}
	defer pool.Close()

	ctx := context.Background()
	if err := run(ctx, pool, *output, *version, *sourceLabel); err != nil {
		log.Fatalf("snapshot: %v", err)
	}
}

func run(ctx context.Context, pool *sqlx.DB, output, version, sourceLabel string) error {
	if err := os.MkdirAll(output, 0o755); err != nil {
		return fmt.Errorf("mkdir output: %w", err)
	}

	// 1. Proceeding types — UPC primaries only. The unified upc.apl row
	// from B1 mig 134 is included; the 3 archived old appeal codes
	// (is_active=false) are filtered out by the is_active predicate.
	// The kind='proceeding' predicate (mig 153, t-paliad-325) belts the
	// is_active filter so phase/side_action/meta rows can't slip into
	// the embedded catalog even if some future deploy re-activates one
	// for an admin task.
	var procs []litigationplanner.ProceedingType
	if err := pool.SelectContext(ctx, &procs, `
		SELECT id, code, name, name_en, description, jurisdiction,
		       category, default_color, sort_order, is_active,
		       trigger_event_label_de, trigger_event_label_en,
		       appeal_target
		  FROM paliad.proceeding_types
		 WHERE jurisdiction = 'UPC'
		   AND is_active = true
		   AND kind = 'proceeding'
		 ORDER BY sort_order, id`); err != nil {
		return fmt.Errorf("select proceeding_types: %w", err)
	}

	if len(procs) == 0 {
		return fmt.Errorf("no active UPC proceeding_types — refusing to write empty snapshot")
	}

	procIDs := make([]int, 0, len(procs))
	for _, p := range procs {
		procIDs = append(procIDs, p.ID)
	}

	// 2. Deadline rules — published + active rules for those proceedings.
	const ruleCols = `id, proceeding_type_id, parent_id, submission_code, name, name_en,
		description, primary_party, event_type, duration_value,
		duration_unit, timing, rule_code, deadline_notes, deadline_notes_en, sequence_order,
		alt_duration_value, alt_duration_unit, alt_rule_code,
		anchor_alt, concept_id, legal_source, is_spawn, spawn_label, is_active,
		created_at, updated_at,
		trigger_event_id, spawn_proceeding_type_id, combine_op, condition_expr,
		priority, is_court_set, lifecycle_state, draft_of, published_at,
		choices_offered, applies_to_target`

	q, args, err := sqlx.In(`
		SELECT `+ruleCols+`
		  FROM paliad.deadline_rules
		 WHERE proceeding_type_id IN (?)
		   AND is_active = true
		   AND lifecycle_state = 'published'
		 ORDER BY proceeding_type_id, sequence_order`, procIDs)
	if err != nil {
		return fmt.Errorf("build rules IN: %w", err)
	}
	q = pool.Rebind(q)
	var rules []litigationplanner.Rule
	if err := pool.SelectContext(ctx, &rules, q, args...); err != nil {
		return fmt.Errorf("select rules: %w", err)
	}

	// 3. Trigger events referenced by any UPC rule's trigger_event_id.
	triggerIDSet := make(map[int64]struct{})
	for _, r := range rules {
		if r.TriggerEventID != nil {
			triggerIDSet[*r.TriggerEventID] = struct{}{}
		}
	}
	var triggers []litigationplanner.TriggerEvent
	if len(triggerIDSet) > 0 {
		triggerIDs := make([]int64, 0, len(triggerIDSet))
		for id := range triggerIDSet {
			triggerIDs = append(triggerIDs, id)
		}
		q, args, err := sqlx.In(`
			SELECT id, code, name, name_de, description, is_active, created_at
			  FROM paliad.trigger_events
			 WHERE id IN (?)
			 ORDER BY id`, triggerIDs)
		if err != nil {
			return fmt.Errorf("build triggers IN: %w", err)
		}
		q = pool.Rebind(q)
		if err := pool.SelectContext(ctx, &triggers, q, args...); err != nil {
			return fmt.Errorf("select trigger_events: %w", err)
		}
	}

	// 4. Holidays — DE national + UPC regime entries. The embedded
	// calendar serves UPC computations so both axes matter.
	var holidays []EmbeddedHoliday
	if err := pool.SelectContext(ctx, &holidays, `
		SELECT to_char(date, 'YYYY-MM-DD') AS date_iso,
		       name, country, regime, state, holiday_type
		  FROM paliad.holidays
		 WHERE country = 'DE' OR regime = 'UPC'
		 ORDER BY date, name`); err != nil {
		return fmt.Errorf("select holidays: %w", err)
	}

	// 5. Courts — UPC subset.
	var courts []EmbeddedCourt
	if err := pool.SelectContext(ctx, &courts, `
		SELECT id, code, name_de, name_en, country, regime, court_type, parent_id, sort_order
		  FROM paliad.courts
		 WHERE is_active = true
		   AND (regime = 'UPC' OR court_type LIKE 'upc%')
		 ORDER BY sort_order, id`); err != nil {
		return fmt.Errorf("select courts: %w", err)
	}

	// 6. Compose meta.
	meta := Meta{
		Version:           resolveVersion(version, output),
		GeneratedAt:       time.Now().UTC().Truncate(time.Second),
		PaliadCommit:      gitCommitShort(),
		SourceDBLabel:     sourceLabel,
		RuleCount:         len(rules),
		ProceedingCount:   len(procs),
		TriggerEventCount: len(triggers),
		HolidayCount:      len(holidays),
		CourtCount:        len(courts),
	}

	// 7. Write each file.
	files := []struct {
		name string
		data any
	}{
		{"proceeding_types.json", procs},
		{"rules.json", rules},
		{"trigger_events.json", triggers},
		{"holidays.json", holidays},
		{"courts.json", courts},
		{"meta.json", meta},
	}
	for _, f := range files {
		path := filepath.Join(output, f.name)
		buf, err := json.MarshalIndent(f.data, "", "  ")
		if err != nil {
			return fmt.Errorf("marshal %s: %w", f.name, err)
		}
		buf = append(buf, '\n')
		if err := os.WriteFile(path, buf, 0o644); err != nil {
			return fmt.Errorf("write %s: %w", path, err)
		}
	}

	log.Printf("snapshot written: version=%s rules=%d proceedings=%d triggers=%d holidays=%d courts=%d → %s",
		meta.Version, meta.RuleCount, meta.ProceedingCount,
		meta.TriggerEventCount, meta.HolidayCount, meta.CourtCount, output)
	return nil
}

// resolveVersion picks a date-stamped version slug, bumping the suffix
// past any pre-existing same-day version found in the existing
// meta.json. If the caller passed -version, that wins.
func resolveVersion(explicit, output string) string {
	if explicit != "" {
		return explicit
	}
	today := time.Now().UTC().Format("2006-01-02")
	// Read prior meta to detect same-day collisions.
	prior, err := os.ReadFile(filepath.Join(output, "meta.json"))
	if err != nil {
		return today + "-1"
	}
	var pm Meta
	if err := json.Unmarshal(prior, &pm); err != nil {
		return today + "-1"
	}
	if !strings.HasPrefix(pm.Version, today+"-") {
		return today + "-1"
	}
	// Same day: bump the suffix.
	suffix := pm.Version[len(today)+1:]
	var n int
	if _, err := fmt.Sscanf(suffix, "%d", &n); err != nil {
		return today + "-1"
	}
	return fmt.Sprintf("%s-%d", today, n+1)
}

// gitCommitShort returns the short SHA of the paliad checkout. Best-
// effort — empty string when we're not in a git checkout.
func gitCommitShort() string {
	out, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}
