package services

import (
	"context"
	"database/sql"
	"fmt"
	"strings"

	"github.com/google/uuid"
)

// EventSearchHit is one ranked hit in the events-shape search response.
// Returned by FristenrechnerService.SearchEvents.
//
// One hit per (procedural_event, proceeding_type) tuple: a single event
// can appear in multiple proceedings (the data carries handful of
// procedural_event rows whose code is null.* and that are anchored by
// rules in different proceedings — those legacy stragglers surface as
// multiple hits, one per proceeding context).
type EventSearchHit struct {
	EventID        uuid.UUID            `json:"id"`
	Code           string               `json:"code"`
	NameDE         string               `json:"name_de"`
	NameEN         string               `json:"name_en"`
	EventKind      *string              `json:"event_kind,omitempty"`
	Description    *string              `json:"description,omitempty"`
	PrimaryParty   *string              `json:"primary_party,omitempty"`
	ProceedingType EventSearchPT        `json:"proceeding_type"`
	AnchorRuleID   uuid.UUID            `json:"anchor_rule_id"`
	FollowUpCount  int                  `json:"follow_up_count"`
	ConceptID      *uuid.UUID           `json:"concept_id,omitempty"`
	Score          float64              `json:"score"`
}

// EventSearchPT is the proceeding-type slice embedded in an EventSearchHit.
type EventSearchPT struct {
	ID           int     `json:"id"`
	Code         string  `json:"code"`
	NameDE       string  `json:"name_de"`
	NameEN       string  `json:"name_en"`
	Jurisdiction *string `json:"jurisdiction,omitempty"`
}

// EventSearchOptions is the filter set for SearchEvents. Empty values
// mean "no narrowing on this axis".
type EventSearchOptions struct {
	// Jurisdiction filters by proceeding_types.jurisdiction
	// ("UPC" | "DE" | "EPA" | "DPMA"). Empty = any.
	Jurisdiction string
	// ProceedingTypeCode narrows to one proceeding. Empty = any.
	ProceedingTypeCode string
	// EventKind filters by procedural_events.event_kind
	// ("filing" | "hearing" | "decision" | "order"). Empty = any.
	EventKind string
	// PrimaryParty narrows by the anchor rule's primary_party
	// ("claimant" | "defendant" | "court" | "both"). Empty = any.
	PrimaryParty string
	// Limit caps the result set; defaults to 50, max 200.
	Limit int
}

// EventSearchResponse is the wire shape for ?kind=events on the
// /api/tools/fristenrechner/search endpoint (design §6.1).
type EventSearchResponse struct {
	Query   string              `json:"query"`
	Filters EventSearchFilters  `json:"filters"`
	Events  []EventSearchHit    `json:"events"`
	Total   int                 `json:"total"`
}

// EventSearchFilters is the filter echo returned to the client.
type EventSearchFilters struct {
	Jurisdiction       *string `json:"jurisdiction"`
	ProceedingTypeCode *string `json:"proceeding_type_code"`
	EventKind          *string `json:"event_kind"`
	PrimaryParty       *string `json:"primary_party"`
}

// SearchEvents implements the ?kind=events response shape (Fristenrechner
// overhaul S1, design §6.1). Returns one hit per (procedural_event ×
// proceeding_type) tuple, ranked by trigram similarity against name /
// name_en / code. Empty q returns the unranked catalog filtered by the
// supplied facets.
func (s *DeadlineSearchService) SearchEvents(ctx context.Context, q string, opts EventSearchOptions) (*EventSearchResponse, error) {
	limit := opts.Limit
	if limit <= 0 {
		limit = 50
	}
	if limit > 200 {
		limit = 200
	}

	qNorm := normalizeQuery(q)

	resp := &EventSearchResponse{
		Query:   q,
		Filters: buildEventFilters(opts),
		Events:  []EventSearchHit{},
	}

	where := []string{
		"sr.is_active = true",
		"sr.lifecycle_state = 'published'",
		"pe.is_active = true",
		"pe.lifecycle_state = 'published'",
		"pt.is_active = true",
		// S1a (m/paliad#149 Phase 2 design §2.2): spawn-only rules are
		// consequences, not triggers — a user who picks "Berufung
		// einlegen" wants the appeal-tree root, not the inf.cfi spawn
		// link that *opens* that tree. Filter them out at the picker.
		// Terminal leaves (Duplik etc.) stay pickable: their own anchor
		// rule is non-spawn, so they surface and their result-view
		// renders an empty follow-up list — honest UX per t-paliad-327
		// §3a.4 / the design's "stay pickable" carve-out.
		"sr.is_spawn = false",
	}
	args := []any{}
	add := func(clause string, val any) {
		args = append(args, val)
		where = append(where, fmt.Sprintf(clause, len(args)))
	}
	if opts.Jurisdiction != "" {
		add("pt.jurisdiction = $%d", opts.Jurisdiction)
	}
	if opts.ProceedingTypeCode != "" {
		add("pt.code = $%d", opts.ProceedingTypeCode)
	}
	if opts.EventKind != "" {
		add("pe.event_kind = $%d", opts.EventKind)
	}
	if opts.PrimaryParty != "" {
		add("sr.primary_party = $%d", opts.PrimaryParty)
	}

	// Trigram score over (name || name_en || code). Empty query collapses
	// the score to 0 — keeps the SQL identical regardless of input mode.
	scoreExpr := "0::float8"
	if qNorm != "" {
		args = append(args, qNorm)
		scoreExpr = fmt.Sprintf(
			`GREATEST(similarity(pe.name, $%[1]d), similarity(pe.name_en, $%[1]d), similarity(pe.code, $%[1]d))`,
			len(args))
		// Drop hits with zero similarity so a typo doesn't return the
		// whole catalog ranked at 0.
		where = append(where, fmt.Sprintf(
			`(pe.name %% $%[1]d OR pe.name_en %% $%[1]d OR pe.code %% $%[1]d)`,
			len(args)))
	}

	// follow_up_count: rules whose parent_id points at this anchor rule.
	// Computed via correlated subquery; cheap at the 231-row scale.
	query := `
		SELECT pe.id AS event_id,
		       pe.code,
		       pe.name AS name_de,
		       pe.name_en,
		       pe.event_kind,
		       pe.description,
		       sr.primary_party,
		       pe.concept_id,
		       sr.id AS anchor_rule_id,
		       pt.id AS pt_id,
		       pt.code AS pt_code,
		       pt.name AS pt_name_de,
		       pt.name_en AS pt_name_en,
		       pt.jurisdiction AS pt_jurisdiction,
		       (SELECT COUNT(*)::int
		          FROM paliad.sequencing_rules child
		         WHERE child.parent_id = sr.id
		           AND child.is_active = true
		           AND child.lifecycle_state = 'published') AS follow_up_count,
		       ` + scoreExpr + ` AS score
		  FROM paliad.sequencing_rules sr
		  JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id
		  JOIN paliad.proceeding_types pt ON pt.id = sr.proceeding_type_id
		 WHERE ` + strings.Join(where, "\n   AND ") + `
		 ORDER BY score DESC, pt.sort_order, pe.code
		 LIMIT $` + fmt.Sprintf("%d", len(args)+1)

	args = append(args, limit)

	type row struct {
		EventID        uuid.UUID      `db:"event_id"`
		Code           string         `db:"code"`
		NameDE         string         `db:"name_de"`
		NameEN         string         `db:"name_en"`
		EventKind      sql.NullString `db:"event_kind"`
		Description    sql.NullString `db:"description"`
		PrimaryParty   sql.NullString `db:"primary_party"`
		ConceptID      *uuid.UUID     `db:"concept_id"`
		AnchorRuleID   uuid.UUID      `db:"anchor_rule_id"`
		PTID           int            `db:"pt_id"`
		PTCode         string         `db:"pt_code"`
		PTNameDE       string         `db:"pt_name_de"`
		PTNameEN       string         `db:"pt_name_en"`
		PTJurisdiction sql.NullString `db:"pt_jurisdiction"`
		FollowUpCount  int            `db:"follow_up_count"`
		Score          float64        `db:"score"`
	}

	var rows []row
	if err := s.db.SelectContext(ctx, &rows, query, args...); err != nil {
		return nil, fmt.Errorf("search events: %w", err)
	}

	hits := make([]EventSearchHit, 0, len(rows))
	for _, r := range rows {
		hit := EventSearchHit{
			EventID:       r.EventID,
			Code:          r.Code,
			NameDE:        r.NameDE,
			NameEN:        r.NameEN,
			AnchorRuleID:  r.AnchorRuleID,
			FollowUpCount: r.FollowUpCount,
			ConceptID:     r.ConceptID,
			Score:         r.Score,
			ProceedingType: EventSearchPT{
				ID:     r.PTID,
				Code:   r.PTCode,
				NameDE: r.PTNameDE,
				NameEN: r.PTNameEN,
			},
		}
		if r.EventKind.Valid {
			v := r.EventKind.String
			hit.EventKind = &v
		}
		if r.Description.Valid {
			v := r.Description.String
			hit.Description = &v
		}
		if r.PrimaryParty.Valid {
			v := r.PrimaryParty.String
			hit.PrimaryParty = &v
		}
		if r.PTJurisdiction.Valid {
			v := r.PTJurisdiction.String
			hit.ProceedingType.Jurisdiction = &v
		}
		hits = append(hits, hit)
	}
	resp.Events = hits
	resp.Total = len(hits)
	return resp, nil
}

func buildEventFilters(opts EventSearchOptions) EventSearchFilters {
	f := EventSearchFilters{}
	if opts.Jurisdiction != "" {
		v := opts.Jurisdiction
		f.Jurisdiction = &v
	}
	if opts.ProceedingTypeCode != "" {
		v := opts.ProceedingTypeCode
		f.ProceedingTypeCode = &v
	}
	if opts.EventKind != "" {
		v := opts.EventKind
		f.EventKind = &v
	}
	if opts.PrimaryParty != "" {
		v := opts.PrimaryParty
		f.PrimaryParty = &v
	}
	return f
}
