package services

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

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

// EventCategoryService backs the Fristenrechner v3 decision tree
// (Pathway B / B1, t-paliad-133). The taxonomy is a recursive tree of
// "what happened" event categories; leaves map to deadline_concepts via
// the paliad.event_category_concepts junction.
//
// Two main operations:
//   1. Tree: hand the entire taxonomy to the frontend as a nested JSON
//      structure for the cascade UI (small dataset, ETag-cached).
//   2. ConceptsForSlug: given a slug like "cms-eingang.gericht.hinweisbeschluss",
//      return the (concept_id, proceeding_type_code) tuples reachable from
//      that node OR any of its descendants. Drives B1's narrowing of the
//      shared concept-card list.
type EventCategoryService struct {
	db *sqlx.DB
}

// NewEventCategoryService wires the service to its DB pool.
func NewEventCategoryService(db *sqlx.DB) *EventCategoryService {
	return &EventCategoryService{db: db}
}

// EventCategoryNode is one row in the taxonomy with its children attached.
// JSON shape is what the frontend consumes from
// GET /api/tools/fristenrechner/event-categories.
//
// Forums carries the optional coarse forum tags (#15) — `upc` / `de` /
// `epa` / `dpma`. Empty / nil = "neutral", node stays reachable from
// every inbox-channel chip setting. The frontend uses this to hide
// non-matching subtrees when an inbox is active.
type EventCategoryNode struct {
	ID              string              `json:"id"`
	Slug            string              `json:"slug"`
	LabelDE         string              `json:"label_de"`
	LabelEN         string              `json:"label_en"`
	DescriptionDE   *string             `json:"description_de,omitempty"`
	DescriptionEN   *string             `json:"description_en,omitempty"`
	StepQuestionDE  *string             `json:"step_question_de,omitempty"`
	StepQuestionEN  *string             `json:"step_question_en,omitempty"`
	Icon            *string             `json:"icon,omitempty"`
	SortOrder       int                 `json:"sort_order"`
	IsLeaf          bool                `json:"is_leaf"`
	Forums          []string            `json:"forums,omitempty"`
	// Party (#15 Slice 3c): coarse role tags — claimant, defendant,
	// both, court. NULL/empty = neutral, visible from every
	// perspective. The B1 panel's perspective chip uses this to hide
	// nodes that don't apply to the user's side (Klägerseite never
	// files Klageerwiderung; Beklagtenseite never files Klageschrift).
	Party           []string            `json:"party,omitempty"`
	Children        []EventCategoryNode `json:"children,omitempty"`
}

// ConceptOutcome maps a leaf to one (concept, optional proceeding) pair.
// Used to narrow the deadline_search matview by event_category.
type ConceptOutcome struct {
	ConceptID          string  `db:"concept_id"          json:"concept_id"`
	ProceedingTypeCode *string `db:"proceeding_type_code" json:"proceeding_type_code,omitempty"`
	SortOrder          int     `db:"sort_order"          json:"sort_order"`
}

// categoryRow is the flat row shape from the DB.
type categoryRow struct {
	ID             string         `db:"id"`
	ParentID       sql.NullString `db:"parent_id"`
	Slug           string         `db:"slug"`
	LabelDE        string         `db:"label_de"`
	LabelEN        string         `db:"label_en"`
	DescriptionDE  sql.NullString `db:"description_de"`
	DescriptionEN  sql.NullString `db:"description_en"`
	StepQuestionDE sql.NullString `db:"step_question_de"`
	StepQuestionEN sql.NullString `db:"step_question_en"`
	Icon           sql.NullString `db:"icon"`
	SortOrder      int            `db:"sort_order"`
	IsLeaf         bool           `db:"is_leaf"`
	Forums         pq.StringArray `db:"forums"`
	Party          pq.StringArray `db:"party"`
}

// Tree returns the full taxonomy as a list of root nodes with children
// nested in. Inactive rows are excluded.
//
// Result is small (≤ ~100 nodes today) and stable across requests, so the
// handler ETag-caches it.
func (s *EventCategoryService) Tree(ctx context.Context) ([]EventCategoryNode, error) {
	const sqlText = `
SELECT id, parent_id, slug, label_de, label_en,
       description_de, description_en,
       step_question_de, step_question_en,
       icon, sort_order, is_leaf, forums, party
  FROM paliad.event_categories
 WHERE is_active = true
 ORDER BY sort_order ASC, slug ASC
`
	var rows []categoryRow
	if err := s.db.SelectContext(ctx, &rows, sqlText); err != nil {
		return nil, fmt.Errorf("event_categories list: %w", err)
	}

	// Build node map and stitch children to parents in one pass.
	nodes := make(map[string]*EventCategoryNode, len(rows))
	for _, r := range rows {
		n := EventCategoryNode{
			ID:        r.ID,
			Slug:      r.Slug,
			LabelDE:   r.LabelDE,
			LabelEN:   r.LabelEN,
			SortOrder: r.SortOrder,
			IsLeaf:    r.IsLeaf,
		}
		if r.DescriptionDE.Valid {
			n.DescriptionDE = &r.DescriptionDE.String
		}
		if r.DescriptionEN.Valid {
			n.DescriptionEN = &r.DescriptionEN.String
		}
		if r.StepQuestionDE.Valid {
			n.StepQuestionDE = &r.StepQuestionDE.String
		}
		if r.StepQuestionEN.Valid {
			n.StepQuestionEN = &r.StepQuestionEN.String
		}
		if r.Icon.Valid {
			n.Icon = &r.Icon.String
		}
		if len(r.Forums) > 0 {
			n.Forums = []string(r.Forums)
		}
		if len(r.Party) > 0 {
			n.Party = []string(r.Party)
		}
		nodes[r.ID] = &n
	}

	var roots []EventCategoryNode
	for _, r := range rows {
		node := nodes[r.ID]
		if !r.ParentID.Valid {
			roots = append(roots, *node)
			continue
		}
		parent, ok := nodes[r.ParentID.String]
		if !ok {
			// Orphan (parent inactive or deleted) — surface as root so the
			// taxonomy doesn't disappear. Defensive; shouldn't happen in
			// practice given is_active=true filter applies to both sides.
			roots = append(roots, *node)
			continue
		}
		parent.Children = append(parent.Children, *node)
	}

	// Re-collect children from the map into the root copies — the
	// `roots = append(roots, *node)` above stored a snapshot, so we need
	// to walk back through and replace each with the live pointer's data.
	final := make([]EventCategoryNode, 0, len(roots))
	for _, root := range roots {
		live := nodes[root.ID]
		final = append(final, *live)
	}
	return final, nil
}

// ConceptsForSlug returns the (concept, optional proceeding_code) tuples
// reachable from the named slug OR any of its descendants. Empty slug
// returns nothing (caller must validate). Unknown slug also returns
// empty without error so the caller can render an empty result UI.
func (s *EventCategoryService) ConceptsForSlug(ctx context.Context, slug string) ([]ConceptOutcome, error) {
	if slug == "" {
		return nil, nil
	}

	// Single recursive CTE walks the descendants of `slug`, then joins
	// the junction. text-cast on concept_id keeps the slice serialisable
	// to a stable JSON shape.
	const sqlText = `
WITH RECURSIVE descendants AS (
    SELECT id FROM paliad.event_categories
     WHERE slug = $1 AND is_active = true
    UNION ALL
    SELECT c.id
      FROM paliad.event_categories c
      JOIN descendants d ON c.parent_id = d.id
     WHERE c.is_active = true
)
SELECT DISTINCT
       ecc.concept_id::text     AS concept_id,
       ecc.proceeding_type_code AS proceeding_type_code,
       min(ecc.sort_order)      AS sort_order
  FROM paliad.event_category_concepts ecc
  JOIN descendants d ON d.id = ecc.event_category_id
 GROUP BY ecc.concept_id, ecc.proceeding_type_code
 ORDER BY sort_order ASC, concept_id ASC
`
	var rows []ConceptOutcome
	if err := s.db.SelectContext(ctx, &rows, sqlText, slug); err != nil {
		return nil, fmt.Errorf("event_category concepts for %q: %w", slug, err)
	}
	return rows, nil
}

// ConceptIDsForSlug is the convenience reduction of ConceptsForSlug to a
// flat slice of concept_ids for use in `WHERE concept_id = ANY($1::uuid[])`
// queries against paliad.deadline_search.
func (s *EventCategoryService) ConceptIDsForSlug(ctx context.Context, slug string) ([]string, error) {
	rows, err := s.ConceptsForSlug(ctx, slug)
	if err != nil {
		return nil, err
	}
	if len(rows) == 0 {
		return nil, nil
	}
	seen := make(map[string]bool, len(rows))
	out := make([]string, 0, len(rows))
	for _, r := range rows {
		if seen[r.ConceptID] {
			continue
		}
		seen[r.ConceptID] = true
		out = append(out, r.ConceptID)
	}
	return out, nil
}

// AllOutcomes returns the distinct (concept_id, proceeding_type_code)
// tuples present in paliad.event_category_concepts across the entire
// taxonomy. Drives B1 browse-all mode (no slug picked yet, show every
// concept-context the tree can reach).
//
// Distinct from "every concept_id ever mapped" because a concept can
// appear at the root view in MULTIPLE proceeding contexts that the tree
// authors intentionally surfaced — e.g. opposition under both epa.opp.opd
// and dpma.opp.dpma. We respect those tuples even at the root so the
// result-card pill set matches the junction's design.
func (s *EventCategoryService) AllOutcomes(ctx context.Context) ([]ConceptOutcome, error) {
	const sqlText = `
SELECT DISTINCT
       ecc.concept_id::text     AS concept_id,
       ecc.proceeding_type_code AS proceeding_type_code,
       min(ecc.sort_order)      AS sort_order
  FROM paliad.event_category_concepts ecc
 GROUP BY ecc.concept_id, ecc.proceeding_type_code
 ORDER BY sort_order ASC, concept_id ASC
`
	var rows []ConceptOutcome
	if err := s.db.SelectContext(ctx, &rows, sqlText); err != nil {
		return nil, fmt.Errorf("event_category all outcomes: %w", err)
	}
	return rows, nil
}

// ProceedingCodesForSlug returns the distinct proceeding_type_code
// values associated with the slug's reachable concept set. Used by the
// search service to AND the user's forum filter against the leaf's
// per-concept narrowing.
//
// NULL proceeding_type_code (concept applies to all contexts) is
// reported as the empty string in the result; callers treat empty as
// "no narrowing at this leaf".
func (s *EventCategoryService) ProceedingCodesForSlug(ctx context.Context, slug string) ([]string, error) {
	if slug == "" {
		return nil, nil
	}
	const sqlText = `
WITH RECURSIVE descendants AS (
    SELECT id FROM paliad.event_categories WHERE slug = $1 AND is_active = true
    UNION ALL
    SELECT c.id FROM paliad.event_categories c
      JOIN descendants d ON c.parent_id = d.id
     WHERE c.is_active = true
)
SELECT DISTINCT COALESCE(ecc.proceeding_type_code, '') AS code
  FROM paliad.event_category_concepts ecc
  JOIN descendants d ON d.id = ecc.event_category_id
 ORDER BY code
`
	var codes pq.StringArray
	rows, err := s.db.QueryContext(ctx, sqlText, slug)
	if err != nil {
		return nil, fmt.Errorf("proceeding codes for %q: %w", slug, err)
	}
	defer rows.Close()
	for rows.Next() {
		var c string
		if err := rows.Scan(&c); err != nil {
			return nil, fmt.Errorf("scan proceeding code: %w", err)
		}
		codes = append(codes, c)
	}
	return []string(codes), rows.Err()
}
