package litigationplanner

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/google/uuid"
)

// Slice D scenarios — m/paliad#124 §5 (revised), mig 145.
//
// A Scenario is a named composition of existing proceedings + flags +
// per-card choices + anchor dates. v1 ships with one primary proceeding
// per scenario; the spec.proceedings[] array is architected to absorb
// multi-peer compose (v2) without a schema migration.
//
// "users should not add their own rules" (m, t-paliad-301) — the spec
// references existing rules by submission_code; it never creates new
// ones. ValidateSpec checks every code/submission resolves against the
// current catalog before a save is accepted.

// Scenario is one row of paliad.scenarios. Wire shape doubles as the
// API request/response payload for /api/scenarios.
type Scenario struct {
	ID          uuid.UUID  `db:"id"          json:"id"`
	ProjectID   *uuid.UUID `db:"project_id"  json:"project_id,omitempty"`
	Name        string     `db:"name"        json:"name"`
	Description *string    `db:"description" json:"description,omitempty"`
	// Spec carries the jsonb composition. Stored raw so we can ship
	// shape evolutions without schema churn; ParseSpec gives the
	// structured view.
	Spec      NullableJSON `db:"spec"       json:"spec"`
	CreatedBy *uuid.UUID   `db:"created_by" json:"created_by,omitempty"`
	CreatedAt time.Time    `db:"created_at" json:"created_at"`
	UpdatedAt time.Time    `db:"updated_at" json:"updated_at"`
}

// ScenarioSpec is the parsed view of Scenario.Spec. v1 = version 1.
// Future shape changes bump the version; ParseSpec rejects unknown
// versions so an old client doesn't silently misread a future-shape
// scenario.
type ScenarioSpec struct {
	Version         int                 `json:"version"`
	BaseTriggerDate string              `json:"base_trigger_date"`
	Proceedings     []ScenarioProceeding `json:"proceedings"`
}

// ScenarioProceeding is one entry under spec.proceedings[]. v1 honours
// exactly one with role="primary" (additional entries with role="peer"
// are reserved for v2 multi-proceeding compose and silently ignored
// by the engine today).
type ScenarioProceeding struct {
	Code                string                       `json:"code"`
	Role                string                       `json:"role"` // "primary" | "peer" (v2)
	TriggerDateOverride string                       `json:"trigger_date_override,omitempty"`
	Flags               []string                     `json:"flags,omitempty"`
	PerCardChoices      map[string]ScenarioCardChoice `json:"per_card_choices,omitempty"`
	AnchorOverrides     map[string]string            `json:"anchor_overrides,omitempty"`
	SkipRules           []string                     `json:"skip_rules,omitempty"`
	AppealTarget        string                       `json:"appeal_target,omitempty"`
}

// ScenarioCardChoice is one entry under
// spec.proceedings[*].per_card_choices. Mirrors the t-paliad-265 choice
// kinds; not every kind is populated on every card.
type ScenarioCardChoice struct {
	Appellant  string `json:"appellant,omitempty"`
	IncludeCCR *bool  `json:"include_ccr,omitempty"`
	Skip       *bool  `json:"skip,omitempty"`
}

// Spec version constant.
const ScenarioSpecVersion = 1

// Sentinel errors for scenarios.
var (
	ErrUnknownScenario   = errors.New("unknown scenario")
	ErrInvalidScenario   = errors.New("invalid scenario spec")
	ErrScenarioNoPrimary = errors.New("scenario spec has no proceeding with role='primary'")
)

// ScenarioRole* are the canonical role slugs for ScenarioProceeding.Role.
const (
	ScenarioRolePrimary = "primary"
	ScenarioRolePeer    = "peer"
)

// ParseSpec decodes Scenario.Spec into a structured ScenarioSpec. Used
// by the engine adapter + the rule-editor preview. Surfaces a friendly
// error wrapping ErrInvalidScenario on malformed JSON / unknown version
// so the handler can map to a 400.
func ParseSpec(raw NullableJSON) (*ScenarioSpec, error) {
	if len(raw) == 0 {
		return nil, fmt.Errorf("%w: spec is empty", ErrInvalidScenario)
	}
	var s ScenarioSpec
	if err := json.Unmarshal([]byte(raw), &s); err != nil {
		return nil, fmt.Errorf("%w: decode spec: %v", ErrInvalidScenario, err)
	}
	if s.Version != ScenarioSpecVersion {
		return nil, fmt.Errorf("%w: spec.version=%d, want %d",
			ErrInvalidScenario, s.Version, ScenarioSpecVersion)
	}
	return &s, nil
}

// PrimaryProceeding returns the entry from spec.proceedings[] with
// role="primary". Returns ErrScenarioNoPrimary if absent — every spec
// must carry exactly one primary entry. (Multiple primaries are also
// rejected: the engine consumes one.)
func (s *ScenarioSpec) PrimaryProceeding() (*ScenarioProceeding, error) {
	var primary *ScenarioProceeding
	for i := range s.Proceedings {
		if s.Proceedings[i].Role == ScenarioRolePrimary {
			if primary != nil {
				return nil, fmt.Errorf("%w: multiple proceedings with role='primary'", ErrInvalidScenario)
			}
			primary = &s.Proceedings[i]
		}
	}
	if primary == nil {
		return nil, ErrScenarioNoPrimary
	}
	return primary, nil
}

// CalcOptionsFromSpec builds a CalcOptions from the scenario's primary
// entry. The caller still needs the proceeding code + the trigger date,
// both returned alongside.
//
// v1: only the primary entry is honoured. v2 will iterate over peer
// entries; the multi-peer merge lives in the paliad-side
// ProjectionService (one Calculate call per entry, merged + sorted by
// date).
func (s *ScenarioSpec) CalcOptionsFromSpec() (proceedingCode, triggerDate string, opts CalcOptions, err error) {
	primary, err := s.PrimaryProceeding()
	if err != nil {
		return "", "", CalcOptions{}, err
	}
	td := s.BaseTriggerDate
	if primary.TriggerDateOverride != "" {
		td = primary.TriggerDateOverride
	}
	if td == "" {
		return "", "", CalcOptions{}, fmt.Errorf("%w: no base_trigger_date and no per-proceeding override", ErrInvalidScenario)
	}

	perCardAppellant := make(map[string]string, len(primary.PerCardChoices))
	skipRules := make(map[string]struct{}, len(primary.SkipRules))
	includeCCRFor := make(map[string]struct{}, len(primary.PerCardChoices))
	for code, choice := range primary.PerCardChoices {
		if choice.Appellant != "" {
			perCardAppellant[code] = choice.Appellant
		}
		if choice.IncludeCCR != nil && *choice.IncludeCCR {
			includeCCRFor[code] = struct{}{}
		}
		if choice.Skip != nil && *choice.Skip {
			skipRules[code] = struct{}{}
		}
	}
	for _, code := range primary.SkipRules {
		skipRules[code] = struct{}{}
	}

	return primary.Code, td, CalcOptions{
		Flags:            primary.Flags,
		AnchorOverrides:  primary.AnchorOverrides,
		AppealTarget:     primary.AppealTarget,
		PerCardAppellant: perCardAppellant,
		SkipRules:        skipRules,
		IncludeCCRFor:    includeCCRFor,
	}, nil
}

// ScenarioFilter narrows Catalog.LoadScenarios. All fields optional:
//
//   - ProjectID non-nil: only scenarios attached to that project
//     (project_id = filter.ProjectID).
//   - AbstractForUser non-nil: only abstract scenarios (project_id IS
//     NULL) created by that user.
//   - Both nil: list every scenario the caller can see (RLS-gated).
type ScenarioFilter struct {
	ProjectID       *uuid.UUID
	AbstractForUser *uuid.UUID
}

// CalculateFromScenario is the high-level engine entry for scenario-
// driven rendering. Unpacks the spec, builds CalcOptions, and delegates
// to Calculate.
//
// v1: surfaces only the primary proceeding's timeline. v2 multi-peer
// expansion lives on the paliad-side ProjectionService (per-entry
// Calculate + client-side merge); the package doesn't own that
// orchestration.
func CalculateFromScenario(
	ctx context.Context,
	scenario *Scenario,
	catalog Catalog,
	holidays HolidayCalendar,
	courts CourtRegistry,
) (*Timeline, error) {
	spec, err := ParseSpec(scenario.Spec)
	if err != nil {
		return nil, err
	}
	code, triggerDate, opts, err := spec.CalcOptionsFromSpec()
	if err != nil {
		return nil, err
	}
	return Calculate(ctx, code, triggerDate, opts, catalog, holidays, courts)
}
