#!/usr/bin/env python3
"""kanshi-autoconfig - generiert kanshi-Profile aus live erkannten Outputs.

Liest die angeschlossenen Monitore via `wlr-randr --json` aus (EDID-Namen,
bevorzugte Modi, aktuelle Skalierung) und schreibt daraus eine
funktionsfaehige ~/.config/kanshi/config mit den Profilen
docked / undocked / fallback.

Verhalten:
  --print             Vorschlag auf stdout, schreibt nichts.
  --apply             Schreibt (mit Backup *.bak), fragt nicht.
  --apply-if-managed  Schreibt nur, wenn die Config fehlt oder den Marker
                      '# kanshi-autoconfig: managed' enthaelt.
                      -> Hand-Edits bleiben unangetastet. Wird vom
                      labwc-Autostart bei jedem Login aufgerufen.

Faellt die Erkennung fehl (kein Wayland, kein wlr-randr), wird eine
reine Fallback-Config geschrieben, damit nie ein schwarzer Bildschirm
ohne Layout dasteht.
"""

import json
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path

MARKER = "# kanshi-autoconfig: managed"
CONFIG_PATH = Path.home() / ".config" / "kanshi" / "config"


def detect_outputs():
    """Liste von Dicts: name, internal, width, height, rate, scale."""
    try:
        proc = subprocess.run(
            ["wlr-randr", "--json"], capture_output=True, text=True, timeout=10
        )
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return []
    if proc.returncode != 0:
        return []
    try:
        data = json.loads(proc.stdout)
    except json.JSONDecodeError:
        return []
    outputs = []
    for out in data:
        name = out.get("name", "?")
        # Nur verbundene Outputs betrachten (kanshi matcht nur angesteckte)
        if out.get("connected") is False:
            continue
        modes = out.get("modes", []) or []
        preferred = next((m for m in modes if m.get("preferred")), None)
        current = out.get("current_mode") or out.get("mode")
        chosen = preferred or current or (modes[0] if modes else None)
        if not chosen:
            continue
        width = chosen.get("width")
        height = chosen.get("height")
        rate = chosen.get("refresh") or chosen.get("rate")
        if isinstance(rate, (int, float)) and rate > 1000:
            rate = rate / 1000  # mHz -> Hz
        outputs.append(
            {
                "name": name,
                "internal": name.startswith(("eDP-", "LVDS-", "DSI-")),
                "width": width,
                "height": height,
                "rate": rate,
                "scale": out.get("scale", 1) or 1,
            }
        )
    return outputs


def mode_str(o):
    s = f"{o['width']}x{o['height']}"
    if o["rate"]:
        s += f"@{o['rate']:.2f}" if isinstance(o["rate"], float) else f"@{o['rate']}"
    return s


def render(outputs):
    lines = [
        MARKER,
        f"# generiert am {datetime.now().isoformat(timespec='seconds')}",
        "# Quelle: wlr-randr (live). Feintuning: wdisplays oder Hand-Edit",
        "# (Hand-Edit: Marker-Zeile loeschen, dann fasst --apply-if-managed",
        "#  diese Datei nicht mehr an.)",
        "",
    ]
    externals = [o for o in outputs if not o["internal"]]
    internals = [o for o in outputs if o["internal"]]

    if externals:
        lines.append("profile docked {")
        for i in internals:
            lines.append(f"  output {i['name']} disable")
        x = 0
        for o in externals:
            lines.append(
                f"  output {o['name']} mode {mode_str(o)} position {x},0 scale {o['scale']}"
            )
            x += o["width"] or 0
        lines.append("}")
        lines.append("")

    if internals:
        lines.append("profile undocked {")
        for o in internals:
            lines.append(
                f"  output {o['name']} mode {mode_str(o)} position 0,0 scale {o['scale']}"
            )
        lines.append("}")
        lines.append("")
    elif not externals:
        # Gar nichts erkannt -> reiner Fallback, nie leer lassen
        lines.append("# Keine Outputs erkannt (ausserhalb einer Wayland-Session")
        lines.append("# aufgerufen?) - nur Fallback geschrieben.")
        lines.append("")

    lines.append("profile fallback {")
    lines.append("  output * enable")
    lines.append("}")
    lines.append("")
    return "\n".join(lines)


def is_managed(path: Path):
    if not path.exists():
        return True
    try:
        return MARKER in path.read_text()
    except OSError:
        return False


def apply(text):
    CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
    if CONFIG_PATH.exists():
        backup = CONFIG_PATH.with_suffix(".bak")
        shutil.copy2(CONFIG_PATH, backup)
        print(f"Backup: {backup}")
    CONFIG_PATH.write_text(text)
    print(f"Geschrieben: {CONFIG_PATH}")


def main(argv):
    outputs = detect_outputs()
    if not outputs:
        print("WARN: keine Outputs erkannt - schreibe Fallback-Config", file=sys.stderr)
    text = render(outputs)
    # Default ohne Schreib-Flag: nur anzeigen, nichts schreiben (sicher).
    if "--print" in argv or ("--apply" not in argv and "--apply-if-managed" not in argv):
        print(text)
        if outputs:
            print(
                f"# Erkannt: {', '.join(o['name'] for o in outputs)}",
                file=sys.stderr,
            )
        return 0
    if "--apply-if-managed" in argv and not is_managed(CONFIG_PATH):
        print(f"{CONFIG_PATH} ist hand-gepflegt - lasse sie unangetastet.")
        return 0
    apply(text)
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
