package upc

import (
	"context"
	"fmt"

	"github.com/google/uuid"

	lp "mgit.msbls.de/m/paliad/pkg/litigationplanner"
)

// SnapshotCatalog is the embedded-JSON implementation of lp.Catalog.
// All lookups are O(1) on indexed in-memory maps; LookupEvents does a
// linear scan of the rule slice (< 100 rows in the UPC corpus, no
// index needed).
//
// ProjectHint is ignored — the snapshot has no project-scoped rules.
// applies_to_target (B1) and condition_expr (Phase 2) ride along on
// each Rule as ordinary fields; the engine consumes them identically
// whether the catalog is paliad-backed or snapshot-backed.
type SnapshotCatalog struct {
	procs         []lp.ProceedingType
	rules         []lp.Rule
	triggerByID   map[int64]lp.TriggerEvent
	rulesByProc   map[int][]lp.Rule
	ruleByID      map[uuid.UUID]lp.Rule
	procByID      map[int]lp.ProceedingType
	procByCode    map[string]lp.ProceedingType
	rulesByTriggr map[int64][]lp.Rule
}

// NewCatalog parses the embedded snapshot and returns a ready-to-use
// Catalog. Returns an error when the JSON is missing or malformed
// (e.g. snapshot never generated, or stale relative to the package
// types).
func NewCatalog() (*SnapshotCatalog, error) {
	var procs []lp.ProceedingType
	if err := readJSON("proceeding_types.json", &procs); err != nil {
		return nil, err
	}
	var rules []lp.Rule
	if err := readJSON("rules.json", &rules); err != nil {
		return nil, err
	}
	var triggers []lp.TriggerEvent
	if err := readJSON("trigger_events.json", &triggers); err != nil {
		return nil, err
	}

	c := &SnapshotCatalog{
		procs:         procs,
		rules:         rules,
		triggerByID:   make(map[int64]lp.TriggerEvent, len(triggers)),
		rulesByProc:   make(map[int][]lp.Rule),
		ruleByID:      make(map[uuid.UUID]lp.Rule, len(rules)),
		procByID:      make(map[int]lp.ProceedingType, len(procs)),
		procByCode:    make(map[string]lp.ProceedingType, len(procs)),
		rulesByTriggr: make(map[int64][]lp.Rule),
	}
	for _, p := range procs {
		c.procByID[p.ID] = p
		c.procByCode[p.Code] = p
	}
	for _, r := range rules {
		c.ruleByID[r.ID] = r
		if r.ProceedingTypeID != nil {
			c.rulesByProc[*r.ProceedingTypeID] = append(c.rulesByProc[*r.ProceedingTypeID], r)
		}
		if r.TriggerEventID != nil {
			c.rulesByTriggr[*r.TriggerEventID] = append(c.rulesByTriggr[*r.TriggerEventID], r)
		}
	}
	for _, t := range triggers {
		c.triggerByID[t.ID] = t
	}
	return c, nil
}

// LoadProceeding returns the proceeding-type metadata + rules. The
// ProjectHint is ignored on the snapshot side (no projects).
func (c *SnapshotCatalog) LoadProceeding(_ context.Context, code string, _ lp.ProjectHint) (*lp.ProceedingType, []lp.Rule, error) {
	p, ok := c.procByCode[code]
	if !ok {
		return nil, nil, lp.ErrUnknownProceedingType
	}
	// Return a defensive copy of the rule slice so callers can sort /
	// mutate without leaking back into the cache.
	src := c.rulesByProc[p.ID]
	dst := make([]lp.Rule, len(src))
	copy(dst, src)
	return &p, dst, nil
}

// LoadProceedingByID is the resolver used by CalculateRule.
func (c *SnapshotCatalog) LoadProceedingByID(_ context.Context, id int) (*lp.ProceedingType, error) {
	p, ok := c.procByID[id]
	if !ok {
		return nil, lp.ErrUnknownProceedingType
	}
	return &p, nil
}

// LoadRuleByID resolves a rule UUID to the rule row.
func (c *SnapshotCatalog) LoadRuleByID(_ context.Context, ruleID string) (*lp.Rule, error) {
	id, err := uuid.Parse(ruleID)
	if err != nil {
		return nil, lp.ErrUnknownRule
	}
	r, ok := c.ruleByID[id]
	if !ok {
		return nil, lp.ErrUnknownRule
	}
	return &r, nil
}

// LoadRuleByCode resolves a rule by (proceedingCode, submissionCode).
func (c *SnapshotCatalog) LoadRuleByCode(_ context.Context, proceedingCode, submissionCode string) (*lp.Rule, *lp.ProceedingType, error) {
	p, ok := c.procByCode[proceedingCode]
	if !ok {
		return nil, nil, lp.ErrUnknownProceedingType
	}
	for _, r := range c.rulesByProc[p.ID] {
		if r.SubmissionCode != nil && *r.SubmissionCode == submissionCode {
			rr := r
			pp := p
			return &rr, &pp, nil
		}
	}
	return nil, nil, lp.ErrUnknownRule
}

// LoadRulesByTriggerEvent lists Pipeline-C trigger-event-rooted rules.
func (c *SnapshotCatalog) LoadRulesByTriggerEvent(_ context.Context, triggerEventID int64) ([]lp.Rule, error) {
	src := c.rulesByTriggr[triggerEventID]
	dst := make([]lp.Rule, len(src))
	copy(dst, src)
	return dst, nil
}

// LoadTriggerEventsByIDs returns trigger-event rows for the given IDs.
func (c *SnapshotCatalog) LoadTriggerEventsByIDs(_ context.Context, ids []int64) (map[int64]lp.TriggerEvent, error) {
	out := make(map[int64]lp.TriggerEvent, len(ids))
	for _, id := range ids {
		if t, ok := c.triggerByID[id]; ok {
			out[id] = t
		}
	}
	return out, nil
}

// LookupEvents runs the multi-axis filter + depth walk against the
// in-memory rule slice. Mirrors the paliad-side semantics: unknown
// axis values fall through as "no filter on this axis"; anchors are
// depth=1, walked-in children are depth=2+; results ordered by
// (proceeding_type_id, sequence_order).
func (c *SnapshotCatalog) LookupEvents(_ context.Context, axes lp.EventLookupAxes, depth lp.EventLookupDepth) ([]lp.EventMatch, error) {
	// Validate axes; unknown values reset to empty (no filter).
	jurisdiction := axes.Jurisdiction
	if jurisdiction != "" && jurisdiction != "UPC" && jurisdiction != "DE" &&
		jurisdiction != "EPA" && jurisdiction != "DPMA" {
		jurisdiction = ""
	}
	party := axes.Party
	if party != "" && !lp.IsValidPrimaryParty(party) {
		party = ""
	}
	appealTarget := axes.AppealTarget
	if appealTarget != "" && !lp.IsValidAppealTarget(appealTarget) {
		appealTarget = ""
	}

	// First pass: find anchor matches (rules that satisfy every
	// non-zero axis directly).
	anchors := make(map[uuid.UUID]bool, len(c.rules))
	for _, r := range c.rules {
		if r.ProceedingTypeID == nil {
			continue
		}
		p := c.procByID[*r.ProceedingTypeID]
		if jurisdiction != "" && (p.Jurisdiction == nil || *p.Jurisdiction != jurisdiction) {
			continue
		}
		if axes.ProceedingTypeID != nil && *r.ProceedingTypeID != *axes.ProceedingTypeID {
			continue
		}
		if party != "" && (r.PrimaryParty == nil || *r.PrimaryParty != party) {
			continue
		}
		// EventCategoryID axis: the embedded snapshot doesn't carry
		// the deadline_concept_event_types junction (only paliad has
		// it). When EventCategoryID is set, we conservatively return
		// no matches — youpc.org doesn't use this axis today. Future
		// snapshot generations can add a concept→category index if
		// needed.
		if axes.EventCategoryID != nil {
			continue
		}
		if appealTarget != "" {
			found := false
			for _, t := range r.AppliesToTarget {
				if t == appealTarget {
					found = true
					break
				}
			}
			if !found {
				continue
			}
		}
		anchors[r.ID] = true
	}

	// Second pass: depth walk. Expand anchors → their immediate
	// children (parent_id ∈ matched). Iterate to fixpoint for
	// EventLookupDepthAllFollowing; stop after one pass for
	// EventLookupDepthNext.
	matched := make(map[uuid.UUID]bool, len(anchors))
	for id := range anchors {
		matched[id] = true
	}
	if depth == lp.EventLookupDepthNext || depth == lp.EventLookupDepthAllFollowing {
		for {
			grew := false
			for _, r := range c.rules {
				if matched[r.ID] {
					continue
				}
				if r.ParentID == nil {
					continue
				}
				if matched[*r.ParentID] {
					matched[r.ID] = true
					grew = true
				}
			}
			if !grew || depth == lp.EventLookupDepthNext {
				break
			}
		}
	}

	// Compute depth from anchor: walk parent_id chain until we hit
	// an anchor.
	depths := make(map[uuid.UUID]int, len(matched))
	for id := range matched {
		if anchors[id] {
			depths[id] = 1
			continue
		}
		// Walk up.
		d := 1
		cur := id
		maxIter := len(matched) + 1
		for i := 0; i < maxIter; i++ {
			r, ok := c.ruleByID[cur]
			if !ok || r.ParentID == nil {
				break
			}
			d++
			cur = *r.ParentID
			if anchors[cur] {
				break
			}
		}
		depths[id] = d
	}

	// Compose output, ordered by (proceeding_type_id, sequence_order)
	// via the catalog's rule slice ordering.
	out := make([]lp.EventMatch, 0, len(matched))
	for _, r := range c.rules {
		if !matched[r.ID] {
			continue
		}
		var parentRuleID *uuid.UUID
		if r.ParentID != nil && matched[*r.ParentID] {
			p := *r.ParentID
			parentRuleID = &p
		}
		proc := lp.ProceedingType{}
		if r.ProceedingTypeID != nil {
			proc = c.procByID[*r.ProceedingTypeID]
		}
		out = append(out, lp.EventMatch{
			Rule:            r,
			ProceedingType:  proc,
			Priority:        r.Priority,
			DepthFromAnchor: depths[r.ID],
			ParentRuleID:    parentRuleID,
		})
	}
	return out, nil
}

// LoadScenarios returns an empty slice. The snapshot catalog has no
// scenarios — youpc.org (the consumer today) doesn't carry a project /
// user model. Future snapshot variants could ship demo scenarios, but
// v1 returns nothing.
func (c *SnapshotCatalog) LoadScenarios(_ context.Context, _ lp.ScenarioFilter) ([]lp.Scenario, error) {
	return []lp.Scenario{}, nil
}

// MatchScenario always returns ErrUnknownScenario — the snapshot has
// no scenarios to match against.
func (c *SnapshotCatalog) MatchScenario(_ context.Context, _ uuid.UUID) (*lp.Scenario, error) {
	return nil, lp.ErrUnknownScenario
}

// Compile-time assertion that SnapshotCatalog satisfies lp.Catalog.
var _ lp.Catalog = (*SnapshotCatalog)(nil)

// ErrSnapshotEmpty is returned by NewCatalog when the embedded files
// parse but the corpus is empty (zero proceedings) — almost always a
// sign that the snapshot has never been generated.
var ErrSnapshotEmpty = fmt.Errorf("upc snapshot is empty — run cmd/gen-upc-snapshot")
