from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any, Protocol
from urllib.parse import quote

from app.batch_processing.domain.models import (
    BATCH_STATUS_CANCELADO,
    BATCH_STATUS_COMPLETADO,
    BATCH_STATUS_COMPLETADO_CON_ERRORES,
    BATCH_STATUS_FALLIDO,
    EPICRISIS_STATUS_EN_COLA,
    EPICRISIS_STATUS_FALLIDO,
    EPICRISIS_STATUS_PENDIENTE,
    EPICRISIS_STATUS_PROCESANDO,
)
from app.individual_ingestion.domain.models import (
    UPLOAD_STATUS_CANCELADO,
    UPLOAD_STATUS_COMPLETADO,
    UPLOAD_STATUS_FALLIDO,
)


COMPLETED_TTL_HOURS = 8


class QueueSource(Protocol):
    def build_items(self, *, username: str, include_terminal: bool) -> list[dict[str, Any]]: ...


def _safe_iso_to_datetime(value: Any) -> datetime:
    if not value:
        return datetime.min
    try:
        return datetime.fromisoformat(str(value))
    except ValueError:
        return datetime.min


def _is_recent_enough(value: Any) -> bool:
    parsed = _safe_iso_to_datetime(value)
    if parsed == datetime.min:
        return False
    now = datetime.now() if parsed.tzinfo is None else datetime.now(parsed.tzinfo)
    return parsed >= now - timedelta(hours=COMPLETED_TTL_HOURS)


def _status_group(status: str) -> str:
    normalized = str(status or "").strip().lower()
    if normalized in {
        "fallido",
        "blocked",
        "blocked_base_failed",
    }:
        return "error"
    if normalized in {
        "cancelado",
        "completed",
        "completado",
    }:
        return "success"
    if normalized in {
        "esperando_confirmacion",
        "listo_para_procesar",
        "waiting_base_materialization",
        "pendiente",
    }:
        return "warning"
    return "info"


def _sort_moment(value: Any) -> float:
    parsed = _safe_iso_to_datetime(value)
    if parsed == datetime.min:
        return float("-inf")
    try:
        return parsed.timestamp()
    except (OverflowError, OSError, ValueError):
        return float("-inf")


def _sort_key(item: dict[str, Any]) -> tuple[int, float]:
    terminal = bool(item.get("terminal"))
    return (0 if terminal else 1, _sort_moment(item.get("updated_at")))


def _sort_children(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
    return sorted(items, key=_sort_key, reverse=True)


def _queue_item(**payload: Any) -> dict[str, Any]:
    return payload


@dataclass
class ManualSessionQueueSource:
    runtime: Any
    demo_identity_service: Any

    def build_items(self, *, username: str, include_terminal: bool) -> list[dict[str, Any]]:
        sessions = self.runtime.list_sessions.execute(username, include_terminal=include_terminal)
        items = [
            item
            for session in sessions
            if (item := self._build_session_item(session=session, username=username, include_terminal=include_terminal))
            is not None
        ]
        return sorted(items, key=_sort_key, reverse=True)

    def _build_session_item(
        self,
        *,
        session: dict[str, Any],
        username: str,
        include_terminal: bool,
    ) -> dict[str, Any] | None:
        uploads = list(session.get("uploads") or [])
        updated_at = self._updated_at(session)
        session_status = self._session_status(session)
        is_terminal = self._is_session_terminal(session_status)
        if not self._should_include_session(
            include_terminal=include_terminal,
            is_terminal=is_terminal,
            updated_at=updated_at,
        ):
            return None

        case_key = str(session.get("active_case_key") or "").strip()
        session_id = str(session.get("session_id") or "").strip()
        base_upload = self._resolve_base_upload(session, uploads)
        visible_identity = self._project_visible_identity(
            username=username,
            case_key=case_key,
            base_upload=base_upload,
        )
        children = _sort_children([self._build_upload_child(upload) for upload in uploads])
        return _queue_item(
            item_type="manual_session",
            node_kind="group",
            item_id=session_id,
            group_id=session_id,
            title=self._build_session_title(
                visible_identity=visible_identity,
                base_upload=base_upload,
                case_key=case_key,
            ),
            subtitle=self._build_session_subtitle(visible_identity),
            status=session_status or "active",
            status_group=_status_group(session_status),
            case_key=visible_identity.get("case_key", case_key),
            session_id=session_id,
            updated_at=updated_at,
            expires_at=(updated_at if is_terminal else ""),
            actions=self._build_session_actions(
                session_id=session_id,
                case_key=case_key,
                visible_identity=visible_identity,
                children=children,
            ),
            children=children,
            terminal=is_terminal,
        )

    def _updated_at(self, session: dict[str, Any]) -> str:
        return str(session.get("updated_at") or "").strip()

    def _session_status(self, session: dict[str, Any]) -> str:
        return str(session.get("session_status") or "").strip()

    def _is_session_terminal(self, session_status: str) -> bool:
        return session_status in {"completed", "cancelled"}

    def _should_include_session(
        self,
        *,
        include_terminal: bool,
        is_terminal: bool,
        updated_at: str,
    ) -> bool:
        if not is_terminal:
            return True
        if not include_terminal:
            return False
        return _is_recent_enough(updated_at)

    def _resolve_base_upload(self, session: dict[str, Any], uploads: list[dict[str, Any]]) -> dict[str, Any]:
        base_upload_id = str(session.get("base_upload_id") or "").strip()
        return next(
            (item for item in uploads if str(item.get("upload_id") or "") == base_upload_id),
            uploads[0] if uploads else {},
        )

    def _project_visible_identity(
        self,
        *,
        username: str,
        case_key: str,
        base_upload: dict[str, Any],
    ) -> dict[str, Any]:
        return self.demo_identity_service.project_case_identity(
            username=username,
            case_key=case_key,
            case_number=self._case_number(base_upload),
            patient_id=self._patient_id(base_upload),
            patient_name=self._patient_name(base_upload),
        )

    def _case_number(self, base_upload: dict[str, Any]) -> str:
        return str(
            base_upload.get("confirmed_case_number") or base_upload.get("provisional_case_number") or ""
        ).strip()

    def _patient_id(self, base_upload: dict[str, Any]) -> str:
        return str(
            base_upload.get("confirmed_patient_id") or base_upload.get("provisional_patient_id") or ""
        ).strip()

    def _patient_name(self, base_upload: dict[str, Any]) -> str:
        return str(
            base_upload.get("confirmed_patient_name")
            or base_upload.get("provisional_patient_name")
            or "Paciente en preparación"
        ).strip()

    def _build_session_title(
        self,
        *,
        visible_identity: dict[str, Any],
        base_upload: dict[str, Any],
        case_key: str,
    ) -> str:
        return str(
            visible_identity.get("case_number")
            or base_upload.get("confirmed_case_number")
            or base_upload.get("provisional_case_number")
            or visible_identity.get("case_key")
            or case_key
            or "Sesión manual"
        ).strip()

    def _build_session_subtitle(self, visible_identity: dict[str, Any]) -> str:
        return " · ".join(
            filter(
                None,
                [
                    str(visible_identity.get("patient_name") or "").strip(),
                    str(visible_identity.get("patient_id") or "").strip(),
                ],
            )
        )

    def _build_session_actions(
        self,
        *,
        session_id: str,
        case_key: str,
        visible_identity: dict[str, Any],
        children: list[dict[str, Any]],
    ) -> list[dict[str, Any]]:
        actions = [
            {
                "action": "resume_session",
                "label": "Reanudar",
                "session_id": session_id,
                "case_key": visible_identity.get("case_key", case_key),
            }
        ]
        if all(bool(child.get("terminal")) for child in children):
            actions.append(
                {
                    "action": "close_session",
                    "label": "Cerrar sesión",
                    "session_id": session_id,
                }
            )
        return actions

    def _build_upload_child(self, upload: dict[str, Any]) -> dict[str, Any]:
        status = str(upload.get("status") or "").strip()
        upload_id = str(upload.get("upload_id") or "").strip()
        selected_type = str(
            upload.get("effective_document_type") or upload.get("selected_document_type") or "documento"
        ).replace("_", " ")
        original_name = str(upload.get("original_name") or "").strip()
        terminal = status in {UPLOAD_STATUS_COMPLETADO, UPLOAD_STATUS_CANCELADO, UPLOAD_STATUS_FALLIDO}
        actions: list[dict[str, Any]] = []
        if not terminal:
            actions.append(
                {
                    "action": "cancel_upload",
                    "label": "Cancelar",
                    "upload_id": upload_id,
                }
            )
        return _queue_item(
            item_type="manual_upload",
            node_kind="leaf",
            item_id=upload_id,
            title=selected_type[:1].upper() + selected_type[1:],
            subtitle=original_name or f"Estado {status}",
            status=status,
            status_group=_status_group(status),
            updated_at=str(upload.get("updated_at") or "").strip(),
            actions=actions,
            terminal=terminal,
        )


@dataclass
class BatchQueueSource:
    runtime: Any

    def build_items(self, *, username: str, include_terminal: bool) -> list[dict[str, Any]]:
        batches = self.runtime.list_user_batches.execute(username, limit=20)
        items: list[dict[str, Any]] = []
        for batch in batches:
            status = str(batch.get("status") or "").strip()
            updated_at = str(batch.get("updated_at") or batch.get("created_at") or "").strip()
            is_terminal = status in {
                BATCH_STATUS_COMPLETADO,
                BATCH_STATUS_COMPLETADO_CON_ERRORES,
                BATCH_STATUS_FALLIDO,
                BATCH_STATUS_CANCELADO,
            }
            if is_terminal and not include_terminal:
                continue
            if is_terminal and not _is_recent_enough(updated_at):
                continue
            batch_id = str(batch.get("batch_id") or "").strip()
            actions = [
                {
                    "action": "open_batch",
                    "label": "Abrir lote",
                    "href": f"/subir_lote?batch_id={quote(batch_id)}" if batch_id else "/subir_lote",
                }
            ]
            if not is_terminal:
                actions.append(
                    {
                        "action": "cancel_batch",
                        "label": "Cancelar",
                        "batch_id": batch_id,
                    }
                )
            if self._is_retryable(batch):
                actions.append(
                    {
                        "action": "retry_batch",
                        "label": "Reintentar",
                        "batch_id": batch_id,
                    }
                )
            items.append(
                _queue_item(
                    item_type="batch",
                    node_kind="leaf",
                    item_id=batch_id,
                    title=str(batch.get("nombre_archivo") or "Lote").strip() or "Lote",
                    subtitle=str(batch.get("ingestion_mode") or "zip").replace("_", " "),
                    status=status or "recibido",
                    status_group=_status_group(status),
                    batch_id=batch_id,
                    updated_at=updated_at,
                    expires_at=(updated_at if is_terminal else ""),
                    actions=actions,
                    terminal=is_terminal,
                )
            )
        return sorted(items, key=_sort_key, reverse=True)

    def _is_retryable(self, batch: dict[str, Any]) -> bool:
        return bool(
            int(batch.get("failed_files") or 0) > 0
            or int(batch.get("clinical_failed_files") or 0) > 0
            or int(batch.get("clinical_pending_files") or 0) > 0
        )


@dataclass
class EpicrisisQueueSource:
    case_repository: Any
    demo_identity_service: Any

    def build_items(self, *, username: str, include_terminal: bool) -> list[dict[str, Any]]:
        cases = self.case_repository.list_user_cases(username, limit=50)
        items: list[dict[str, Any]] = []
        for case in cases:
            status = str(case.get("epicrisis_status") or EPICRISIS_STATUS_PENDIENTE).strip().lower()
            if status == EPICRISIS_STATUS_PENDIENTE:
                continue
            updated_at = str(case.get("updated_at") or "").strip()
            is_terminal = status in {EPICRISIS_STATUS_FALLIDO, "completado"}
            if is_terminal and not include_terminal:
                continue
            if is_terminal and not _is_recent_enough(updated_at):
                continue
            case_key = str(case.get("case_key") or "").strip()
            visible_identity = self.demo_identity_service.project_case_identity(
                username=username,
                case_key=case_key,
                case_number=str(case.get("case_number") or "").strip(),
                patient_id=str(case.get("patient_id") or "").strip(),
                patient_name=str(case.get("patient_name") or "").strip(),
            )
            actions = [
                {
                    "action": "open_epicrisis",
                    "label": "Abrir caso",
                    "href": f"/casos?case_key={quote(visible_identity.get('case_key', case_key))}",
                },
                {
                    "action": "open_epicrisis",
                    "label": "Abrir epicrisis",
                    "href": str(
                        case.get("epicrisis_url")
                        or f"/epicrisis?case_key={quote(visible_identity.get('case_key', case_key))}"
                    ),
                },
            ]
            if status not in {EPICRISIS_STATUS_EN_COLA, EPICRISIS_STATUS_PROCESANDO}:
                actions.append(
                    {
                        "action": "retry_epicrisis",
                        "label": "Reintentar",
                        "case_key": visible_identity.get("case_key", case_key),
                    }
                )
            items.append(
                _queue_item(
                    item_type="epicrisis",
                    node_kind="leaf",
                    item_id=case_key,
                    title=str(
                        visible_identity.get("case_number")
                        or visible_identity.get("case_key")
                        or case_key
                        or "Epicrisis"
                    ),
                    subtitle=str(visible_identity.get("patient_name") or "").strip(),
                    status=status,
                    status_group=_status_group(status),
                    case_key=visible_identity.get("case_key", case_key),
                    updated_at=updated_at,
                    expires_at=(updated_at if is_terminal else ""),
                    actions=actions,
                    terminal=is_terminal,
                )
            )
        return sorted(items, key=_sort_key, reverse=True)


@dataclass
class OperationalQueueFacade:
    sources: list[QueueSource]

    def build(self, *, username: str, include_terminal: bool = False) -> dict[str, Any]:
        items: list[dict[str, Any]] = []
        for source in self.sources:
            items.extend(source.build_items(username=username, include_terminal=include_terminal))
        ordered_items = sorted(items, key=_sort_key, reverse=True)
        summary = {
            "active": sum(1 for item in ordered_items if not bool(item.get("terminal"))),
            "terminal": sum(1 for item in ordered_items if bool(item.get("terminal"))),
            "groups": sum(1 for item in ordered_items if item.get("node_kind") == "group"),
            "leaves": sum(1 for item in ordered_items if item.get("node_kind") == "leaf"),
        }
        return {
            "items": ordered_items,
            "summary": summary,
        }
