#!/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. Bei
                      unveraendertem Output-Set bleibt eine verwaltete Config
                      unangetastet (Modus-Tweaks inklusive). Wird vom
                      labwc-Autostart bei jedem Login aufgerufen.

Moduswahl: Auf echter Hardware der EDID-preferred-Modus (meist nativ =
richtig). In VMs meldet die Firmware oft einen Kleinstmodus als preferred
(QEMU: 1280x800) - dort wird 1920x1080@60 genommen, wenn angeboten, sonst
preferred. Einmal geschrieben bleibt die Config bei gleichem Output-Set
erhalten (Modus-Tweaks inklusive); neu generiert wird nur bei geaendertem
Output-Set (Docking) oder per --apply.

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_vm():
    """True, wenn wir virtualisiert laufen (QEMU/Boxes-Test)."""
    try:
        proc = subprocess.run(
            ["systemd-detect-virt", "--vm"], capture_output=True, timeout=10
        )
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return False
    return proc.returncode == 0


def norm_rate(value):
    if isinstance(value, (int, float)) and value > 1000:
        return value / 1000  # mHz -> Hz
    return value


def choose_mode(modes, current, in_vm):
    """Modus waehlen: VM-Heuristik, sonst preferred, sonst current/erster."""
    if in_vm:
        for m in modes:
            rate = norm_rate(m.get("refresh") or m.get("rate")) or 0
            if m.get("width") == 1920 and m.get("height") == 1080 and round(rate) == 60:
                return m
    for m in modes:
        if m.get("preferred"):
            return m
    return current or (modes[0] if modes else None)


def detect_outputs():
    """Liste von Dicts: name, internal, width, height, rate, scale."""
    in_vm = detect_vm()
    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 []
        current = out.get("current_mode") or out.get("mode")
        chosen = choose_mode(modes, current, in_vm)
        if not chosen:
            continue
        outputs.append(
            {
                "name": name,
                "internal": name.startswith(("eDP-", "LVDS-", "DSI-")),
                "width": chosen.get("width"),
                "height": chosen.get("height"),
                "rate": norm_rate(chosen.get("refresh") or chosen.get("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 Modus direkt",
        "# in dieser Datei anpassen - bei unveraendertem Output-Set bleibt",
        "# die Datei (inkl. deiner Modus-Tweaks) bei jedem Login erhalten.",
        "# Neu generiert wird nur bei geaendertem Output-Set (Docking) oder",
        "# per --apply. Marker-Zeile loeschen = ganz hand-gepflegt.",
        "",
    ]
    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 config_output_names(path: Path):
    """Output-Namen aus einer bestehenden kanshi-Config (ohne '*' Fallback)."""
    names = set()
    try:
        for line in path.read_text().splitlines():
            parts = line.strip().split()
            if len(parts) >= 2 and parts[0] == "output" and parts[1] != "*":
                names.add(parts[1].strip("'\""))
    except OSError:
        pass
    return names


def main(argv):
    outputs = detect_outputs()
    if not outputs:
        print("WARN: keine Outputs erkannt", 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):
        if not outputs:
            print("# (Fallback-Anteil, da keine Outputs erkannt)", file=sys.stderr)
        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:
        if not CONFIG_PATH.exists():
            apply(text)  # Erst-Login: anlegen
            return 0
        if not is_managed(CONFIG_PATH):
            print(f"{CONFIG_PATH} ist hand-gepflegt - lasse sie unangetastet.")
            return 0
        if not outputs:
            # Erkennung fehlgeschlagen -> niemals eine gute Config clobbern.
            print("WARN: Erkennung fehlgeschlagen - lasse verwaltete Config unangetastet.",
                  file=sys.stderr)
            return 0
        if {o["name"] for o in outputs} == config_output_names(CONFIG_PATH):
            # Gleiches Output-Set (z.B. jeden Boot dieselbe VM-Anzeige):
            # Modus-Tweaks des Users bleiben erhalten, kein Rewrite.
            print(f"Outputs unveraendert ({', '.join(sorted(config_output_names(CONFIG_PATH)))})"
                  " - behalte Config (inkl. Modus-Tweaks) bei.")
            return 0
        # Output-Set geaendert (angedockt/abgedockt): neu generieren.
    apply(text)
    return 0


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