"""In-memory operation store and background thread runner."""

from __future__ import annotations

import threading
import uuid
from collections import OrderedDict
from collections.abc import Callable
from datetime import UTC, datetime

from ilum.api.models import OperationLog, OperationResponse
from ilum.errors import IlumError

MAX_OPERATIONS = 100

_lock = threading.Lock()


class OperationStore:
    """Thread-safe in-memory store for async operations."""

    def __init__(self, max_size: int = MAX_OPERATIONS) -> None:
        self._ops: OrderedDict[str, OperationResponse] = OrderedDict()
        self._max_size = max_size
        self._write_lock = threading.Lock()

    def create(self, operation: str, modules: list[str] | None = None) -> str:
        """Create a pending operation and return its ID."""
        op_id = uuid.uuid4().hex[:12]
        op = OperationResponse(
            id=op_id,
            status="pending",
            operation=operation,
            modules=modules or [],
            created_at=datetime.now(UTC).isoformat(),
        )
        with self._write_lock:
            self._ops[op_id] = op
            # FIFO eviction
            while len(self._ops) > self._max_size:
                self._ops.popitem(last=False)
        return op_id

    def get(self, op_id: str) -> OperationResponse | None:
        """Return an operation by ID, or None."""
        return self._ops.get(op_id)

    def list_all(self) -> list[OperationResponse]:
        """Return all operations, newest first."""
        return list(reversed(self._ops.values()))

    # -- Progress and logging ------------------------------------------------

    def update_progress(self, op_id: str, progress: int, message: str = "") -> None:
        """Update the progress percentage and optionally append a log entry."""
        with self._write_lock:
            op = self._ops.get(op_id)
            if op:
                op.progress = progress
                if message:
                    op.logs.append(
                        OperationLog(
                            timestamp=datetime.now(UTC).isoformat(),
                            message=message,
                            level="info",
                        )
                    )

    def add_log(self, op_id: str, message: str, level: str = "info") -> None:
        """Append a log entry without changing progress."""
        with self._write_lock:
            op = self._ops.get(op_id)
            if op:
                op.logs.append(
                    OperationLog(
                        timestamp=datetime.now(UTC).isoformat(),
                        message=message,
                        level=level,
                    )
                )

    # -- Status transitions --------------------------------------------------

    def _mark_running(self, op_id: str) -> None:
        op = self._ops.get(op_id)
        if op:
            op.status = "running"

    def _mark_completed(self, op_id: str) -> None:
        op = self._ops.get(op_id)
        if op:
            op.status = "completed"
            op.progress = 100
            op.completed_at = datetime.now(UTC).isoformat()
            op.logs.append(
                OperationLog(
                    timestamp=datetime.now(UTC).isoformat(),
                    message="Operation completed successfully",
                    level="success",
                )
            )

    def _mark_failed(self, op_id: str, error: str, error_code: str = "") -> None:
        op = self._ops.get(op_id)
        if op:
            op.status = "failed"
            op.completed_at = datetime.now(UTC).isoformat()
            op.error = error
            op.error_code = error_code
            op.logs.append(
                OperationLog(
                    timestamp=datetime.now(UTC).isoformat(),
                    message=f"Operation failed: {error}",
                    level="error",
                )
            )

    def mark_cancelling(self, op_id: str) -> None:
        """Set operation status to cancelling."""
        with self._write_lock:
            op = self._ops.get(op_id)
            if op:
                op.status = "cancelling"

    def mark_cancelled(self, op_id: str) -> None:
        """Set operation status to cancelled."""
        with self._write_lock:
            op = self._ops.get(op_id)
            if op:
                op.status = "cancelled"
                op.completed_at = datetime.now(UTC).isoformat()

    def import_operation(self, op: OperationResponse) -> None:
        """Import a pre-built operation (recovery from K8s Jobs)."""
        with self._write_lock:
            self._ops[op.id] = op
            while len(self._ops) > self._max_size:
                self._ops.popitem(last=False)

    def has_active_helm_operation(self) -> bool:
        """True if any helm operation (enable/disable/upgrade) is pending or running."""
        for op in self._ops.values():
            if op.status in ("pending", "running") and op.operation in (
                "enable",
                "disable",
                "upgrade",
            ):
                return True
        return False

    # -- Background execution ------------------------------------------------

    def run_in_thread(
        self,
        op_id: str,
        fn: Callable[[str, OperationStore], None],
    ) -> threading.Thread:
        """Run *fn* in a background thread, updating the operation status.

        The callable receives ``(op_id, store)`` so it can report progress.
        """

        def _wrapper() -> None:
            with self._write_lock:
                self._mark_running(op_id)
            try:
                fn(op_id, self)
                with self._write_lock:
                    self._mark_completed(op_id)
            except IlumError as exc:
                with self._write_lock:
                    self._mark_failed(op_id, str(exc), exc.error_code)
            except Exception as exc:
                with self._write_lock:
                    self._mark_failed(op_id, str(exc))

        t = threading.Thread(target=_wrapper, daemon=True)
        t.start()
        return t


# Singleton
_store: OperationStore | None = None


def get_operation_store() -> OperationStore:
    """FastAPI dependency — singleton OperationStore."""
    global _store  # noqa: PLW0603
    if _store is None:
        _store = OperationStore()
    return _store
