"""
Single source of truth for list/detail display values.

The web templates are intentionally thin (see
``templates/aa_discord_audit/AGENTS.md``): views pre-compute display
values and the templates render ``{{ value }}``. This module is that
pre-compute layer for the two derived presentation concerns shared
across every list/detail view -- semantic badges keyed on enum values
and relative timestamps -- so a colour map or a phrasing lives in one
place instead of drifting across six templates.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING, Final

from django.utils.timesince import timesince
from django.utils.translation import gettext_lazy as _

from .models import (
    AuditFindingState,
    AuditRunState,
    ConfigAction,
    InvocationRefusal,
    OrphanAction,
    OrphanCategory,
    RunTrigger,
)

if TYPE_CHECKING:  # pragma: no cover
    from collections.abc import Mapping
    from datetime import datetime

    from django.db.models import TextChoices
    from django.utils.functional import Promise

_EM_DASH: Final[str] = "—"

_BADGE: Final[Mapping[str, tuple[type[TextChoices], Mapping[str, str]]]] = {
    "run_state": (
        AuditRunState,
        {
            "succeeded": "bg-success",
            "failed": "bg-danger",
            "budget_exceeded": "bg-warning text-dark",
            "interrupted": "bg-warning text-dark",
            "abandoned": "bg-secondary",
            "running": "bg-info text-dark",
            "pending": "bg-secondary",
        },
    ),
    "finding_category": (
        OrphanCategory,
        {
            "unknown_guest": "bg-warning text-dark",
            "linked_no_perm": "bg-danger",
            "bot_filtered": "bg-secondary",
        },
    ),
    "finding_state": (
        AuditFindingState,
        {
            "applied": "bg-success",
            "skipped": "bg-secondary",
            "failed": "bg-danger",
            "pending": "bg-info text-dark",
        },
    ),
    "action": (
        OrphanAction,
        {
            "report": "bg-info text-dark",
            "strip": "bg-warning text-dark",
            "strip_kick": "bg-danger",
        },
    ),
    "trigger": (
        RunTrigger,
        {
            "cli": "bg-secondary",
            "beat": "bg-info text-dark",
            "manual": "bg-primary",
            "web": "bg-dark",
        },
    ),
    "refusal": (
        InvocationRefusal,
        {
            "permission_denied": "bg-danger",
            "rate_limited": "bg-warning text-dark",
            "confirmation_declined": "bg-secondary",
            "initial_lock_held": "bg-secondary",
        },
    ),
    "config_action": (
        ConfigAction,
        {
            "create": "bg-success",
            "update": "bg-info text-dark",
            "delete": "bg-danger",
        },
    ),
}

# FontAwesome 6 solid glyph + compact label per badge value. A dense
# list table cannot afford the verbose enum ``.label`` (kept verbatim
# for admin / CSV / filter dropdowns): the long string sets a wide
# ``min-content`` and inflates the column. Here the badge gains an icon
# and, where the second tuple element is non-empty, a short label; the
# full enum label then moves into the hover ``title``. ``run_state``
# carries an icon only (empty short label) -- its labels are already
# single words -- so its rendered label stays the full enum label and
# matches the inline span still used by the run-detail view.
_BADGE_GLYPH: Final[Mapping[str, Mapping[str, tuple[str, str | Promise]]]] = {
    "run_state": {
        "pending": ("fa-hourglass-start", ""),
        "running": ("fa-spinner", ""),
        "succeeded": ("fa-circle-check", ""),
        "failed": ("fa-circle-xmark", ""),
        "budget_exceeded": ("fa-gauge-high", ""),
        "abandoned": ("fa-circle-stop", ""),
        "interrupted": ("fa-circle-pause", ""),
    },
    "trigger": {
        "cli": ("fa-terminal", _("CLI")),
        "beat": ("fa-clock", _("Beat")),
        "manual": ("fa-plug", _("API")),
        "web": ("fa-globe", _("Web")),
    },
    "refusal": {
        "rate_limited": ("fa-hourglass-half", _("Rate limit")),
        "permission_denied": ("fa-ban", _("No perm")),
        "confirmation_declined": ("fa-triangle-exclamation", _("No --yes")),
        "initial_lock_held": ("fa-lock", _("Not ack'd")),
    },
}

_FLAG_CSS: Final[Mapping[str, str]] = {
    "dry_run": "bg-info text-dark",
    "was_kicked": "bg-danger",
    "first_run_lock": "bg-warning text-dark",
    "was_confirmation_bypassed": "bg-danger",
}

_FLAG_GLYPH: Final[Mapping[str, str]] = {
    "dry_run": "fa-flask",
    "was_kicked": "fa-user-slash",
    "first_run_lock": "fa-lock",
    "was_confirmation_bypassed": "fa-triangle-exclamation",
}


def build_badge(kind: str, value: str) -> dict[str, str]:
    """
    Map an enum ``value`` to ``{"css", "icon", "label", "title"}``.

    ``kind`` must be a registered key in ``_BADGE`` -- an unknown kind
    raises ``KeyError`` (a programming error). An unknown ``value``
    falls back to ``bg-secondary`` with the raw string as the label.

    ``label`` is the compact term shown in the dense list cell; when it
    differs from the full enum label, the full text is returned in
    ``title`` for the hover tooltip. ``icon`` is a FontAwesome class
    (empty string when the value has no registered glyph).
    """
    enum_cls, colours = _BADGE[kind]
    css = colours.get(value, "bg-secondary")
    try:
        full = str(enum_cls(value).label)
    except ValueError:
        full = value
    icon, short = _BADGE_GLYPH.get(kind, {}).get(value, ("", ""))
    short_text = str(short)
    label = short_text or full
    title = full if short_text and short_text != full else ""
    return {"css": css, "icon": icon, "label": label, "title": title}


def flag_badge(kind: str, *, value: bool) -> dict[str, str] | None:
    """Risk badge for a boolean flag; ``None`` (no badge) when false."""
    if not value:
        return None
    return {
        "css": _FLAG_CSS[kind],
        "icon": _FLAG_GLYPH[kind],
        "label": str(_("yes")),
        "title": "",
    }


def relative_since(dt: datetime | None) -> str:
    """Human "3 hours ago"; em-dash for ``None`` (unfinished/unset)."""
    if dt is None:
        return _EM_DASH
    return str(_("%(time)s ago") % {"time": timesince(dt)})


def pretty_json(value: object) -> str:
    """
    Operator-facing JSON block: indented, stable key order, unicode-safe.

    Single source of truth for how a JSON snapshot is rendered in the
    dashboard (changelog payloads, policy snapshots, diff cells) so the
    ``indent`` / ``sort_keys`` / ``ensure_ascii`` contract cannot drift
    across views. Redaction stays the caller's concern -- wrap the
    result in ``redact_str`` where the value may carry a secret.
    """
    return json.dumps(value, indent=2, ensure_ascii=False, sort_keys=True)
