"""Operation tracking endpoints."""

from __future__ import annotations

import logging
import os
import re
import threading
from datetime import UTC, datetime
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException

from ilum.api.deps import get_manager
from ilum.api.models import OperationCancelResponse, OperationResponse
from ilum.api.operations import OperationStore, get_operation_store
from ilum.core.release import ReleaseManager

logger = logging.getLogger(__name__)

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


@router.get("", response_model=list[OperationResponse])
async def list_operations(
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
    store: Annotated[OperationStore, Depends(get_operation_store)],
    module: str | None = None,
    status: str | None = None,
) -> list[OperationResponse]:
    """List recent operations (newest first) with optional filtering."""
    ops = store.list_all()
    for op in ops:
        _refresh_job_status(op, mgr)
    if module:
        ops = [o for o in ops if module in o.modules]
    if status:
        ops = [o for o in ops if o.status == status]
    return ops


READINESS_TIMEOUT = 600.0  # 10 min, same as CLI

_REVISION_RE = re.compile(r"REVISION:\s*(\d+)")


def _extract_revision_from_logs(op: OperationResponse) -> int:
    """Extract the Helm revision from a completed Job's log output.

    Helm prints ``REVISION: <N>`` after a successful upgrade.  This is more
    reliable than querying the current release revision, because the release
    may have moved on (rollback, new upgrade) since the Job finished.
    """
    for log in op.logs:
        m = _REVISION_RE.search(log.message)
        if m:
            return int(m.group(1))
    return 0


def _refresh_job_status(op: OperationResponse, mgr: ReleaseManager) -> None:
    """Update an operation's status from its backing K8s Job (best-effort).

    ``refresh_operation_from_job`` handles both status transitions and log
    capture at the moment a Job reaches a terminal state.  For still-running
    operations we fetch a live log snapshot here.  Once terminal with logs
    already captured, we do not re-fetch — the logs are frozen.
    """
    from ilum.api.job_runner import _capture_job_logs, refresh_operation_from_job

    namespace = os.environ.get("ILUM_NAMESPACE", "default")
    refresh_operation_from_job(op, mgr.k8s, namespace)

    # Pod readiness check after helm Job completes
    if op.status == "awaiting_readiness":
        if not op.expected_revision:
            op.expected_revision = _extract_revision_from_logs(op)
            if not op.expected_revision:
                try:
                    release = os.environ.get("ILUM_RELEASE_NAME", "ilum")
                    info = mgr.get_release_info(release)
                    op.expected_revision = info.revision
                except Exception:
                    pass
        _check_pod_readiness(op, mgr, namespace)
        return

    # For running ops: live log snapshot (overwrite is fine, they're in flux)
    # For terminal ops without logs: one last attempt to capture
    # For terminal ops WITH logs: skip — logs were captured at transition
    if op.job_name and op.status == "running":
        _capture_job_logs(op, mgr.k8s, namespace, tail_lines=50, max_chars=2000)
    elif op.job_name and op.status in ("completed", "failed") and not op.logs:
        _capture_job_logs(op, mgr.k8s, namespace)


def _mark_op_completed(op: OperationResponse) -> None:
    """Transition an operation to its final completed state."""
    op.status = "completed"
    op.progress = 100
    op.completed_at = datetime.now(UTC).isoformat()


def _check_pod_readiness(op: OperationResponse, mgr: ReleaseManager, namespace: str) -> None:
    """Check whether modules' pods are Running+ready.

    Promotes to ``completed`` when all pods are ready, or ``failed`` on timeout.
    Skipped for ``disable`` operations (pods are terminating).
    """
    # Disable operations — pods are terminating, nothing to wait for
    if op.operation == "disable":
        _mark_op_completed(op)
        return

    # Collect pod labels for each module in this operation
    labels: list[tuple[str, str]] = []
    for name in op.modules:
        try:
            mod = mgr.resolver.get(name)
            if mod.pod_label:
                labels.append((name, mod.pod_label))
        except Exception:
            continue

    # No pods to check → complete immediately
    if not labels:
        _mark_op_completed(op)
        return

    # Check if release was rolled back or superseded
    if op.expected_revision:
        try:
            release = os.environ.get("ILUM_RELEASE_NAME", "ilum")
            info = mgr.get_release_info(release)
            if info.revision != op.expected_revision:
                op.status = "failed"
                op.error = (
                    f"Release was rolled back or superseded "
                    f"(expected revision {op.expected_revision}, current {info.revision})"
                )
                op.completed_at = datetime.now(UTC).isoformat()
                return
        except Exception:
            pass  # Fall through to pod check + timeout

    # Check timeout
    if op.helm_completed_at:
        elapsed = (datetime.now(UTC) - datetime.fromisoformat(op.helm_completed_at)).total_seconds()
        if elapsed > READINESS_TIMEOUT:
            op.status = "failed"
            op.error = f"Pods did not become ready within {int(READINESS_TIMEOUT)}s"
            op.completed_at = datetime.now(UTC).isoformat()
            return

    # Check pod readiness per module
    all_ready = True
    total = 0
    ready = 0
    for _, label in labels:
        try:
            pods = mgr.k8s.list_pods_by_label(namespace, label)
        except Exception:
            all_ready = False
            continue
        if not pods:
            all_ready = False
            continue
        for p in pods:
            # Skip completed/failed Job pods (migrations, init containers)
            if p.phase in ("Succeeded", "Failed"):
                continue
            total += 1
            if p.ready and p.phase == "Running":
                ready += 1
            else:
                all_ready = False

    if all_ready and total > 0:
        _mark_op_completed(op)
        return

    # Update progress (80-99 range during readiness)
    if total > 0:
        op.progress = 80 + int((ready / total) * 19)


@router.get("/{op_id}", response_model=OperationResponse)
async def get_operation(
    op_id: str,
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
    store: Annotated[OperationStore, Depends(get_operation_store)],
) -> OperationResponse:
    """Get operation status by ID."""
    op = store.get(op_id)
    if op is None:
        raise HTTPException(404, f"Operation '{op_id}' not found.")
    _refresh_job_status(op, mgr)
    return op


@router.post("/{op_id}/cancel", response_model=OperationCancelResponse)
async def cancel_operation(
    op_id: str,
    mgr: Annotated[ReleaseManager, Depends(get_manager)],
    store: Annotated[OperationStore, Depends(get_operation_store)],
) -> OperationCancelResponse:
    """Cancel an operation by rolling back to the previous Helm revision."""
    op = store.get(op_id)
    if op is None:
        raise HTTPException(404, f"Operation '{op_id}' not found.")

    if op.status in ("completed", "failed"):
        return OperationCancelResponse(
            id=op_id, status=op.status, message="Operation already finished."
        )

    if op.status in ("cancelled", "cancelling"):
        return OperationCancelResponse(id=op_id, status=op.status, message="Already cancelling.")

    store.mark_cancelling(op_id)

    def _rollback() -> None:
        release = os.environ.get("ILUM_RELEASE_NAME", "ilum")
        namespace = os.environ.get("ILUM_NAMESPACE", "default")
        # Delete the backing Job if present
        if op.job_name:
            try:
                mgr.k8s.delete_job(op.job_name, namespace)
            except Exception:
                logger.warning("Failed to delete Job '%s' during cancel", op.job_name)
        # Only rollback for operations that ran helm upgrade;
        # restart only deletes pods and has no helm revision to revert.
        if op.operation != "restart":
            mgr.helm.rollback(release)
        store.mark_cancelled(op_id)

    thread = threading.Thread(target=_rollback, daemon=True)
    thread.start()

    return OperationCancelResponse(
        id=op_id,
        status="cancelling",
        message="Operation cancelling. Rolling back to previous revision.",
    )
