"""System information endpoints — network, subsystems, dashboard binding."""

from __future__ import annotations

import asyncio
import os
import platform
import shutil
import socket
import sys
from pathlib import Path
from typing import Any

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

from Project.Sanad.config import (
    AUDIO_RECORDINGS_DIR,
    BASE_DIR,
    DASHBOARD_HOST,
    DASHBOARD_INTERFACE,
    DASHBOARD_PORT,
    DATA_DIR,
    DDS_NETWORK_INTERFACE,
    LOGS_DIR,
    list_network_interfaces,
)
from Project.Sanad.core.logger import get_logger

log = get_logger("system_route")

router = APIRouter()


def _runtime_bind() -> tuple[str, int]:
    """The host/port the server is ACTUALLY bound to.

    main.py launches `uvicorn.run(_app, host=args.host, port=args.port)` with
    the CLI --host/--port (start_sanad.sh passes `--port $PORT`, default 8001),
    which can differ from the import-time DASHBOARD_HOST/DASHBOARD_PORT config
    defaults (port 8000). Reading the live argv reports the real URL instead of
    a stale config value. Falls back to the config constants when an arg is
    absent (e.g. argparse default in effect)."""
    host = DASHBOARD_HOST
    port = DASHBOARD_PORT
    argv = sys.argv
    for i, tok in enumerate(argv):
        if tok == "--host" and i + 1 < len(argv):
            host = argv[i + 1]
        elif tok.startswith("--host="):
            host = tok.split("=", 1)[1]
        elif tok == "--port" and i + 1 < len(argv):
            try:
                port = int(argv[i + 1])
            except (TypeError, ValueError):
                pass
        elif tok.startswith("--port="):
            try:
                port = int(tok.split("=", 1)[1])
            except (TypeError, ValueError):
                pass
    return host, port


def _safe_status(component, name: str) -> dict[str, Any]:
    if component is None:
        return {"available": False}
    try:
        if hasattr(component, "status") and callable(component.status):
            s = component.status()
            if not isinstance(s, dict):
                s = {"raw": str(s)}
            s.setdefault("available", True)
            return s
        return {"available": True}
    except Exception as exc:
        log.warning("status() failed for %s: %s", name, exc)
        return {"available": True, "error": str(exc)}


@router.get("/info")
async def system_info():
    """One-shot system snapshot for the dashboard system panel."""
    def _do():
        # Subsystems
        try:
            from Project.Sanad.main import SUBSYSTEMS
        except Exception:
            SUBSYSTEMS = {}

        subsystem_list = []
        for name in sorted(SUBSYSTEMS):
            comp = SUBSYSTEMS[name]
            entry = {
                "name": name,
                "connected": comp is not None,
            }
            if comp is not None and hasattr(comp, "status") and callable(comp.status):
                try:
                    s = comp.status()
                    if isinstance(s, dict):
                        entry["status"] = s
                except Exception as exc:
                    entry["status_error"] = str(exc)
            subsystem_list.append(entry)

        connected_count = sum(1 for s in subsystem_list if s["connected"])

        # Audio device current selection (best-effort)
        audio_info = {}
        try:
            from Project.Sanad.voice import audio_devices as ad
            audio_info = {
                "pactl_available": ad.pactl_available(),
                "current": ad.current_selection(),
                "detected_profile_ids": [
                    d["profile"]["id"] for d in ad.detect_plugged_profiles()
                ] if ad.pactl_available() else [],
            }
        except Exception as exc:
            audio_info = {"error": str(exc)}

        # Network interfaces
        try:
            interfaces = list_network_interfaces()
        except Exception:
            interfaces = []

        # Determine the URL the dashboard is reachable at — use the ACTUAL
        # runtime bind args (argv), not the import-time config defaults.
        bound_host, bound_port = _runtime_bind()
        if bound_host == "0.0.0.0":
            # Try to find the wlan0 IP for display purposes
            up_ifaces = [i for i in interfaces if i["is_up"] and i["ip"] and not i["ip"].startswith("127.")]
            display_host = up_ifaces[0]["ip"] if up_ifaces else bound_host
        else:
            display_host = bound_host

        return {
            "host": {
                "hostname": socket.gethostname(),
                "platform": platform.platform(),
                "python": sys.version.split()[0],
                "executable": sys.executable,
                "base_dir": str(BASE_DIR),
                "pid": os.getpid(),
            },
            "dashboard": {
                "interface": DASHBOARD_INTERFACE,
                "bound_host": bound_host,
                "display_host": display_host,
                "port": bound_port,
                "url": f"http://{display_host}:{bound_port}",
            },
            "dds": {
                "interface": DDS_NETWORK_INTERFACE,
            },
            "network": {
                "interfaces": interfaces,
            },
            "subsystems": {
                "total": len(subsystem_list),
                "connected": connected_count,
                "disconnected": len(subsystem_list) - connected_count,
                "list": subsystem_list,
            },
            "audio": audio_info,
        }

    return await asyncio.to_thread(_do)


# ───────────────────── storage tracking + cleanup ─────────────────────
# Categories surfaced in the Settings → Storage panel. `cleanable` ones get a
# Clean button + are included in "Clean all"; the rest (faces/motions/zones)
# are shown for tracking only — they're operational assets (enrollments,
# motion configs) managed in their own tabs, not disposable clutter.
_STORAGE_CATS = [
    ("recordings", "Conversation recordings", DATA_DIR / "recordings", True),
    ("records",    "Named records (Typed Replay)", AUDIO_RECORDINGS_DIR, True),
    ("logs",       "Logs", LOGS_DIR, True),
    ("faces",      "Enrolled faces", DATA_DIR / "faces", False),
    ("motions",    "Motion replays + config", DATA_DIR / "motions", False),
    ("photos",     "Photos", DATA_DIR / "photos", False),
    ("zones",      "Vision zones", DATA_DIR / "zones", False),
]
_CLEANABLE = {k for k, _l, _p, c in _STORAGE_CATS if c}


def _dir_stats(path: Path) -> tuple[int, int]:
    """(total_bytes, file_count) of a dir tree. Missing dir → (0, 0)."""
    total, n = 0, 0
    try:
        for root, _dirs, files in os.walk(path):
            for f in files:
                try:
                    total += os.path.getsize(os.path.join(root, f))
                    n += 1
                except OSError:
                    pass
    except Exception:
        pass
    return total, n


def _human(b: float) -> str:
    f = float(b)
    for u in ("B", "KB", "MB", "GB", "TB"):
        if f < 1024 or u == "TB":
            return f"{f:.0f} {u}" if u == "B" else f"{f:.1f} {u}"
        f /= 1024
    return f"{f:.1f} TB"


@router.get("/storage")
async def storage_usage():
    """Per-category data/log sizes + disk free, for the Storage panel."""
    def _do():
        cats = []
        for key, label, path, cleanable in _STORAGE_CATS:
            size, files = _dir_stats(Path(path))
            cats.append({
                "key": key, "label": label, "path": str(path),
                "size_bytes": size, "size_human": _human(size),
                "files": files, "cleanable": cleanable,
            })
        data_b, _ = _dir_stats(DATA_DIR)
        logs_b, _ = _dir_stats(LOGS_DIR)
        try:
            du = shutil.disk_usage(str(BASE_DIR))
            disk = {
                "free_human": _human(du.free), "total_human": _human(du.total),
                "used_pct": round(100.0 * (du.total - du.free) / du.total, 1),
            }
        except Exception:
            disk = {}
        return {
            "categories": cats,
            "data_bytes": data_b, "data_human": _human(data_b),
            "logs_human": _human(logs_b),
            "total_human": _human(data_b + logs_b),
            "disk": disk,
        }
    return await asyncio.to_thread(_do)


class _CleanReq(BaseModel):
    target: str   # recordings | records | logs | all


def _clean_recordings() -> tuple[int, int]:
    d = DATA_DIR / "recordings"
    freed, n = 0, 0
    for f in list(d.glob("*.wav")) + [d / "index.json"]:
        if f.is_file():
            try:
                freed += f.stat().st_size
                f.unlink()
                n += 1
            except OSError:
                pass
    return n, freed


def _clean_records() -> tuple[int, int]:
    d = AUDIO_RECORDINGS_DIR
    freed, n = 0, 0
    for f in list(d.glob("*.wav")) + [d / "records.json"]:
        if f.is_file():
            try:
                freed += f.stat().st_size
                f.unlink()
                n += 1
            except OSError:
                pass
    return n, freed


def _clean_logs() -> tuple[int, int]:
    # Truncate (not delete) — active loggers hold append-mode handles, so
    # truncating to 0 clears content cleanly without losing the fd.
    freed, n = 0, 0
    for f in Path(LOGS_DIR).glob("*.log"):
        try:
            freed += f.stat().st_size
            open(f, "w").close()
            n += 1
        except OSError:
            pass
    return n, freed


@router.post("/storage/clean")
async def storage_clean(req: _CleanReq):
    """Clean a disposable category (recordings | records | logs) or 'all'.
    Recordings/records are deleted; logs are truncated. Assets (faces, motions,
    zones) are never touched here."""
    t = (req.target or "").strip().lower()
    if t != "all" and t not in _CLEANABLE:
        raise HTTPException(400, f"target must be 'all' or one of {sorted(_CLEANABLE)}")

    def _do():
        targets = ["recordings", "records", "logs"] if t == "all" else [t]
        fns = {"recordings": _clean_recordings, "records": _clean_records,
               "logs": _clean_logs}
        result, total = {}, 0
        for tg in targets:
            n, freed = fns[tg]()
            result[tg] = {"items": n, "freed_bytes": freed, "freed_human": _human(freed)}
            total += freed
        log.info("storage clean %s → freed %s", targets, _human(total))
        return {"ok": True, "cleaned": targets,
                "total_freed_bytes": total, "total_freed_human": _human(total),
                "result": result}
    return await asyncio.to_thread(_do)
