"""Custom-element builder, registry, and TypeScript codegen.

A custom element is a light-DOM Web Component: the Python builder emits a
semantic tag whose typed props become kebab-case attributes and whose behavior
lives in a compiled TS module (loaded via Media). One ``TypedDict`` per element
is the single source of truth for the server<->client contract;
``gen_element_types`` turns each registered spec into a TS interface + attribute
reader so drift fails ``tsc``.
"""

from dataclasses import dataclass
from typing import TypedDict, get_type_hints

from common.components.core import Node
from common.components.primitives import (
    Div,
    Input,
    Label,
    Template,
    custom_element_builder,
)


@dataclass(frozen=True)
class ElementSpec:
    tag: str  # e.g. "game-status-selector"
    ts_name: str  # e.g. "GameStatusSelector"
    props: type  # a TypedDict subclass


ELEMENT_REGISTRY: list[ElementSpec] = []


def register_element(tag: str, ts_name: str, props: type) -> None:
    """Register an element so codegen can emit its TS contract."""
    ELEMENT_REGISTRY.append(ElementSpec(tag, ts_name, props))


def _kebab(name: str) -> str:
    return name.replace("_", "-")


# ── Codegen ──────────────────────────────────────────────────────────────────

_TYPE_MAP = {int: "number", float: "number", str: "string", bool: "boolean"}


def _camel(name: str) -> str:
    head, *tail = name.split("_")
    return head + "".join(part.title() for part in tail)


def _reader_expr(name: str, python_type: type) -> str:
    attr = _kebab(name)
    if python_type in (int, float):
        return f'Number(el.getAttribute("{attr}"))'
    if python_type is bool:
        return f'el.getAttribute("{attr}") === "true"'
    return f'el.getAttribute("{attr}") ?? ""'


def _ts_for_spec(spec: ElementSpec) -> str:
    hints = get_type_hints(spec.props)
    interface_lines = "\n".join(
        f"  {_camel(name)}: {_TYPE_MAP[python_type]};"
        for name, python_type in hints.items()
    )
    reader_lines = "\n".join(
        f"    {_camel(name)}: {_reader_expr(name, python_type)},"
        for name, python_type in hints.items()
    )
    return (
        f"export interface {spec.ts_name}Props {{\n{interface_lines}\n}}\n\n"
        f"export function read{spec.ts_name}Props(el: HTMLElement): "
        f"{spec.ts_name}Props {{\n  return {{\n{reader_lines}\n  }};\n}}"
    )


def render_props_module() -> str:
    """The full ``ts/generated/props.ts`` content for every registered element."""
    header = "// GENERATED by `manage.py gen_element_types` — do not edit.\n"
    blocks = [_ts_for_spec(spec) for spec in ELEMENT_REGISTRY]
    return header + "\n" + "\n\n".join(blocks) + "\n"


# ── Element prop schemas (registered at import time) ─────────────────────────


class GameStatusSelectorProps(TypedDict):
    game_id: int
    status: str
    csrf: str


register_element("game-status-selector", "GameStatusSelector", GameStatusSelectorProps)


class SessionDeviceSelectorProps(TypedDict):
    session_id: int
    csrf: str


register_element(
    "session-device-selector", "SessionDeviceSelector", SessionDeviceSelectorProps
)


class PlayEventRowProps(TypedDict):
    game_id: int
    csrf: str
    api_create_url: str


register_element("play-event-row", "PlayEventRow", PlayEventRowProps)


class SessionTimestampButtonsProps(TypedDict):
    pass


register_element(
    "session-timestamp-buttons", "SessionTimestampButtons", SessionTimestampButtonsProps
)


# ── Named tag builders (consistent htpy-style with Div/Span) ─────────────────
# Underscore-prefixed: used internally by domain wrappers.
# Public ones (no domain wrapper): exported directly.

_GameStatusSelector = custom_element_builder("game-status-selector")
_SessionDeviceSelector = custom_element_builder("session-device-selector")
_PlayEventRow = custom_element_builder("play-event-row")
SessionTimestampButtons = custom_element_builder("session-timestamp-buttons")


class SelectionFieldsProps(TypedDict):
    source: str  # data-name of the source SearchSelect to mirror
    name_prefix: str  # each rendered input is named f"{name_prefix}{item_id}"
    field_type: str  # input type, e.g. "number"
    min_items: int  # render nothing until at least this many items are selected
    active: bool  # when false, render nothing (but preserve typed values)


register_element("selection-fields", "SelectionFields", SelectionFieldsProps)

_SelectionFields = custom_element_builder("selection-fields")


class RangeSliderProps(TypedDict):
    min: int
    max: int
    step: int
    mode: str  # "range" | "point"


register_element("range-slider", "RangeSlider", RangeSliderProps)
_RangeSlider = custom_element_builder("range-slider")


class DateRangePickerProps(TypedDict):
    pass


register_element("date-range-picker", "DateRangePicker", DateRangePickerProps)
_DateRangePicker = custom_element_builder("date-range-picker")


class SearchSelectProps(TypedDict):
    name: str
    search_url: str
    multi: bool
    filter_mode: bool
    free_text: bool
    always_visible: bool
    prefetch: int
    sync_url: bool


register_element("search-select", "SearchSelect", SearchSelectProps)
_SearchSelect = custom_element_builder("search-select")


class FilterBarProps(TypedDict):
    preset_list_url: str
    preset_save_url: str


register_element("filter-bar", "FilterBar", FilterBarProps)
_FilterBarElement = custom_element_builder("filter-bar")


class YearPickerProps(TypedDict):
    selected_year: str  # "" for the all-time/empty state
    available_years: str  # csv, e.g. "2019,2020"
    url_template: str  # contains the literal __year__ placeholder


# The <year-picker> builder lives in primitives.py (next to YearPicker, which
# uses it) because custom_elements imports from primitives — registering here
# would be a circular import. Registration is codegen-only, so it belongs here.
register_element("year-picker", "YearPicker", YearPickerProps)


def SelectionFields(
    *,
    source: str,
    name_prefix: str,
    field_type: str = "text",
    min_items: int = 1,
    active: bool = False,
    input_attributes: list[tuple[str, str]] | None = None,
) -> Node:
    """Render one synced form field per selected item of a source SearchSelect.

    General-purpose: it mirrors the SearchSelect named ``source`` and emits an
    input named ``f"{name_prefix}{item_id}"`` per selected item. Behavior lives
    in ``ts/elements/selection-fields.ts``; this is just the server-rendered
    light DOM (an empty rows container + a row ``<template>``). Inputs inherit
    the global ``#add-form`` styling, so the markup stays minimal.
    """
    row_template = Template(attributes=[("data-selection-fields-row", "")])[
        Div(attributes=[("data-selection-fields-row-item", "")])[
            Label(attributes=[("data-selection-fields-label", "")]),
            Input(type=field_type, attributes=list(input_attributes or [])),
        ]
    ]
    return _SelectionFields(
        source=source,
        name_prefix=name_prefix,
        field_type=field_type,
        min_items=min_items,
        active="true" if active else "false",
    )[
        Div(attributes=[("data-selection-fields-rows", "")]),
        row_template,
    ]
