"""
Sovereign Orchestrator - FastAPI Application

Main entrypoint that wires up all API routes, middleware, and static file
serving.  Run with:

    uvicorn app.main:app --host 0.0.0.0 --port 8888
"""

from __future__ import annotations

import asyncio
import logging
from pathlib import Path
from typing import Any

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles

from app.config import settings
from app.iso_builder import (
    build_iso,
    get_all_builds,
    get_build,
    get_build_iso_path,
    get_active_builds,
    get_latest_build,
)
from app.models import (
    BuildState,
    BuildStatus,
    DeployConfig,
    DeployResult,
    ISOConfig,
    ISOInfo,
    NodeInfo,
    SystemStatus,
    VMStatus,
)
from app.proxmox_client import ProxmoxError, proxmox

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s  %(levelname)-8s  %(name)s  %(message)s",
)
logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# App setup
# ---------------------------------------------------------------------------

app = FastAPI(
    title="Sovereign Orchestrator",
    description=(
        "API for building Proxmox auto-install ISOs and deploying "
        "VMs on a Proxmox VE cluster."
    ),
    version="0.1.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ---------------------------------------------------------------------------
# Static files  – mount the frontend at / (must be last, after API routes)
# ---------------------------------------------------------------------------

_static_dir = Path(__file__).resolve().parent.parent / "static"
_static_dir.mkdir(parents=True, exist_ok=True)


# ---------------------------------------------------------------------------
# Health / status
# ---------------------------------------------------------------------------

@app.get("/api/status", response_model=SystemStatus, tags=["Status"])
async def get_system_status() -> SystemStatus:
    """Return an aggregate system status dashboard.

    Includes Proxmox node info, test VM state, available ISOs, and the
    active build queue.  Proxmox errors are captured gracefully so the
    endpoint always returns *something*.
    """
    status = SystemStatus()

    try:
        # Nodes
        raw_nodes = await proxmox.get_nodes()
        status.nodes = [
            NodeInfo(
                node=n.get("node", "unknown"),
                status=n.get("status", "unknown"),
                cpu=n.get("cpu"),
                maxcpu=n.get("maxcpu"),
                mem=n.get("mem"),
                maxmem=n.get("maxmem"),
                uptime=n.get("uptime"),
            )
            for n in raw_nodes
        ]

        # Test VM
        try:
            vm_data = await proxmox.get_vm_status()
            status.test_vm = VMStatus(
                vmid=settings.default_vmid,
                name=vm_data.get("name"),
                status=vm_data.get("status", "unknown"),
                node=settings.default_node,
                cpus=vm_data.get("cpus"),
                maxmem=vm_data.get("maxmem"),
                maxdisk=vm_data.get("maxdisk"),
                uptime=vm_data.get("uptime"),
                pid=vm_data.get("pid"),
            )
        except ProxmoxError:
            logger.debug("Test VM %d not found (may not exist yet)", settings.default_vmid)

        # ISOs
        try:
            raw_isos = await proxmox.get_isos()
            status.available_isos = [
                ISOInfo(
                    volid=iso.get("volid", ""),
                    filename=iso.get("volid", "").split("/")[-1],
                    size=iso.get("size"),
                )
                for iso in raw_isos
            ]
        except ProxmoxError:
            logger.debug("Could not list ISOs")

        status.proxmox_connected = True

    except ProxmoxError as exc:
        status.error = str(exc)
        logger.warning("Proxmox status check failed: %s", exc)
    except Exception as exc:
        status.error = f"Unexpected error: {exc}"
        logger.exception("Status check failed")

    # Build queue (always available even if Proxmox is down)
    status.active_builds = get_active_builds()

    return status


# ---------------------------------------------------------------------------
# ISO generation
# ---------------------------------------------------------------------------

@app.post("/api/generate-iso", response_model=BuildStatus, tags=["ISO"])
async def generate_iso(config: ISOConfig) -> BuildStatus:
    """Generate a custom Proxmox auto-install ISO.

    Accepts the installation configuration, writes an ``answers.toml``,
    and asynchronously runs ``proxmox-auto-install-assistant prepare-iso``.
    Returns the build status immediately; poll ``/api/builds/{id}`` for
    progress.
    """
    logger.info("ISO generation requested for %s", config.fqdn)
    build = await build_iso(config)
    return build


# ---------------------------------------------------------------------------
# Builds
# ---------------------------------------------------------------------------

@app.get("/api/builds", response_model=list[BuildStatus], tags=["Builds"])
async def list_builds() -> list[BuildStatus]:
    """List all ISO builds, newest first."""
    return get_all_builds()


@app.get("/api/builds/{build_id}", response_model=BuildStatus, tags=["Builds"])
async def get_build_detail(build_id: str) -> BuildStatus:
    """Get detailed status and logs for a specific build."""
    build = get_build(build_id)
    if build is None:
        raise HTTPException(status_code=404, detail=f"Build {build_id} not found")
    return build


# ---------------------------------------------------------------------------
# Deploy
# ---------------------------------------------------------------------------

@app.post("/api/deploy", response_model=DeployResult, tags=["Deploy"])
async def deploy_vm(config: DeployConfig) -> DeployResult:
    """Deploy a VM using the custom auto-install ISO.

    This will:
    1. Resolve which ISO to use (from *build_id* or the latest build)
    2. Upload the ISO to Proxmox storage if needed
    3. Stop and destroy the existing VM (if any)
    4. Create a new VM with the ISO mounted
    5. Start the VM

    The entire flow runs synchronously so the caller gets a final status.
    """
    node = config.node
    vmid = config.vmid

    # --- Resolve the ISO to deploy ---
    build_id = config.build_id
    if build_id is None:
        latest = get_latest_build()
        if latest is None or latest.state != BuildState.COMPLETED:
            raise HTTPException(
                status_code=400,
                detail="No completed build available. Run /api/generate-iso first.",
            )
        build_id = latest.id

    build = get_build(build_id)
    if build is None or build.state != BuildState.COMPLETED:
        raise HTTPException(
            status_code=400,
            detail=f"Build {build_id} is not in a completed state.",
        )

    iso_path = get_build_iso_path(build_id)
    if iso_path is None or not iso_path.exists():
        raise HTTPException(
            status_code=500,
            detail=f"ISO file for build {build_id} not found on disk.",
        )

    logger.info(
        "Deploying VM %d on %s from build %s (%s)",
        vmid, node, build_id, iso_path.name,
    )

    try:
        # --- Upload ISO to Proxmox ---
        iso_volid = f"{config.iso_storage}:iso/{iso_path.name}"
        try:
            existing_isos = await proxmox.get_isos(node, config.iso_storage)
            already_uploaded = any(
                i.get("volid") == iso_volid for i in existing_isos
            )
        except ProxmoxError:
            already_uploaded = False

        if not already_uploaded:
            logger.info("Uploading ISO %s to Proxmox storage", iso_path.name)
            upload_upid = await proxmox.upload_iso(
                str(iso_path), node, config.iso_storage
            )
            await proxmox.wait_for_task(node, upload_upid, max_wait=300)
            logger.info("ISO upload completed")

        # --- Tear down existing VM ---
        try:
            vm_status = await proxmox.get_vm_status(node, vmid)
            current_state = vm_status.get("status", "")

            if current_state == "running":
                logger.info("Stopping existing VM %d", vmid)
                stop_upid = await proxmox.stop_vm(node, vmid)
                await proxmox.wait_for_task(node, stop_upid, max_wait=60)
                # Brief pause for lock release
                await asyncio.sleep(3)

            logger.info("Destroying existing VM %d", vmid)
            destroy_upid = await proxmox.destroy_vm(node, vmid)
            await proxmox.wait_for_task(node, destroy_upid, max_wait=60)
            await asyncio.sleep(2)

        except ProxmoxError as exc:
            if exc.status_code == 500 or exc.status_code == 404:
                logger.info("VM %d does not exist, skipping teardown", vmid)
            else:
                raise

        # --- Create new VM ---
        vm_config: dict[str, Any] = {
            "vmid": vmid,
            "name": build.iso_config.fqdn.split(".")[0] if build.iso_config else f"vm-{vmid}",
            "cores": config.cores,
            "memory": config.memory,
            "ostype": "l26",
            "sata0": f"{config.storage}:{config.disk_size.rstrip('G')}",
            "sata1": f"{config.storage}:1000",
            "sata2": f"{config.storage}:1000",
            "ide2": f"{iso_volid},media=cdrom",
            "boot": f"order=sata0;ide2",
            "net0": f"virtio,bridge={config.bridge}",
            "serial0": "socket",
            "vga": "std",
            "tpmstate0": f"{config.storage}:4",
        }

        create_upid = await proxmox.create_vm(node, vmid, vm_config)
        if isinstance(create_upid, str) and create_upid.startswith("UPID"):
            await proxmox.wait_for_task(node, create_upid, max_wait=30)

        await asyncio.sleep(1)

        # --- Start the VM ---
        start_upid = await proxmox.start_vm(node, vmid)

        return DeployResult(
            vmid=vmid,
            node=node,
            status="started",
            message=f"VM {vmid} created and started with ISO {iso_path.name}",
            task_id=start_upid if isinstance(start_upid, str) else None,
        )

    except ProxmoxError as exc:
        logger.error("Deploy failed: %s", exc)
        raise HTTPException(status_code=502, detail=str(exc))
    except Exception as exc:
        logger.exception("Unexpected deploy error")
        raise HTTPException(status_code=500, detail=str(exc))


# ---------------------------------------------------------------------------
# VM management
# ---------------------------------------------------------------------------

@app.get("/api/vm/{vmid}/status", response_model=VMStatus, tags=["VM"])
async def vm_status(vmid: int, node: str | None = None) -> VMStatus:
    """Get the current status of a VM."""
    node = node or settings.default_node
    try:
        data = await proxmox.get_vm_status(node, vmid)
        return VMStatus(
            vmid=vmid,
            name=data.get("name"),
            status=data.get("status", "unknown"),
            node=node,
            cpus=data.get("cpus"),
            maxmem=data.get("maxmem"),
            maxdisk=data.get("maxdisk"),
            uptime=data.get("uptime"),
            pid=data.get("pid"),
        )
    except ProxmoxError as exc:
        raise HTTPException(status_code=502, detail=str(exc))


@app.post("/api/vm/{vmid}/start", tags=["VM"])
async def vm_start(vmid: int, node: str | None = None) -> dict[str, str]:
    """Start a VM."""
    node = node or settings.default_node
    try:
        task_id = await proxmox.start_vm(node, vmid)
        return {"status": "starting", "vmid": str(vmid), "task_id": str(task_id)}
    except ProxmoxError as exc:
        raise HTTPException(status_code=502, detail=str(exc))


@app.post("/api/vm/{vmid}/stop", tags=["VM"])
async def vm_stop(vmid: int, node: str | None = None) -> dict[str, str]:
    """Stop a VM."""
    node = node or settings.default_node
    try:
        task_id = await proxmox.stop_vm(node, vmid)
        return {"status": "stopping", "vmid": str(vmid), "task_id": str(task_id)}
    except ProxmoxError as exc:
        raise HTTPException(status_code=502, detail=str(exc))


# ---------------------------------------------------------------------------
# ISOs
# ---------------------------------------------------------------------------

@app.get("/api/isos", response_model=list[ISOInfo], tags=["ISO"])
async def list_isos(
    node: str | None = None, storage: str | None = None
) -> list[ISOInfo]:
    """List available ISO images on Proxmox storage."""
    try:
        raw = await proxmox.get_isos(node, storage)
        return [
            ISOInfo(
                volid=iso.get("volid", ""),
                filename=iso.get("volid", "").split("/")[-1],
                size=iso.get("size"),
            )
            for iso in raw
        ]
    except ProxmoxError as exc:
        raise HTTPException(status_code=502, detail=str(exc))


# ---------------------------------------------------------------------------
# Static files — mounted last so API routes take precedence
# ---------------------------------------------------------------------------

app.mount("/", StaticFiles(directory=str(_static_dir), html=True), name="static")
