"""Device control routes for remote management via the dashboard.

These routes are mounted at ``/control/*`` on the local proxy and allow
authenticated users to manage their device through the tunnel:

- ``POST /control/pull``     -- Pull a model remotely
- ``POST /control/restart``  -- Restart the inference engine
- ``GET  /control/status``   -- Detailed device status

**GPU RESPECT**: These are user-initiated management operations. The dashboard
user explicitly clicks "Pull Model" or "Restart Engine". We never auto-modify
user GPU state.

Auth: requires a valid proxy API key in ``Authorization: Bearer <key>`` header.
"""

from __future__ import annotations

import asyncio
import logging
import time
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/control", tags=["control"])


# ---------------------------------------------------------------------------
# Auth dependency
# ---------------------------------------------------------------------------


async def _require_control_auth(request: Request) -> str:
    """Verify the request has a valid proxy API key.

    Returns the key_id on success; raises 401 otherwise.
    """
    auth_header = request.headers.get("authorization", "")
    if not auth_header.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")

    raw_key = auth_header[7:]
    key_store = getattr(request.app.state, "key_store", None)
    if key_store is None:
        raise HTTPException(status_code=503, detail="Key store not available")

    key_id = key_store.validate_key(raw_key)
    if key_id is None:
        raise HTTPException(status_code=401, detail="Invalid API key")

    return str(key_id)


# ---------------------------------------------------------------------------
# Request / Response models
# ---------------------------------------------------------------------------


class PullRequest(BaseModel):
    """Request body for pulling a model."""

    model: str = Field(..., description="Model name to pull (e.g. 'llama3.1:8b')")


class PullProgress(BaseModel):
    """Progress update during model pull."""

    status: str
    progress_pct: float | None = None
    detail: str | None = None


class RestartResponse(BaseModel):
    """Response for engine restart."""

    status: str
    message: str


class DeviceStatus(BaseModel):
    """Detailed device status."""

    status: str = "ok"
    uptime_seconds: int = 0
    loaded_models: list[str] = Field(default_factory=list)
    active_requests: int = 0
    vram_usage_mb: int | None = None
    vram_total_mb: int | None = None
    hardware_summary: str = ""


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@router.post("/pull")
async def pull_model(
    body: PullRequest,
    request: Request,
    _key_id: str = Depends(_require_control_auth),
) -> StreamingResponse:
    """Pull a model remotely. Returns streaming progress updates.

    The user explicitly requested this from the dashboard. We pull the model
    using the local inference engine API.
    """
    model_name = body.model.strip()
    if not model_name:
        raise HTTPException(status_code=400, detail="Model name cannot be empty")

    async def _stream_pull() -> Any:
        """Stream pull progress as JSON lines."""
        try:
            import httpx

            # Attempt to pull via the local inference engine (engine-agnostic API)
            # Try the standard pull endpoint
            engine_url = _get_engine_url(request)

            async with httpx.AsyncClient(timeout=httpx.Timeout(600.0, connect=10.0)) as client:
                # Start the pull request (streaming)
                async with client.stream(
                    "POST",
                    f"{engine_url}/api/pull",
                    json={"name": model_name, "stream": True},
                ) as resp:
                    if resp.status_code != 200:
                        error_msg = PullProgress(status="error", detail=f"Engine returned {resp.status_code}")
                        yield error_msg.model_dump_json() + "\n"
                        return

                    async for line in resp.aiter_lines():
                        if not line.strip():
                            continue
                        try:
                            import json

                            data = json.loads(line)
                            # Convert engine progress format to our format
                            status = data.get("status", "downloading")
                            total = data.get("total", 0)
                            completed = data.get("completed", 0)
                            pct = (completed / total * 100.0) if total > 0 else None

                            progress = PullProgress(
                                status=status,
                                progress_pct=pct,
                                detail=f"{completed}/{total}" if total > 0 else None,
                            )
                            yield progress.model_dump_json() + "\n"
                        except Exception:
                            # Forward raw line as status
                            yield PullProgress(status="pulling", detail=line).model_dump_json() + "\n"

                # Final success message
                yield PullProgress(status="success", progress_pct=100.0).model_dump_json() + "\n"

        except Exception as exc:
            logger.error("Pull failed for model %s: %s", model_name, exc)
            yield PullProgress(status="error", detail=str(exc)).model_dump_json() + "\n"

    return StreamingResponse(
        _stream_pull(),
        media_type="application/x-ndjson",
        headers={"X-Content-Type-Options": "nosniff"},
    )


@router.post("/restart", response_model=RestartResponse)
async def restart_engine(
    request: Request,
    _key_id: str = Depends(_require_control_auth),
) -> RestartResponse:
    """Restart the local inference engine.

    User-initiated restart from the dashboard.
    """
    try:
        import httpx

        engine_url = _get_engine_url(request)

        # Verify engine is reachable first
        async with httpx.AsyncClient(timeout=10.0) as client:
            try:
                health_resp = await client.get(engine_url)
                if health_resp.status_code != 200:
                    return RestartResponse(
                        status="error",
                        message="Inference engine is not responding",
                    )
            except httpx.ConnectError:
                return RestartResponse(
                    status="error",
                    message="Inference engine is not reachable",
                )

        # Attempt a graceful restart via systemd or direct signal
        restarted = await _restart_engine_service()

        if restarted:
            # Wait for engine to come back up
            engine_up = await _wait_for_engine(engine_url, timeout=30)
            if engine_up:
                return RestartResponse(status="ok", message="Inference engine restarted successfully")
            else:
                return RestartResponse(
                    status="warning",
                    message="Restart initiated but engine has not responded yet",
                )
        else:
            return RestartResponse(
                status="error",
                message="Could not restart inference engine. Manual restart may be needed.",
            )

    except Exception as exc:
        logger.error("Engine restart failed: %s", exc)
        return RestartResponse(status="error", message=f"Restart failed: {exc}")


@router.get("/status", response_model=DeviceStatus)
async def device_status(
    request: Request,
    _key_id: str = Depends(_require_control_auth),
) -> DeviceStatus:
    """Return detailed device status: VRAM, loaded models, uptime, etc."""
    result = DeviceStatus()

    # Get models from dispatcher if available
    dispatcher = getattr(request.app.state, "dispatcher", None)
    if dispatcher is not None:
        try:
            models_info = dispatcher.list_models()
            result.loaded_models = [m["id"] for m in models_info]
        except Exception:
            pass

    # Get hardware info from startup report
    report = getattr(request.app.state, "startup_report", None)
    if report is not None:
        result.hardware_summary = getattr(report, "hardware_summary", "")

    # Get VRAM info
    try:
        from llmhosts.discovery.hardware import HardwareDetector

        hw = await HardwareDetector.detect()
        if hw.gpus:
            total_vram = sum(g.vram_total_mb for g in hw.gpus)
            # Estimate used VRAM (total - free)
            free_vram = sum(g.vram_free_mb for g in hw.gpus if g.vram_free_mb is not None)
            if free_vram > 0:
                result.vram_usage_mb = total_vram - free_vram
            result.vram_total_mb = total_vram
    except Exception:
        pass

    # Uptime from app start time
    start_time = getattr(request.app.state, "start_time", None)
    if start_time is not None:
        result.uptime_seconds = int(time.time() - start_time)

    result.status = "ok"
    return result


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _get_engine_url(request: Request) -> str:
    """Get the local inference engine base URL from config or default."""
    config = getattr(request.app.state, "llmhosts_config", None)
    if config is not None:
        host = getattr(config.ollama, "host", "http://127.0.0.1:11434")
        return host
    return "http://127.0.0.1:11434"


async def _restart_engine_service() -> bool:
    """Attempt to restart the inference engine service.

    Tries systemd first, then direct process management.
    Returns True if restart was initiated.
    """
    import shutil

    # Try systemd restart (Linux)
    if shutil.which("systemctl"):
        try:
            proc = await asyncio.create_subprocess_exec(
                "systemctl",
                "restart",
                "ollama",
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            _, stderr = await asyncio.wait_for(proc.communicate(), timeout=15)
            if proc.returncode == 0:
                return True
            logger.debug("systemctl restart failed: %s", stderr.decode())
        except Exception as exc:
            logger.debug("systemctl restart error: %s", exc)

    # Try user-level systemd
    if shutil.which("systemctl"):
        try:
            proc = await asyncio.create_subprocess_exec(
                "systemctl",
                "--user",
                "restart",
                "ollama",
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            _, stderr = await asyncio.wait_for(proc.communicate(), timeout=15)
            if proc.returncode == 0:
                return True
            logger.debug("systemctl --user restart failed: %s", stderr.decode())
        except Exception as exc:
            logger.debug("systemctl --user restart error: %s", exc)

    # Try launchctl (macOS)
    if shutil.which("launchctl"):
        try:
            proc = await asyncio.create_subprocess_exec(
                "launchctl",
                "kickstart",
                "-kp",
                "system/com.ollama.ollama",
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            await asyncio.wait_for(proc.communicate(), timeout=15)
            if proc.returncode == 0:
                return True
        except Exception:
            pass

    return False


async def _wait_for_engine(url: str, timeout: int = 30) -> bool:
    """Wait for the inference engine to become reachable after restart."""
    import httpx

    deadline = time.time() + timeout
    while time.time() < deadline:
        try:
            async with httpx.AsyncClient(timeout=3.0) as client:
                resp = await client.get(url)
                if resp.status_code == 200:
                    return True
        except Exception:
            pass
        await asyncio.sleep(1)
    return False
