"""Docker topology — read-only daemon view via socket-proxy.

The cockpit never touches /var/run/docker.sock directly. It talks to a
tecnativa/docker-socket-proxy sidecar over the backend network. The proxy is
configured GET-only (CONTAINERS, NETWORKS, PING) so a web-app compromise
can't drive the daemon. Returned data is normalized for templates: a flat
list of containers and a network-grouped topology.
"""

from __future__ import annotations

import os
from typing import Any, Dict, List, Optional

import httpx

from psyc import log


_log = log.get(__name__)

PROXY_URL = os.environ.get("PSYC_DOCKER_PROXY", "http://docker-socket-proxy:2375")
HTTP_TIMEOUT = 5.0


class DockerProxyError(RuntimeError):
    """Proxy unreachable or returned an error."""


def _get(path: str) -> Any:
    try:
        with httpx.Client(timeout=HTTP_TIMEOUT) as client:
            resp = client.get(f"{PROXY_URL}{path}")
            resp.raise_for_status()
            return resp.json()
    except httpx.HTTPError as exc:
        _log.warning("docker_view.proxy.error", path=path, error=str(exc))
        raise DockerProxyError(str(exc)) from exc


def list_containers() -> List[Dict[str, Any]]:
    raw = _get("/containers/json?all=1")
    out: List[Dict[str, Any]] = []
    for c in raw:
        names = c.get("Names") or []
        name = (names[0] if names else "?").lstrip("/")
        nets = []
        for net_name, net_data in (c.get("NetworkSettings", {}).get("Networks", {}) or {}).items():
            nets.append({
                "name": net_name,
                "ip": net_data.get("IPAddress") or "",
                "mac": net_data.get("MacAddress") or "",
                "gateway": net_data.get("Gateway") or "",
            })
        ports = []
        published_ports = []  # outer ports only — the ones reachable from the host
        for p in c.get("Ports") or []:
            inner = p.get("PrivatePort")
            outer = p.get("PublicPort")
            proto = p.get("Type") or "tcp"
            if outer:
                ports.append(f"{p.get('IP', '0.0.0.0')}:{outer}->{inner}/{proto}")
                published_ports.append(f"{outer}/{proto}")
            elif inner:
                ports.append(f"{inner}/{proto}")
        # Dedupe published_ports while keeping order.
        seen: set = set()
        published_ports = [x for x in published_ports if not (x in seen or seen.add(x))]
        out.append({
            "id": (c.get("Id") or "")[:12],
            "name": name,
            "image": c.get("Image", ""),
            "state": c.get("State", ""),
            "status": c.get("Status", ""),
            "networks": nets,
            "ports": ports,
            "published_ports": published_ports,
        })
    out.sort(key=lambda c: (c["state"] != "running", c["name"]))
    return out


def list_networks() -> List[Dict[str, Any]]:
    raw = _get("/networks")
    out: List[Dict[str, Any]] = []
    for n in raw:
        attached = []
        for cid, info in (n.get("Containers") or {}).items():
            ip = (info.get("IPv4Address") or "").split("/")[0]
            attached.append({
                "id": (cid or "")[:12], "name": info.get("Name", ""),
                "ip": ip, "mac": info.get("MacAddress") or "",
            })
        attached.sort(key=lambda x: x["name"])
        ipam_cfgs = (n.get("IPAM") or {}).get("Config") or []
        subnet = ipam_cfgs[0].get("Subnet") if ipam_cfgs else ""
        gateway = ipam_cfgs[0].get("Gateway") if ipam_cfgs else ""
        out.append({
            "id": (n.get("Id") or "")[:12],
            "name": n.get("Name", ""),
            "driver": n.get("Driver", ""),
            "scope": n.get("Scope", ""),
            "internal": bool(n.get("Internal")),
            "subnet": subnet or "",
            "gateway": gateway or "",
            "containers": attached,
        })
    _DEFAULTS = {"bridge", "host", "none"}
    out.sort(key=lambda n: (n["name"] in _DEFAULTS, n["name"]))
    return out


def host_info() -> Dict[str, Any]:
    """Daemon-side info for the synthetic host node. Best-effort."""
    try:
        info = _get("/info")
    except DockerProxyError:
        return {"name": "docker host", "os": "", "ncpu": None}
    return {
        "name": info.get("Name") or "docker host",
        "os": info.get("OperatingSystem") or info.get("OSType") or "",
        "ncpu": info.get("NCPU"),
        "containers": info.get("Containers"),
        "containers_running": info.get("ContainersRunning"),
    }


def topology() -> Dict[str, Any]:
    """Combined snapshot. Either field may be [] with an 'error' key set."""
    state: Dict[str, Any] = {
        "containers": [], "networks": [], "host": {"name": "docker host"},
        "error": None, "proxy": PROXY_URL,
    }
    try:
        state["containers"] = list_containers()
    except DockerProxyError as exc:
        state["error"] = f"containers: {exc}"
        return state
    try:
        state["networks"] = list_networks()
    except DockerProxyError as exc:
        state["error"] = f"networks: {exc}"
    state["host"] = host_info()
    return state
