"""
Sovereign Orchestrator - ISO Builder

Generates answers.toml files and invokes `proxmox-auto-install-assistant`
to produce custom auto-installer ISOs.  Build state is tracked in-memory
and exposed through the API.
"""

from __future__ import annotations

import asyncio
import logging
import os
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

import toml

from app.config import settings
from app.models import BuildState, BuildStatus, ISOConfig

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# In-memory build registry
# ---------------------------------------------------------------------------

_builds: dict[str, BuildStatus] = {}


def get_all_builds() -> list[BuildStatus]:
    """Return all builds, newest first."""
    return sorted(
        _builds.values(), key=lambda b: b.created_at, reverse=True
    )


def get_build(build_id: str) -> Optional[BuildStatus]:
    """Look up a single build by ID."""
    return _builds.get(build_id)


def get_latest_build() -> Optional[BuildStatus]:
    """Return the most recent build, or None."""
    builds = get_all_builds()
    return builds[0] if builds else None


def get_active_builds() -> list[BuildStatus]:
    """Return builds that are still in progress."""
    return [
        b for b in _builds.values()
        if b.state in (BuildState.PENDING, BuildState.BUILDING, BuildState.UPLOADING)
    ]


# ---------------------------------------------------------------------------
# answers.toml generation
# ---------------------------------------------------------------------------

def generate_answers_toml(config: ISOConfig) -> str:
    """Render an ``answers.toml`` string from the given configuration.

    The output format matches the Proxmox auto-installer specification
    with [global], [network], and [disk-setup] sections.
    """
    doc: dict = {
        "global": {
            "keyboard": config.keyboard,
            "country": config.country,
            "timezone": config.timezone,
            "mailto": config.mailto,
            "root-password": config.root_password,
            "fqdn": config.fqdn,
            "reboot-on-error": config.reboot_on_error,
        },
        "network": {
            "source": config.network_source.value,
        },
        "disk-setup": {
            "filesystem": config.filesystem.value,
            "disk-list": config.disk_list,
        },
    }

    if config.root_ssh_keys:
        doc["global"]["root-ssh-keys"] = config.root_ssh_keys

    return toml.dumps(doc)


# ---------------------------------------------------------------------------
# Base ISO discovery
# ---------------------------------------------------------------------------

_BASE_ISO_SEARCH_PATHS = [
    Path(settings.iso_storage_path),
    Path("/var/lib/vz/template/iso"),
    Path("/usr/share/proxmox-installer"),
    Path.home() / "isos",
]


def find_base_iso() -> Optional[Path]:
    """Locate the first Proxmox VE ISO in well-known directories.

    Searches for files matching ``proxmox-ve_*.iso``.
    """
    for search_dir in _BASE_ISO_SEARCH_PATHS:
        if not search_dir.is_dir():
            continue
        candidates = sorted(
            search_dir.glob("proxmox-ve_*.iso"), reverse=True
        )
        if candidates:
            logger.info("Found base ISO: %s", candidates[0])
            return candidates[0]
    logger.warning("No base Proxmox VE ISO found in search paths")
    return None


# ---------------------------------------------------------------------------
# ISO build pipeline
# ---------------------------------------------------------------------------

def _ensure_build_dir() -> Path:
    """Create and return the build output directory."""
    build_dir = Path(settings.build_output_path)
    build_dir.mkdir(parents=True, exist_ok=True)
    return build_dir


async def build_iso(config: ISOConfig) -> BuildStatus:
    """Kick off an asynchronous ISO build and return the BuildStatus.

    The actual subprocess work runs in the background so the API can
    return immediately.
    """
    build = BuildStatus(iso_config=config)
    _builds[build.id] = build
    build.log(f"Build {build.id} created")

    # Launch the background task
    asyncio.create_task(_run_build(build))

    return build


async def _run_build(build: BuildStatus) -> None:
    """Execute the full ISO build pipeline."""
    try:
        build.state = BuildState.BUILDING
        build.log("Build started")

        build_dir = _ensure_build_dir()
        work_dir = build_dir / build.id
        work_dir.mkdir(parents=True, exist_ok=True)

        # 1. Write answers.toml
        answers_path = work_dir / "answers.toml"
        answers_content = generate_answers_toml(build.iso_config)
        answers_path.write_text(answers_content)
        build.log(f"answers.toml written to {answers_path}")
        logger.debug("answers.toml content:\n%s", answers_content)

        # 2. Locate base ISO
        base_iso = find_base_iso()
        if base_iso is None:
            raise FileNotFoundError(
                "No base Proxmox VE ISO found. Place a proxmox-ve_*.iso in "
                f"{settings.iso_storage_path}"
            )
        build.log(f"Using base ISO: {base_iso}")

        # 3. Determine output filename
        fqdn_slug = build.iso_config.fqdn.replace(".", "-")
        timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
        output_filename = f"pve-autoinstall-{fqdn_slug}-{timestamp}.iso"
        output_path = work_dir / output_filename

        # 4. Copy the base ISO to the output path first (prepare-iso works
        #    on a copy so we don't mutate the original)
        build.log(f"Copying base ISO to {output_path}")
        await asyncio.to_thread(shutil.copy2, str(base_iso), str(output_path))
        build.log("Base ISO copied")

        # 5. Run proxmox-auto-install-assistant prepare-iso
        cmd = [
            "proxmox-auto-install-assistant",
            "prepare-iso",
            str(output_path),
            "--fetch-from", "iso",
            "--answer-file", str(answers_path),
        ]
        build.log(f"Running: {' '.join(cmd)}")

        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.STDOUT,
            cwd=str(work_dir),
        )

        # Stream output into build logs
        assert proc.stdout is not None
        while True:
            line = await proc.stdout.readline()
            if not line:
                break
            decoded = line.decode("utf-8", errors="replace").rstrip()
            build.log(decoded)
            logger.debug("[build %s] %s", build.id, decoded)

        return_code = await proc.wait()

        if return_code != 0:
            raise RuntimeError(
                f"proxmox-auto-install-assistant exited with code {return_code}"
            )

        prepared_filename = output_filename.replace(".iso", "-auto-from-iso.iso")
        build.iso_filename = prepared_filename
        build.state = BuildState.COMPLETED
        build.log(f"Build completed: {prepared_filename}")
        logger.info("Build %s completed: %s", build.id, work_dir / prepared_filename)

    except Exception as exc:
        build.state = BuildState.FAILED
        build.error = str(exc)
        build.log(f"Build failed: {exc}")
        logger.exception("Build %s failed", build.id)


# ---------------------------------------------------------------------------
# Utility: get the output ISO path for a build
# ---------------------------------------------------------------------------

def get_build_iso_path(build_id: str) -> Optional[Path]:
    """Return the absolute path to the output ISO for a completed build."""
    build = _builds.get(build_id)
    if not build or not build.iso_filename:
        return None
    return Path(settings.build_output_path) / build.id / build.iso_filename
