"""Build the right Adwaita row for each feature, by type and access mode.

Continuous RW -> Adw.SpinRow (debounced setvcp). Non-continuous RW with value
labels -> Adw.ComboRow. Read-only -> Adw.ActionRow showing the formatted value.
Write-only action codes -> ActionRow with an Apply button.
"""

from __future__ import annotations

from collections.abc import Callable

import gi

gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, GLib, Gtk  # noqa: E402

from .i18n import _  # noqa: E402
from .models import Access, Feature, ValueType  # noqa: E402

OnChange = Callable[[int, int, bool], None]
OnAction = Callable[[int], None]

_DEBOUNCE_MS = 350
# Editable non-continuous features without enumerated labels get a numeric
# slider over 0..max. Above this, max is almost always an identifier/counter
# (firmware level, usage hours) rather than an adjustable range.
_NC_RANGE_CAP = 255


def _esc(text: str) -> str:
    """Escape text for Adw title/subtitle, which interpret Pango markup."""
    return GLib.markup_escape_text(text, -1)


def format_value(feature: Feature) -> str:
    v = feature.value
    if v is None or not v.supported or v.current is None:
        return "—"
    if feature.values and v.current in feature.values:
        return _(feature.values[v.current])
    if feature.vtype is ValueType.CONTINUOUS and v.maximum:
        return f"{v.current} / {v.maximum}"
    return f"0x{v.current:02X}"


def _subtitle(feature: Feature, extra: str = "", *, show_code: bool = False) -> str:
    """Row subtitle. The raw VCP hex code is jargon for non-technical users, so
    it is shown only in the advanced section (``show_code``)."""
    if not show_code:
        return extra
    base = f"0x{feature.code:02X}"
    return f"{base} · {extra}" if extra else base


def _range_row(feature: Feature, on_change: OnChange, *, show_code: bool) -> Adw.PreferencesRow:
    v = feature.value
    assert v is not None
    maximum = max(1, v.maximum or 100)
    current = min(max(v.current or 0, 0), maximum)
    adjustment = Gtk.Adjustment(
        lower=0, upper=maximum, step_increment=1, page_increment=10, value=current
    )
    row = Adw.SpinRow(
        title=_esc(_(feature.name)),
        subtitle=_subtitle(feature, f"0–{maximum}", show_code=show_code),
        adjustment=adjustment,
        digits=0,
    )
    state: dict[str, int] = {"source": 0, "last": current}

    def apply() -> bool:
        state["source"] = 0
        value = int(row.get_value())
        if value != state["last"]:
            state["last"] = value
            on_change(feature.code, value, True)
        return GLib.SOURCE_REMOVE

    def on_notify(*_: object) -> None:
        if state["source"]:
            GLib.source_remove(state["source"])
        state["source"] = GLib.timeout_add(_DEBOUNCE_MS, apply)

    row.connect("notify::value", on_notify)
    return row


def _combo_row(feature: Feature, on_change: OnChange, *, show_code: bool) -> Adw.PreferencesRow:
    items = sorted(feature.values.items())
    model = Gtk.StringList()
    for _code, label in items:
        model.append(_(label))
    row = Adw.ComboRow(
        title=_esc(_(feature.name)), subtitle=_subtitle(feature, show_code=show_code), model=model
    )

    current = feature.value.current if feature.value else None
    for index, (val, _label) in enumerate(items):
        if val == current:
            row.set_selected(index)
            break

    def on_selected(*_: object) -> None:
        index = row.get_selected()
        if 0 <= index < len(items):
            on_change(feature.code, items[index][0], False)

    row.connect("notify::selected", on_selected)
    return row


def _info_row(feature: Feature, *, show_code: bool) -> Adw.PreferencesRow:
    subtitle = _subtitle(feature, show_code=show_code)
    row = Adw.ActionRow(title=_esc(_(feature.name)), subtitle=subtitle)
    label = Gtk.Label(label=format_value(feature), valign=Gtk.Align.CENTER)
    label.add_css_class("dim-label")
    row.add_suffix(label)
    return row


def _action_row(feature: Feature, on_action: OnAction, *, show_code: bool) -> Adw.PreferencesRow:
    subtitle = _subtitle(feature, show_code=show_code)
    row = Adw.ActionRow(title=_esc(_(feature.name)), subtitle=subtitle)
    button = Gtk.Button(label=_("Apply"), valign=Gtk.Align.CENTER)
    button.add_css_class("flat")
    button.connect("clicked", lambda *_a: on_action(feature.code))
    row.add_suffix(button)
    row.set_activatable_widget(button)
    return row


def build_feature_row(
    feature: Feature, on_change: OnChange, on_action: OnAction, *, show_code: bool = False
) -> Adw.PreferencesRow:
    """Pick and build the appropriate row widget for a feature.

    Editable features always get an actionable control: a dropdown when the
    monitor enumerates named values, otherwise a numeric slider over the
    feature's 0..max range (so manufacturer/unlabelled features stay usable).
    ``show_code`` adds the raw VCP hex code to the subtitle (advanced section).
    """
    if feature.access is Access.READ_ONLY:
        return _info_row(feature, show_code=show_code)
    if feature.access is Access.WRITE_ONLY:
        return _action_row(feature, on_action, show_code=show_code)

    v = feature.value
    if v is None or not v.supported:
        return _info_row(feature, show_code=show_code)
    if feature.values:
        return _combo_row(feature, on_change, show_code=show_code)
    if feature.vtype is ValueType.CONTINUOUS and v.maximum:
        return _range_row(feature, on_change, show_code=show_code)
    if v.maximum is not None and 0 < v.maximum <= _NC_RANGE_CAP:
        return _range_row(feature, on_change, show_code=show_code)
    return _info_row(feature, show_code=show_code)
