"""Read-only system facts and a safe subprocess helper.

Everything here is strictly read-only: no service is touched, nothing is
written to the server. On non-Linux dev machines functions degrade gracefully
and return None / empty values instead of raising.
"""

from __future__ import annotations

import ipaddress
import os
import platform
import shutil
import socket
import subprocess
from typing import Any

from logger import get_logger

log = get_logger("discovery.system")

_CMD_TIMEOUT = 10


def run_readonly(cmd: list[str]) -> str | None:
    """Run a read-only command and return stdout, or None on any failure.

    Never raises. Used for inspection commands only (systemctl list, ss, etc.).
    Destructive commands must never be passed here.
    """
    if not shutil.which(cmd[0]):
        return None
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=_CMD_TIMEOUT,
            check=False,
        )
        if result.returncode != 0:
            return None
        return result.stdout
    except (subprocess.SubprocessError, OSError) as exc:
        log.debug("command failed %s: %s", cmd, exc)
        return None


def os_info() -> dict[str, Any]:
    """Operating system name/version, reading /etc/os-release when present."""
    info: dict[str, Any] = {
        "system": platform.system(),
        "release": platform.release(),
        "pretty_name": platform.platform(),
    }
    try:
        with open("/etc/os-release", "r", encoding="utf-8") as handle:
            fields = {}
            for line in handle:
                if "=" in line:
                    key, _, value = line.strip().partition("=")
                    fields[key] = value.strip('"')
        if "PRETTY_NAME" in fields:
            info["pretty_name"] = fields["PRETTY_NAME"]
        info["distro_id"] = fields.get("ID")
        info["distro_version"] = fields.get("VERSION_ID")
    except OSError:
        pass
    return info


def kernel() -> str:
    return platform.release()


def architecture() -> str:
    return platform.machine()


def cpu_count() -> int:
    return os.cpu_count() or 0


def memory_total_bytes() -> int | None:
    """Total RAM in bytes from /proc/meminfo (Linux), else None."""
    try:
        with open("/proc/meminfo", "r", encoding="utf-8") as handle:
            for line in handle:
                if line.startswith("MemTotal:"):
                    kib = int(line.split()[1])
                    return kib * 1024
    except (OSError, ValueError):
        return None
    return None


def memory_usage() -> dict[str, int]:
    """Total/available memory in bytes (best effort)."""
    usage: dict[str, int] = {}
    try:
        with open("/proc/meminfo", "r", encoding="utf-8") as handle:
            values = {}
            for line in handle:
                key, _, rest = line.partition(":")
                values[key.strip()] = int(rest.split()[0]) * 1024
        if "MemTotal" in values:
            usage["total"] = values["MemTotal"]
        if "MemAvailable" in values:
            usage["available"] = values["MemAvailable"]
    except (OSError, ValueError, IndexError):
        pass
    return usage


def disk_total_bytes(path: str = "/") -> int | None:
    try:
        return shutil.disk_usage(path).total
    except OSError:
        return None


def disk_usage(path: str = "/") -> dict[str, int]:
    try:
        usage = shutil.disk_usage(path)
        return {"total": usage.total, "used": usage.used, "free": usage.free}
    except OSError:
        return {}


def load_average() -> list[float]:
    try:
        return list(os.getloadavg())
    except (OSError, AttributeError):  # not available on all platforms
        return []


def hostname() -> str:
    return socket.gethostname()


def uptime_seconds() -> float | None:
    try:
        with open("/proc/uptime", "r", encoding="utf-8") as handle:
            return float(handle.readline().split()[0])
    except (OSError, ValueError, IndexError):
        return None


def _is_private_ipv4(addr: str) -> bool:
    """True only for RFC1918, loopback and link-local IPv4 addresses.

    Public addresses must never be labelled private, so we classify with the
    stdlib ipaddress module rather than naive prefix checks.
    """
    try:
        ip = ipaddress.ip_address(addr)
    except ValueError:
        return False
    return ip.version == 4 and (ip.is_private or ip.is_loopback or ip.is_link_local)


def private_ips() -> list[str]:
    """Best-effort list of local *private* IPv4 addresses (RFC1918 et al.)."""
    ips: set[str] = set()
    try:
        for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
            ips.add(info[4][0])
    except socket.gaierror:
        pass
    # Also probe the outbound interface without sending traffic.
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.connect(("10.255.255.255", 1))
        ips.add(sock.getsockname()[0])
        sock.close()
    except OSError:
        pass
    # Only report genuinely private ranges; never label a public IP as private.
    return sorted(ip for ip in ips if _is_private_ipv4(ip))


def public_ip() -> str | None:
    """Placeholder for v1. The control plane already sees the source IP, and
    discovery must stay read-only/local, so we do not call an external echo
    service here. TODO(prod): resolve via panel-side observation or opt-in."""
    return None


def network_interfaces() -> list[dict[str, Any]]:
    """Parse `ip -o addr` output when available (read-only)."""
    output = run_readonly(["ip", "-o", "addr"])
    interfaces: list[dict[str, Any]] = []
    if not output:
        return interfaces
    for line in output.splitlines():
        parts = line.split()
        if len(parts) >= 4:
            interfaces.append({"interface": parts[1], "family": parts[2], "address": parts[3]})
    return interfaces


def open_ports() -> list[dict[str, Any]]:
    """Listening TCP sockets via `ss -tlnH` (read-only). Empty if unavailable."""
    output = run_readonly(["ss", "-tlnH"])
    ports: list[dict[str, Any]] = []
    if not output:
        return ports
    for line in output.splitlines():
        fields = line.split()
        if len(fields) >= 4:
            local = fields[3]
            port = local.rsplit(":", 1)[-1]
            ports.append({"local": local, "port": port})
    return ports
