"""Display / Output-Einstellungen — niri-only über native IPC. UI-frei.

Liest Monitore via `niri msg --json outputs` und normalisiert sie auf ein
`Output`-Dataclass. Apply erzeugt das passende argv (`niri msg output <name> …`).

Bewusst NUR live: `niri msg output` ändert die Konfiguration temporär, nichts
wird in die niri-Config (config.kdl) geschrieben. Nach einem Reload/Login sind
die Änderungen wieder weg.
"""
from __future__ import annotations

import json
import re
import shutil
import subprocess
from dataclasses import dataclass, field
from pathlib import Path

# Eigener TANINUX-User-Service für wlsunset (analog swayidle in core/power.py).
# Der niri-Autostart (spawn-at-startup) wird NICHT angefasst.
WLSUNSET_UNIT = (
    Path.home() / ".config" / "systemd" / "user" / "taninux-wlsunset.service"
)


@dataclass
class Output:
    name: str
    make: str
    model: str
    width: int
    height: int
    refresh_hz: float
    x: int
    y: int
    scale: float
    transform: str           # "normal" / "90" / "180" / "270"
    enabled: bool
    modes: list[tuple[int, int, float]] = field(default_factory=list)  # (w, h, hz)


# -- Lesen ----------------------------------------------------------------

def _run_json(argv: list[str]):
    try:
        r = subprocess.run(argv, capture_output=True, text=True, timeout=5)
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return None
    if r.returncode != 0 or not r.stdout.strip():
        return None
    try:
        return json.loads(r.stdout)
    except json.JSONDecodeError:
        return None


def _norm_transform(value) -> str:
    """niri-Transform → "normal"/"90"/"180"/"270".

    niri liefert z.B. "Normal", "_90", "Flipped", "Flipped90". Die UI bietet
    nur die vier rechtwinkligen Rotationen; flipped-* fallen auf "normal" /
    deren Rotationsanteil zurück.
    """
    s = str(value).lower().replace("flipped", "").strip("_-")
    return s if s in ("90", "180", "270") else "normal"


def outputs() -> list[Output]:
    # `niri msg --json outputs` liefert ein dict {name: output}.
    data = _run_json(["niri", "msg", "--json", "outputs"])
    if not isinstance(data, dict):
        return []
    result: list[Output] = []
    for name, o in sorted(data.items()):
        modes: list[tuple[int, int, float]] = []
        for md in o.get("modes", []) or []:
            modes.append((
                int(md.get("width", 0)),
                int(md.get("height", 0)),
                int(md.get("refresh_rate", 0)) / 1000.0,   # niri: mHz -> Hz
            ))
        # logical ist null, wenn der Output deaktiviert ist.
        logical = o.get("logical") or None
        enabled = logical is not None

        # current_mode ist ein Index in modes (null wenn aus / unbekannt).
        cur_idx = o.get("current_mode")
        if isinstance(cur_idx, int) and 0 <= cur_idx < len(modes):
            cw, ch, chz = modes[cur_idx]
        elif modes:
            cw, ch, chz = modes[0]
        else:
            cw, ch, chz = 0, 0, 0.0

        result.append(Output(
            name=name,
            make=o.get("make", "") or "",
            model=o.get("model", "") or "",
            width=cw,
            height=ch,
            refresh_hz=chz,
            x=int(logical.get("x", 0)) if logical else 0,
            y=int(logical.get("y", 0)) if logical else 0,
            scale=float(logical.get("scale", 1.0)) if logical else 1.0,
            transform=_norm_transform(logical.get("transform", "normal")) if logical else "normal",
            enabled=enabled,
            modes=modes,
        ))
    return result


# -- Apply ----------------------------------------------------------------

def _fmt_hz(hz: float) -> str:
    """Refresh kompakt: '143.973' statt '143.97300'. Ganze Zahlen ohne Punkt.

    Drei Nachkommastellen, weil niri die Modes mit mHz-Präzision führt
    (z.B. 59.951) und den Mode-String sonst nicht trifft.
    """
    s = f"{hz:.3f}".rstrip("0").rstrip(".")
    return s or "0"


def _fmt_scale(scale: float) -> str:
    s = f"{scale:.2f}".rstrip("0").rstrip(".")
    return s or "1"


def apply_argv(
    out: Output,
    *,
    mode: tuple[int, int, float] | None = None,
    scale: float | None = None,
    transform: str | None = None,
    enabled: bool | None = None,
) -> list[str]:
    """argv für GENAU eine Änderung an `out` via `niri msg output`.
    Unprivilegiert ausführbar. Genau einer der Keyword-Parameter sollte gesetzt sein.

    niri ändert die Output-Config temporär (nicht persistent) — passt zur
    bewussten „nur live"-Semantik dieses Moduls.
    """
    base = ["niri", "msg", "output", out.name]
    if enabled is not None:
        return base + (["on"] if enabled else ["off"])
    if mode is not None:
        w, h, hz = mode
        return base + ["mode", f"{w}x{h}@{_fmt_hz(hz)}"]
    if scale is not None:
        return base + ["scale", _fmt_scale(scale)]
    if transform is not None:
        return base + ["transform", transform]
    return base


# -- Night Light / Farbtemperatur (wlsunset) ------------------------------
#
# TANINUX verwaltet einen EIGENEN systemd-User-Service
# taninux-wlsunset.service (analog zum swayidle-Ansatz in core/power.py).
# wlsunset hat keine Config-Datei — alle Parameter stehen in den ExecStart-Args.
# Karims Compositor-Autostart bleibt unberührt.

# Defaults = Karims Werte aus dem Autostart.
_NL_DEFAULT_DAY = 6500
_NL_DEFAULT_NIGHT = 4000
_NL_DEFAULT_LAT = 47.0
_NL_DEFAULT_LON = 8.3


@dataclass
class NightLight:
    enabled: bool
    day_temp: int
    night_temp: int
    lat: float
    lon: float


def wlsunset_installed() -> bool:
    return shutil.which("wlsunset") is not None


def night_light() -> NightLight:
    """Liest die aktuellen Night-Light-Werte aus der TANINUX-Unit.

    enabled = Unit existiert (wir verwalten genau diesen Service). Die
    Parameter kommen aus den ExecStart-Args: -t <night>, -T <day>,
    -l <lat>, -L <lon>. Fehlt etwas, greifen die Defaults. Robust gegen
    fehlende/kaputte Unit.
    """
    day = _NL_DEFAULT_DAY
    night = _NL_DEFAULT_NIGHT
    lat = _NL_DEFAULT_LAT
    lon = _NL_DEFAULT_LON
    enabled = WLSUNSET_UNIT.is_file()
    if enabled:
        try:
            text = WLSUNSET_UNIT.read_text(errors="ignore")
        except OSError:
            text = ""
        m = re.search(r"^\s*ExecStart\s*=\s*(.+)$", text, re.MULTILINE)
        exec_line = m.group(1) if m else ""

        def _int_arg(flag: str, fallback: int) -> int:
            mm = re.search(rf"(?:^|\s){re.escape(flag)}\s+(\d+)", exec_line)
            return int(mm.group(1)) if mm else fallback

        def _float_arg(flag: str, fallback: float) -> float:
            mm = re.search(
                rf"(?:^|\s){re.escape(flag)}\s+(-?[\d.]+)", exec_line
            )
            try:
                return float(mm.group(1)) if mm else fallback
            except ValueError:
                return fallback

        night = _int_arg("-t", night)
        day = _int_arg("-T", day)
        lat = _float_arg("-l", lat)
        lon = _float_arg("-L", lon)
    return NightLight(
        enabled=enabled, day_temp=day, night_temp=night, lat=lat, lon=lon
    )


def _fmt_coord(v: float) -> str:
    """Koordinate kompakt: '47' statt '47.0', '8.3' bleibt '8.3'."""
    s = f"{v:.4f}".rstrip("0").rstrip(".")
    return s or "0"


def write_night_light(
    day_temp: int, night_temp: int, lat: float, lon: float
) -> Path:
    """Schreibt die TANINUX-verwaltete wlsunset-User-Unit. Alle Parameter
    landen in der ExecStart-Zeile. Gibt den geschriebenen Pfad zurück."""
    wlsunset = shutil.which("wlsunset") or "/usr/bin/wlsunset"
    exec_start = (
        f"{wlsunset} -l {_fmt_coord(lat)} -L {_fmt_coord(lon)} "
        f"-t {int(night_temp)} -T {int(day_temp)}"
    )
    lines = [
        "[Unit]",
        "Description=TANINUX-managed wlsunset (night light)",
        "Documentation=man:wlsunset(1)",
        "PartOf=graphical-session.target",
        "",
        "[Service]",
        "Type=simple",
        f"ExecStart={exec_start}",
        "Restart=on-failure",
        "",
        "[Install]",
        "WantedBy=default.target",
        "",
    ]
    WLSUNSET_UNIT.parent.mkdir(parents=True, exist_ok=True)
    WLSUNSET_UNIT.write_text("\n".join(lines))
    return WLSUNSET_UNIT


def apply_night_light(enable: bool) -> bool:
    """User-Service anwenden (best-effort, unprivilegiert).

    enable → daemon-reload + enable --now + restart.
    disable → disable --now. True wenn die Befehle abgesetzt wurden.
    """
    try:
        subprocess.run(
            ["systemctl", "--user", "daemon-reload"],
            check=False, timeout=8,
        )
        if enable:
            subprocess.run(
                ["systemctl", "--user", "enable", "--now",
                 "taninux-wlsunset.service"],
                check=False, timeout=8,
            )
            subprocess.run(
                ["systemctl", "--user", "restart", "taninux-wlsunset.service"],
                check=False, timeout=8,
            )
        else:
            subprocess.run(
                ["systemctl", "--user", "disable", "--now",
                 "taninux-wlsunset.service"],
                check=False, timeout=8,
            )
        return True
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return False
