"""Netzwerkeinstellungen des Hosts -- mit Sicherheitsnetz.

Eine falsche IP-Adresse auf einem Host, den man nur ueber das Netz erreicht,
ist ein Ausflug ins Rechenzentrum. Darum gilt hier dasselbe Muster wie im
Vorgaenger, nur sauberer umgesetzt:

1. Aenderung wird geschrieben und angewandt.
2. Ein Zeitgeber laeuft (``REVERT_SECONDS``).
3. Bestaetigt der Administrator nicht rechtzeitig **von der neuen Adresse aus**,
   wird automatisch zurueckgerollt.

Zwei Aenderungen gegenueber dem Vorgaenger:

* **Eigener Dateiname pro Anwendung.** Der Vorgaenger schrieb aus *beiden*
  Anwendungen nach ``/etc/netplan/90-tesm.yaml``. Liefen sie auf demselben Host,
  ueberschrieb eine die Konfiguration der anderen. Hier heisst die Datei
  ``90-<app_key>.yaml``.
* **DNS-Anzeige ueber ``resolvectl``**, nicht ``/etc/resolv.conf`` -- unter
  systemd-resolved steht dort immer nur der Stub 127.0.0.53.
"""

from __future__ import annotations

import ipaddress
import logging
import re
import shutil
from dataclasses import dataclass, field
from typing import Any

from ..sysops import SysOps

logger = logging.getLogger(__name__)

REVERT_SECONDS = 45

BACKEND_NETPLAN = "netplan"
BACKEND_NETWORK_MANAGER = "NetworkManager"
BACKEND_DHCPCD = "dhcpcd"
BACKEND_UNKNOWN = "unbekannt"


@dataclass(slots=True)
class InterfaceInfo:
    name: str
    addresses: list[str] = field(default_factory=list)
    state: str = "unbekannt"
    mac: str = ""
    gateway: str = ""
    dns: list[str] = field(default_factory=list)
    method: str = "unbekannt"


def detect_backend(sysops: SysOps) -> str:
    """Erkennt das aktive Netzwerk-Backend -- rein lesend."""
    if shutil.which("netplan"):
        return BACKEND_NETPLAN
    if sysops.read_command(["systemctl", "is-active", "NetworkManager"]).stdout.strip() == "active":
        return BACKEND_NETWORK_MANAGER
    if sysops.read_command(["systemctl", "is-active", "dhcpcd"]).stdout.strip() == "active":
        return BACKEND_DHCPCD
    return BACKEND_UNKNOWN


def list_interfaces(sysops: SysOps) -> list[InterfaceInfo]:
    result: dict[str, InterfaceInfo] = {}
    addr = sysops.read_command(["ip", "-o", "addr", "show"])
    for line in addr.stdout.splitlines():
        parts = line.split()
        if len(parts) < 4:
            continue
        name = parts[1]
        entry = result.setdefault(name, InterfaceInfo(name=name))
        if parts[2] in {"inet", "inet6"}:
            entry.addresses.append(parts[3])

    link = sysops.read_command(["ip", "-o", "link", "show"])
    for line in link.stdout.splitlines():
        parts = line.split()
        if len(parts) < 2:
            continue
        name = parts[1].rstrip(":").split("@")[0]
        entry = result.setdefault(name, InterfaceInfo(name=name))
        if "state" in parts:
            entry.state = parts[parts.index("state") + 1]
        if "link/ether" in parts:
            entry.mac = parts[parts.index("link/ether") + 1]

    routes = sysops.read_command(["ip", "route", "show", "default"])
    for line in routes.stdout.splitlines():
        parts = line.split()
        if "via" in parts and "dev" in parts:
            gateway = parts[parts.index("via") + 1]
            device = parts[parts.index("dev") + 1]
            if device in result:
                result[device].gateway = gateway

    for entry in result.values():
        entry.dns = read_dns(sysops, entry.name)
        entry.method = "unbekannt"

    return [entry for name, entry in sorted(result.items()) if name != "lo"]


def read_dns(sysops: SysOps, interface: str) -> list[str]:
    """Liest die DNS-Server per ``resolvectl dns <iface>``.

    Mehrzeilige Ausgabe beachten: IPv6-Fortsetzungszeilen enthalten selbst
    Doppelpunkte -- ein naives ``split(":")`` zerlegt sie falsch.
    """
    if not shutil.which("resolvectl"):
        return []
    result = sysops.read_command(["resolvectl", "dns", interface])
    if not result.ok:
        return []
    servers: list[str] = []
    for line in result.stdout.splitlines():
        text = line.strip()
        if not text:
            continue
        if text.lower().startswith("link"):
            _, _, tail = text.partition(":")
            text = tail.strip()
        for token in text.split():
            try:
                ipaddress.ip_address(token)
            except ValueError:
                continue
            servers.append(token)
    return servers


def validate_static(
    *, address: str, prefix: Any, gateway: str, dns: str
) -> tuple[str, int, str, list[str]]:
    try:
        prefix_length = int(prefix)
    except (TypeError, ValueError) as exc:
        raise ValueError("Die Praefixlaenge muss eine Zahl sein (z. B. 24).") from exc
    if not 1 <= prefix_length <= 32:
        raise ValueError("Die Praefixlaenge muss zwischen 1 und 32 liegen.")
    try:
        interface_ip = ipaddress.ip_interface(f"{address.strip()}/{prefix_length}")
    except ValueError as exc:
        raise ValueError(f"{address!r} ist keine gueltige IPv4-Adresse.") from exc

    gateway_clean = (gateway or "").strip()
    if gateway_clean:
        try:
            gateway_ip = ipaddress.ip_address(gateway_clean)
        except ValueError as exc:
            raise ValueError(f"{gateway_clean!r} ist keine gueltige Gateway-Adresse.") from exc
        if gateway_ip not in interface_ip.network:
            raise ValueError(
                f"Das Gateway {gateway_clean} liegt nicht im Netz {interface_ip.network}. "
                "So waere der Host nach der Aenderung nicht mehr erreichbar."
            )

    servers: list[str] = []
    for token in re.split(r"[\s,;]+", dns or ""):
        if not token:
            continue
        try:
            ipaddress.ip_address(token)
        except ValueError as exc:
            raise ValueError(f"{token!r} ist keine gueltige DNS-Adresse.") from exc
        servers.append(token)

    return str(interface_ip.ip), prefix_length, gateway_clean, servers


def render_netplan(
    *,
    interface: str,
    dhcp: bool,
    address: str = "",
    prefix: int = 24,
    gateway: str = "",
    dns: list[str] | None = None,
) -> str:
    """Erzeugt eine Override-Datei fuer genau **ein** Interface.

    Nur der Block des betroffenen Interfaces steht darin. Ein vollstaendiger
    Neuaufbau der Netzwerkkonfiguration wuerde andere, unveraenderte Interfaces
    zuruecksetzen -- ein realer Fehler des Vorgaengerprojekts.
    """
    lines = [
        "# Von TESM verwaltet. Diese Datei enthaelt ausschliesslich das unten",
        "# genannte Interface; alle anderen bleiben bei ihrer",
        "# Distributionskonfiguration.",
        "network:",
        "  version: 2",
        "  ethernets:",
        f"    {interface}:",
    ]
    if dhcp:
        lines.append("      dhcp4: true")
    else:
        lines.append("      dhcp4: false")
        lines.append(f"      addresses: [{address}/{prefix}]")
        if gateway:
            lines.append("      routes:")
            lines.append(f"        - to: default")
            lines.append(f"          via: {gateway}")
        if dns:
            lines.append("      nameservers:")
            lines.append(f"        addresses: [{', '.join(dns)}]")
    return "\n".join(lines) + "\n"
