"""Shared constants, styles, and small helpers for the Segment library.

The visual language mirrors AI Edit's prompt-templates dialog component for
component (navigation rail, cards, detail popup, buttons), with the accents
swapped to AI Segmentation's brand green.
"""
from __future__ import annotations

import calendar
import time

from qgis.PyQt.QtCore import QLocale
from qgis.PyQt.QtWidgets import QLabel, QWidget

from ....core import qt_compat as QtC
from ....core.i18n import tr
from ....core.presets.segmentation_presets_client import absolute_demo_url
from ...dock.font_scale import scale_qss_font_px as _scale_qss_font_px
from ...dock.styles import (
    BRAND_BLUE,
    BRAND_BLUE_HOVER,
    BRAND_GREEN,
    BTN_GREEN,
    BTN_GREEN_HOVER,
)

_BRAND_GREEN = BTN_GREEN       # AI Segmentation primary (Detect / Use)

# Favorite-star gold: a library-local accent for the checked/hover star. Kept
# here rather than in styles.py since nothing outside this dialog uses it.
_FAVORITE_STAR_GOLD = "#f6b100"

# Rail targets for the rows that are not a catalogue category. The two history
# rows map onto the server-side view names via _RAIL_HISTORY_VIEWS.
_RAIL_RECENT_TARGET = "__recent__"
_RAIL_FAVORITES_TARGET = "__favorites__"
_RAIL_POPULAR_TARGET = "__top__"
_RAIL_HISTORY_VIEWS = {
    _RAIL_RECENT_TARGET: "all",
    _RAIL_FAVORITES_TARGET: "favorites",
}

# ---------------------------------------------------------------------------
# QSS (mirrors AI Edit prompt_templates/common.py + generation_detail/styles.py)
# ---------------------------------------------------------------------------

_SEARCH_QSS = (
    "QLineEdit { border: 1px solid rgba(128,128,128,0.3);"
    " border-radius: 4px; padding: 6px 10px; font-size: 13px;"
    " color: palette(text); background: palette(base); }"
)
_CARD_NORMAL = (
    "QFrame#card { border: 1px solid rgba(128,128,128,0.30);"
    " border-radius: 6px; background: rgba(128,128,128,0.05); }"
)
# Hover is an active tint, so it belongs to the lime family. The CTA green is
# reserved for advance/commit and must not stand in for it.
_CARD_HOVER = (
    "QFrame#card { border: 1px solid rgba(139,172,39,0.75);"
    " border-radius: 6px; background: rgba(139,172,39,0.09); }"
)
# Count pill painted over the preview image, so the footer keeps the prompt and
# the date to itself. Dark scrim, because it sits on unpredictable imagery.
_OVERLAY_BADGE_QSS = (
    "QLabel { background: rgba(0,0,0,0.62); color: #ffffff; font-size: 10px;"
    " font-weight: 700; border: none; border-radius: 9px; padding: 2px 8px; }"
)
# Right-aligned click affordance on every card footer: a faint chevron at rest
# that becomes a green "Use ->" on hover. Swapped by each card's enter/leave.
_USE_HINT_REST = (
    "QLabel { color: rgba(128,128,128,0.60); font-size: 13px; font-weight: 700;"
    " background: transparent; border: none; }"
)
_USE_HINT_HOVER = (
    f"QLabel {{ color: {BTN_GREEN_HOVER}; font-size: 12px; font-weight: 700;"
    " background: transparent; border: none; }"
)
_META_QSS = (
    "font-size: 11px; color: rgba(128,128,128,0.85);"
    " background: transparent; border: none;"
)
# Empty states are hero-only: one glyph, one sentence, centered. The padding
# lives on the layout, not here, so the two parts stay a fixed distance apart.
_EMPTY_GLYPH = (
    "color: rgba(128,128,128,0.45); font-size: 34px;"
    " background: transparent; border: none;"
)
_EMPTY_MSG = (
    "color: rgba(128,128,128,0.95); font-size: 13px;"
    " background: transparent; border: none;"
)

_BLUE_BTN_QSS = (
    f"QPushButton {{ background-color: {BRAND_BLUE}; color: #ffffff; border: none;"
    " border-radius: 6px; padding: 9px 18px; font-weight: bold;"
    " font-size: 13px; }"
    f"QPushButton:hover {{ background-color: {BRAND_BLUE_HOVER}; }}"
    "QPushButton:disabled { background-color: rgba(128,128,128,0.25);"
    " color: rgba(128,128,128,0.8); }"
)
_GHOST_BTN_QSS = (
    "QPushButton { background: rgba(128,128,128,0.10); color: palette(text);"
    " border: none; border-radius: 6px; padding: 7px 14px; font-weight: 600;"
    " font-size: 12px; }"
    "QPushButton:hover { background: rgba(128,128,128,0.20); }"
)
_STAR_BTN_QSS = (
    "QToolButton { border: none; background: transparent; font-size: 16px;"
    " color: rgba(128,128,128,0.8); padding: 0 2px; }"
    f"QToolButton:checked {{ color: {_FAVORITE_STAR_GOLD}; }}"
    f"QToolButton:hover {{ color: {_FAVORITE_STAR_GOLD}; }}"
)

# ---- detail popup styles (AI Edit generation_detail/styles.py) ------------

_TITLE_STYLE = (
    "color: palette(text); font-size: 18px; font-weight: 800;"
    " letter-spacing: -0.2px; background: transparent; border: none;"
)
_SECTION_STYLE = (
    "color: rgba(128,128,128,0.95); font-size: 10px; font-weight: 700;"
    " letter-spacing: 1.2px; background: transparent; border: none;"
)
# Type/category tag above the title. Brand-green tint, hugs its content.
_BADGE_STYLE = (
    f"QLabel {{ color: {_BRAND_GREEN}; background: rgba(67,160,71,0.13);"
    " border: 1px solid rgba(67,160,71,0.40); border-radius: 9px;"
    " font-size: 10px; font-weight: 800; letter-spacing: 1.0px;"
    " padding: 2px 9px; }"
)
_SEPARATOR = "background: rgba(128,128,128,0.20); border: none;"
_PROMPT_STYLE = (
    "QLabel { color: palette(text); font-size: 12px;"
    " font-family: monospace;"
    " background: rgba(128,128,128,0.05); border: 1px solid rgba(128,128,128,0.15);"
    " border-radius: 4px; padding: 8px 10px; }"
)
_COPY_BTN = (
    "QPushButton { background: transparent; border: none;"
    " color: rgba(128,128,128,0.95); font-size: 11px; font-weight: 600;"
    " padding: 1px 6px; border-radius: 4px; }"
    "QPushButton:hover { background: rgba(128,128,128,0.14); color: palette(text); }"
)
_CHIP_STYLE = (
    "QFrame { background: rgba(128,128,128,0.06);"
    " border: 1px solid rgba(128,128,128,0.15); border-radius: 4px; }"
)
_CHIP_CAPTION = (
    "color: rgba(128,128,128,0.95); font-size: 9px; font-weight: 600;"
    " letter-spacing: 0.5px; background: transparent; border: none;"
)
_CHIP_VALUE = (
    "color: palette(text); font-size: 12px; font-weight: 600;"
    " background: transparent; border: none;"
)
_ACTION_BTN = (
    "QPushButton { background: transparent; border: 1px solid rgba(128,128,128,0.35);"
    " border-radius: 4px; padding: 7px 12px; font-size: 12px; color: palette(text); }"
    "QPushButton:hover { background: rgba(128,128,128,0.12);"
    " border-color: rgba(128,128,128,0.55); }"
    "QPushButton:disabled { color: rgba(128,128,128,0.5);"
    " border-color: rgba(128,128,128,0.15); }"
)
_PRIMARY_BTN = (
    f"QPushButton {{ background: {_BRAND_GREEN}; border: none; border-radius: 4px;"
    " padding: 8px 14px; font-size: 12px; font-weight: 600; color: #ffffff; }}"
    f"QPushButton:hover {{ background: {BTN_GREEN_HOVER}; }}"
    "QPushButton:disabled { background: rgba(128,128,128,0.25);"
    " color: rgba(128,128,128,0.6); }"
)
_FS_BTN = (
    "QToolButton { background: rgba(0,0,0,0.55); color: white; border: none;"
    " border-radius: 15px; font-size: 15px; }"
    "QToolButton:hover { background: rgba(0,0,0,0.8); }"
)
_DETAIL_STAR_BTN = (
    "QToolButton { background: transparent; border: 1px solid"
    " rgba(128,128,128,0.35); border-radius: 4px; font-size: 17px;"
    " color: rgba(128,128,128,0.8); }"
    "QToolButton:hover { background: rgba(128,128,128,0.15); }"
    f"QToolButton:checked {{ color: {_FAVORITE_STAR_GOLD}; }}"
)

# ---------------------------------------------------------------------------
# Navigation rail (left of the card grid). Mirrors AI Edit's library rail: the
# rows are deliberately plain (label + muted count, no glyph column and no
# per-entry hue) so the rail reads as a quiet index instead of a rainbow. The
# per-category tinted glyphs it replaces were the loudest thing in the dialog.
# ---------------------------------------------------------------------------

_RAIL_PANEL = (
    "QFrame#librail { border: none;"
    " border-right: 1px solid rgba(128,128,128,0.22); background: transparent; }"
)
# Group label above a cluster of rail rows. Sentence case, never uppercase:
# the design system bans uppercase across the plugin, so this is the one place
# the rail deviates from AI Edit's own sheet.
_RAIL_GROUP = (
    "QLabel { color: rgba(128,128,128,0.85); font-size: 10px; font-weight: 700;"
    " letter-spacing: 0.9px; background: transparent; border: none; }"
)
_RAIL_ITEM_LABEL = (
    "QLabel { color: palette(text); font-size: 13px;"
    " background: transparent; border: none; }"
)
# Muted count on the right of a rail row.
_RAIL_ITEM_COUNT = (
    "QLabel { color: rgba(128,128,128,0.85); font-size: 11px;"
    " background: transparent; border: none; }"
)


def _rail_item_style(active: bool) -> str:
    """QSS for one rail row. At rest: flat, transparent, hover tint. Active: a
    filled block, a bold label and a 3px left bar in the lime accent, so the
    selection reads as "you are here" rather than as a pressed button. The
    inactive row reserves the same 3px so the labels never shift sideways."""
    if active:
        qss = (
            "QPushButton#railitem { text-align: left; border: none;"
            f" border-left: 3px solid {BRAND_GREEN}; border-radius: 4px;"
            " padding: 8px 10px 8px 9px; font-size: 13px; font-weight: 700;"
            " color: palette(text); background: rgba(128,128,128,0.16); }"
        )
    else:
        qss = (
            "QPushButton#railitem { text-align: left; border: none;"
            " border-left: 3px solid transparent; border-radius: 4px;"
            " padding: 8px 10px 8px 9px; font-size: 13px;"
            " color: palette(text); background: transparent; }"
            "QPushButton#railitem:hover { background: rgba(128,128,128,0.10); }"
        )
    return _scale_qss_font_px(qss)


def _rail_label_style(active: bool) -> str:
    """QSS for a rail row's text. QSS does not cascade the button's color and
    weight into a child QLabel, so the active row restyles its label too."""
    if not active:
        return _RAIL_ITEM_LABEL
    return _scale_qss_font_px(
        "QLabel { color: palette(text); font-size: 13px; font-weight: 700;"
        " background: transparent; border: none; }"
    )


def _build_use_hint(parent) -> QLabel:
    hint = QLabel("›", parent)
    hint.setStyleSheet(_USE_HINT_REST)
    hint.setAttribute(QtC.WA_TransparentForMouseEvents)
    return hint


def _set_use_hint(hint: QLabel, hovered: bool) -> None:
    if hovered:
        hint.setText(f"{tr('Use')} →")
        hint.setStyleSheet(_USE_HINT_HOVER)
    else:
        hint.setText("›")
        hint.setStyleSheet(_USE_HINT_REST)


class _AspectBox(QWidget):
    """Keeps its single child at a fixed width:height ratio, centered. The
    before/after slider draws cover-fit, so matching the box ratio to the
    image ratio shows the whole image with no crop."""

    def __init__(self, child: QWidget, ratio: float, parent=None):
        super().__init__(parent)
        self._child = child
        child.setParent(self)
        self._ratio = ratio if ratio and ratio > 0 else 1.0
        self._overlay: QWidget | None = None
        self._overlay_margin = 10

    def set_ratio(self, ratio: float) -> None:
        self._ratio = ratio if ratio and ratio > 0 else 1.0
        self._relayout()

    def set_overlay(self, widget: QWidget) -> None:
        """Float ``widget`` over the bottom-right corner of the image rect."""
        self._overlay = widget
        widget.setParent(self)
        widget.raise_()
        self._relayout()

    def resizeEvent(self, event):  # noqa: N802 - Qt signature
        self._relayout()
        super().resizeEvent(event)

    def _relayout(self) -> None:
        w, h = self.width(), self.height()
        if w <= 0 or h <= 0:
            return
        if w / h > self._ratio:
            ch = h
            cw = int(round(h * self._ratio))
        else:
            cw = w
            ch = int(round(w / self._ratio))
        cx, cy = (w - cw) // 2, (h - ch) // 2
        self._child.setGeometry(cx, cy, cw, ch)
        if self._overlay is not None:
            ow = self._overlay.width()
            oh = self._overlay.height()
            m = self._overlay_margin
            self._overlay.move(cx + cw - ow - m, cy + ch - oh - m)
            self._overlay.raise_()


# ---------------------------------------------------------------------------
# Small data helpers
# ---------------------------------------------------------------------------


def _demo_url(base: str, preset: dict, which: str, preview: bool = False) -> str:
    """Resolve a preset's demo image URL (server path or synthesized from id)."""
    rel = preset.get(f"demo_url_{which}") or (
        f"/api/ai-segmentation/template-demos/{preset.get('id', '')}/{which}")
    url = absolute_demo_url(base, rel)
    if preview and url:
        url += ("&" if "?" in url else "?") + "size=preview"
    return url


def _relative_when(ts: str) -> str:
    """Relative age of a UTC ISO timestamp, coarsening as it gets older.

    Past a week, counting days stops helping ("412 days ago" tells nobody
    anything), so the unit grows with the distance.
    """
    try:
        parsed = time.strptime(ts, "%Y-%m-%dT%H:%M:%SZ")
        secs = time.time() - calendar.timegm(parsed)
    except (ValueError, TypeError):
        return ""
    days = int(secs // 86400)
    if days <= 0:
        return tr("today")
    if days == 1:
        return tr("yesterday")
    if days < 7:
        return tr("{n} days ago").format(n=days)
    if days < 31:
        weeks = days // 7
        return tr("a week ago") if weeks == 1 else tr("{n} weeks ago").format(n=weeks)
    if days < 365:
        months = max(1, days // 30)
        return tr("a month ago") if months == 1 else tr("{n} months ago").format(n=months)
    years = days // 365
    return tr("a year ago") if years == 1 else tr("{n} years ago").format(n=years)


def _iso_norm(ts) -> str:
    """Normalize a server ISO timestamp to the '%Y-%m-%dT%H:%M:%SZ' shape
    _relative_when parses.

    The offset is read, not discarded: chopping at 19 characters would turn a
    '+02:00' stamp into a UTC one and shift the age by the offset.
    """
    ts = str(ts or "").strip()
    if len(ts) < 19:
        return ""
    body, tail = ts[:19], ts[19:]
    # Drop fractional seconds, then look at what is left for an offset.
    if tail.startswith("."):
        idx = 1
        while idx < len(tail) and tail[idx].isdigit():
            idx += 1
        tail = tail[idx:]
    if not tail or tail in ("Z", "z", "+00:00", "-00:00", "+0000", "-0000"):
        return body + "Z"
    sign = tail[0]
    if sign not in ("+", "-"):
        return body + "Z"
    digits = tail[1:].replace(":", "")
    if len(digits) < 4 or not digits[:4].isdigit():
        return body + "Z"
    try:
        parsed = time.strptime(body, "%Y-%m-%dT%H:%M:%S")
    except ValueError:
        return body + "Z"
    shift = (int(digits[:2]) * 3600 + int(digits[2:4]) * 60) * (-1 if sign == "+" else 1)
    return time.strftime("%Y-%m-%dT%H:%M:%SZ",
                         time.gmtime(calendar.timegm(parsed) + shift))


def _fmt_count(value) -> str:
    """Group a count's thousands so 125549 reads as a magnitude, not a serial."""
    try:
        return QLocale().toString(int(value or 0))
    except (TypeError, ValueError):
        return "0"


def _run_key(run: dict) -> str:
    """Stable identity of a history run (real run_id or legacy group key)."""
    return str(run.get("run_id") or run.get("group_key") or "")


def _history_error(resp) -> str | None:
    """Error string of a history response, or None when it is usable."""
    if not isinstance(resp, dict):
        return "parse_error"
    if resp.get("error"):
        return str(resp.get("code") or "SERVER_ERROR")
    return None


# The panel follows the text size the user set in QGIS (see font_scale). A
# constant applied at build time is caught by the pass over the finished panel,
# but the same constant re-applied on a state change is not, and the widget
# would snap back to the base size mid-session. Growing them here, once, covers
# both. A no-op on the default text size, and outside QGIS.
for _qss_name in (
    "_RAIL_GROUP",
    "_RAIL_ITEM_LABEL",
    "_RAIL_ITEM_COUNT",
    "_SEARCH_QSS",
    "_OVERLAY_BADGE_QSS",
    "_USE_HINT_REST",
    "_USE_HINT_HOVER",
    "_META_QSS",
    "_EMPTY_GLYPH",
    "_EMPTY_MSG",
    "_BLUE_BTN_QSS",
    "_GHOST_BTN_QSS",
    "_STAR_BTN_QSS",
    "_TITLE_STYLE",
    "_SECTION_STYLE",
    "_BADGE_STYLE",
    "_PROMPT_STYLE",
    "_COPY_BTN",
    "_CHIP_CAPTION",
    "_CHIP_VALUE",
    "_ACTION_BTN",
    "_PRIMARY_BTN",
    "_FS_BTN",
    "_DETAIL_STAR_BTN",
):
    globals()[_qss_name] = _scale_qss_font_px(globals()[_qss_name])
del _qss_name
