package services

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
	"github.com/lib/pq"
)

// EventDeadlineService backs the "Was kommt nach…" Fristenrechner mode:
// given a trigger event + date, return all deadlines that flow from it
// with their computed due dates. Mirrors youpc.org's deadline-calc
// shape (event-driven).
//
// Phase 3 Slice 3 (t-paliad-184) refactor: the math + rule SELECT moved
// into FristenrechnerService.calculateByTriggerEvent (which reads from
// the unified paliad.deadline_rules backed by mig 085's data-move).
// EventDeadlineService.Calculate delegated and wrapped the unified
// response in the legacy CalculateResponse shape, but still SELECTed
// paliad.event_deadlines + paliad.event_deadline_rule_codes for the
// per-row metadata (DurationValue, DurationUnit, Timing, Notes, RuleCodes,
// alt_*, combine_op).
//
// Phase 3 Slice 9 follow-up A (t-paliad-199): EventDeadlineService now
// reads source rows from paliad.deadline_rules directly — the
// trigger_event_id IS NOT NULL filter scopes to the 77 Pipeline-C rows
// mig 085 unified. Multi-code citations (the legacy
// event_deadline_rule_codes junction) live in the new
// paliad.deadline_rules.rule_codes text[] column populated by mig 092's
// backfill. event_deadlines + event_deadline_rule_codes are dropped by
// mig 092; the service no longer references either.
//
// Phase 3 Slice 4 (t-paliad-185) collapsed the prior on-service
// applyDuration / addWorkingDays helpers into package-level functions
// shared with FristenrechnerService — single source-of-truth for
// timing / working_days / holiday-rollover arithmetic.
type EventDeadlineService struct {
	db             *sqlx.DB
	calc           *DeadlineCalculator
	holidays       *HolidayService
	courts         *CourtService
	fristenrechner *FristenrechnerService
}

// NewEventDeadlineService wires the service to its dependencies.
func NewEventDeadlineService(db *sqlx.DB, calc *DeadlineCalculator, holidays *HolidayService, courts *CourtService, fristenrechner *FristenrechnerService) *EventDeadlineService {
	return &EventDeadlineService{
		db:             db,
		calc:           calc,
		holidays:       holidays,
		courts:         courts,
		fristenrechner: fristenrechner,
	}
}

// TriggerEventSummary is the shape returned to the picker UI: lightweight
// (no description, no audit timestamps) and sorted alphabetically by name.
type TriggerEventSummary struct {
	ID     int64  `db:"id"      json:"id"`
	Code   string `db:"code"    json:"code"`
	Name   string `db:"name"    json:"name"`
	NameDE string `db:"name_de" json:"name_de"`
}

// ErrUnknownTriggerEvent is returned when a request references a trigger
// event that doesn't exist or is inactive.
var ErrUnknownTriggerEvent = errors.New("unknown trigger event")

// ListTriggerEvents returns active triggers for the picker, sorted by name.
func (s *EventDeadlineService) ListTriggerEvents(ctx context.Context) ([]TriggerEventSummary, error) {
	var rows []TriggerEventSummary
	err := s.db.SelectContext(ctx, &rows, `
		SELECT id, code, name, name_de
		  FROM paliad.trigger_events
		 WHERE is_active = true
		 ORDER BY lower(name)`)
	if err != nil {
		return nil, fmt.Errorf("list trigger events: %w", err)
	}
	return rows, nil
}

// EventDeadlineResult is one computed deadline returned to the UI.
// Bilingual title + bilingual notes + the rule codes attached to the
// deadline + the computed due date (after weekend/holiday rollover).
type EventDeadlineResult struct {
	ID              int64    `json:"id"`
	Title           string   `json:"title"`
	TitleDE         string   `json:"titleDE"`
	DurationValue   int      `json:"durationValue"`
	DurationUnit    string   `json:"durationUnit"`
	Timing          string   `json:"timing"`
	Notes           string   `json:"notes,omitempty"`
	NotesEN         string   `json:"notesEN,omitempty"`
	RuleCodes       []string `json:"ruleCodes"`
	DueDate         string   `json:"dueDate"`         // YYYY-MM-DD, after holiday/weekend adjust
	OriginalDueDate string   `json:"originalDueDate"` // YYYY-MM-DD, before adjust
	WasAdjusted     bool     `json:"wasAdjusted"`
	IsComposite     bool     `json:"isComposite,omitempty"` // true when alt_* + combine_op resolved this row
	CompositeNote   string   `json:"compositeNote,omitempty"`
}

// CalculateResponse is what the API hands back to the client.
type CalculateResponse struct {
	TriggerEvent TriggerEventSummary   `json:"triggerEvent"`
	TriggerDate  string                `json:"triggerDate"`
	Deadlines    []EventDeadlineResult `json:"deadlines"`
}

// Calculate resolves all deadlines flowing from a trigger event + date.
//
// Phase 3 Slice 3 (t-paliad-184) delegated the rule SELECT + math to
// FristenrechnerService.calculateByTriggerEvent — which reads from
// paliad.deadline_rules WHERE trigger_event_id = X (the rows mig 085
// moved out of event_deadlines).
//
// Phase 3 Slice 9 follow-up A (t-paliad-199): the per-row metadata
// SELECT now also reads from paliad.deadline_rules. Mig 092 dropped
// paliad.event_deadlines + paliad.event_deadline_rule_codes after
// backfilling the multi-code junction rows into
// paliad.deadline_rules.rule_codes (text[]). The legacy
// EventDeadlineResult shape is built by mapping fields:
//
//	deadline_rules.name           → EventDeadlineResult.TitleDE
//	deadline_rules.name_en        → EventDeadlineResult.Title
//	deadline_rules.deadline_notes → EventDeadlineResult.Notes
//	deadline_rules.deadline_notes_en → EventDeadlineResult.NotesEN
//	deadline_rules.rule_codes     → EventDeadlineResult.RuleCodes
//	deadline_rules.sequence_order → EventDeadlineResult.ID
//	  (legacy event_deadlines.id semantic via mig 085's
//	   sequence_order = 1000 + event_deadlines.id convention)
//
// The public /api/tools/event-deadlines wire shape is unchanged from
// pre-Slice-9-followup-A — only the backing query changes.
//
// courtID may be empty for legacy callers — defaults to UPC München
// (DE country, UPC regime) for the trigger-event surface.
func (s *EventDeadlineService) Calculate(ctx context.Context, triggerEventID int64, triggerDateStr, courtID string) (*CalculateResponse, error) {
	var trig TriggerEventSummary
	err := s.db.GetContext(ctx, &trig, `
		SELECT id, code, name, name_de
		  FROM paliad.trigger_events
		 WHERE id = $1 AND is_active = true`, triggerEventID)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrUnknownTriggerEvent
	}
	if err != nil {
		return nil, fmt.Errorf("load trigger event: %w", err)
	}

	// Source-of-truth columns the unified UIResponse drops (the
	// frontend still reads DurationValue/Unit/Timing literally to render
	// the "X days after" pill). Reading from paliad.deadline_rules with
	// trigger_event_id = $1 — the same row set FristenrechnerService.
	// calculateByTriggerEvent uses, so a join by rule.ID is exact.
	// COALESCE(timing, 'after') matches the column default. Pipeline-C
	// rows seeded by mig 085 always carry an explicit timing (the
	// source event_deadlines.timing was NOT NULL); the COALESCE guards
	// any future hand-edited rule that left the column NULL.
	var rows []eventDeadlineRuleRow
	err = s.db.SelectContext(ctx, &rows, `
		SELECT id, sequence_order, name, name_en, duration_value, duration_unit,
		       COALESCE(timing, 'after') AS timing,
		       deadline_notes, deadline_notes_en, alt_duration_value, alt_duration_unit,
		       combine_op, rule_codes
		  FROM paliad.deadline_rules
		 WHERE trigger_event_id = $1 AND is_active = true
		 ORDER BY sequence_order`, triggerEventID)
	if err != nil {
		return nil, fmt.Errorf("load deadlines: %w", err)
	}

	byRuleID := make(map[uuid.UUID]eventDeadlineRuleRow, len(rows))
	for _, r := range rows {
		byRuleID[r.ID] = r
	}

	// Delegate to the unified calculator. UIResponse comes back with the
	// adjusted/original dates + wasAdjusted; UIDeadline.RuleID is
	// rule.ID.String(), so we can merge precisely on the rule UUID
	// without relying on title_de string equality (the pre-Slice-9
	// shape) — a fragile match if a rule's name ever diverges from its
	// source.
	unified, err := s.fristenrechner.Calculate(ctx, "", triggerDateStr, CalcOptions{
		TriggerEventIDFilter: &triggerEventID,
		CourtID:              courtID,
	})
	if err != nil {
		return nil, err
	}

	// Holiday/regime resolution is cheap but happens up to N times in
	// the composite-recompute loop below; pull it out so we hit the
	// CourtService once per call.
	country, regime, cerr := s.courts.CountryRegime(courtID, CountryDE, RegimeUPC)
	if cerr != nil {
		return nil, cerr
	}
	triggerDate, terr := time.Parse("2006-01-02", triggerDateStr)
	if terr != nil {
		return nil, fmt.Errorf("invalid trigger date %q: %w", triggerDateStr, terr)
	}

	results := make([]EventDeadlineResult, 0, len(unified.Deadlines))
	for _, d := range unified.Deadlines {
		ruleID, perr := uuid.Parse(d.RuleID)
		if perr != nil {
			// UIDeadline.RuleID is always rule.ID.String() — a non-UUID
			// here would mean a calculator bug. Skip defensively rather
			// than fail the request.
			continue
		}
		src, ok := byRuleID[ruleID]
		if !ok {
			// Defensive: a unified row exists for which no source
			// deadline_rules row matches by ID. Should be impossible
			// since both branches read the same rows; skip rather than
			// emit a broken row.
			continue
		}
		isComposite := src.CombineOp != nil && src.AltDurationValue != nil && src.AltDurationUnit != nil
		compositeNote := ""
		if isComposite {
			// Recompute which leg won by re-running applyDuration with
			// the source's exact inputs — cheaper than threading the
			// pick through the unified UIDeadline shape.
			_, baseAdj, _, _ := applyDuration(triggerDate, src.DurationValue, src.DurationUnit, src.Timing, country, regime, s.holidays)
			_, altAdj, _, _ := applyDuration(triggerDate, *src.AltDurationValue, *src.AltDurationUnit, src.Timing, country, regime, s.holidays)
			pickedUnit := src.DurationUnit
			switch *src.CombineOp {
			case "max":
				if altAdj.After(baseAdj) {
					pickedUnit = *src.AltDurationUnit
				}
			case "min":
				if altAdj.Before(baseAdj) {
					pickedUnit = *src.AltDurationUnit
				}
			}
			compositeNote = fmt.Sprintf("%s(%d %s, %d %s) → %s leg",
				*src.CombineOp,
				src.DurationValue, src.DurationUnit,
				*src.AltDurationValue, *src.AltDurationUnit,
				pickedUnit)
		}
		notes := ""
		if src.DeadlineNotes != nil {
			notes = *src.DeadlineNotes
		}
		notesEN := ""
		if src.DeadlineNotesEn != nil {
			notesEN = *src.DeadlineNotesEn
		}
		// rule_codes is NULL when the Pipeline-C rule had no junction
		// rows pre-mig-092 (7 of 77 deadlines). Emit an empty slice in
		// that case so the JSON contract stays `"ruleCodes": []` rather
		// than `null`.
		ruleCodes := []string(src.RuleCodes)
		if ruleCodes == nil {
			ruleCodes = []string{}
		}
		results = append(results, EventDeadlineResult{
			// Legacy event_deadlines.id semantic: mig 085 set
			// sequence_order = 1000 + event_deadlines.id, so the
			// pre-Slice-9-followup-A integer IDs (1..206) round-trip
			// via sequence_order - 1000. Preserves the wire contract
			// for the existing 77 Pipeline-C rows; Pipeline-C rules
			// added by the rule editor get whatever sequence_order
			// the editor assigns (no event_deadlines counterpart).
			ID:              int64(src.SequenceOrder - 1000),
			Title:           src.NameEN,
			TitleDE:         src.Name,
			DurationValue:   src.DurationValue,
			DurationUnit:    src.DurationUnit,
			Timing:          src.Timing,
			Notes:           notes,
			NotesEN:         notesEN,
			RuleCodes:       ruleCodes,
			DueDate:         d.DueDate,
			OriginalDueDate: d.OriginalDate,
			WasAdjusted:     d.WasAdjusted,
			IsComposite:     isComposite,
			CompositeNote:   compositeNote,
		})
	}

	return &CalculateResponse{
		TriggerEvent: trig,
		TriggerDate:  triggerDateStr,
		Deadlines:    results,
	}, nil
}

// eventDeadlineRuleRow is the package-private row shape used by
// Calculate's SELECT against paliad.deadline_rules. Keeps optional
// fields as pointers (nil = no composite alt-leg / no notes). rule_codes
// is pq.StringArray so the text[] column scans cleanly; Pipeline-C
// rules without junction rows have a NULL column and end up with a nil
// slice (treated as "no codes").
type eventDeadlineRuleRow struct {
	ID               uuid.UUID      `db:"id"`
	SequenceOrder    int            `db:"sequence_order"`
	Name             string         `db:"name"`
	NameEN           string         `db:"name_en"`
	DurationValue    int            `db:"duration_value"`
	DurationUnit     string         `db:"duration_unit"`
	Timing           string         `db:"timing"`
	DeadlineNotes    *string        `db:"deadline_notes"`
	DeadlineNotesEn  *string        `db:"deadline_notes_en"`
	AltDurationValue *int           `db:"alt_duration_value"`
	AltDurationUnit  *string        `db:"alt_duration_unit"`
	CombineOp        *string        `db:"combine_op"`
	RuleCodes        pq.StringArray `db:"rule_codes"`
}
