"""Repositorios Mongo y almacenamiento local usados por el flujo batch."""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any

from bson import ObjectId
from pymongo.errors import OperationFailure

from app.core.logging import InstrumentedMongoCollection, bind_log_context, get_audit_logger
from app.core.mongo_runtime import get_mongo_runtime


def _serialize_document(doc: dict[str, Any] | None) -> dict[str, Any] | None:
    if not doc:
        return None
    serialized = dict(doc)
    if "_id" in serialized:
        serialized["_id"] = str(serialized["_id"])
    return serialized


def _safe_create_index(collection, keys: list[tuple[str, int]], **kwargs: Any) -> None:
    """
    Crea índices de forma tolerante para no romper arranque si ya existen
    con opciones previas (ej. unique en entornos con migraciones parciales).
    """

    try:
        collection.create_index(keys, **kwargs)
    except OperationFailure as exc:
        if getattr(exc, "code", None) in {85, 86}:
            return
        raise


class MongoBatchRepository:
    """Persistencia principal del lote en MongoDB."""

    def __init__(self, database: Any | None = None) -> None:
        db = database if database is not None else get_mongo_runtime().sync_database
        self.collection = InstrumentedMongoCollection(db["processing_batches"], service="batch_repository")
        self.audit_logger = get_audit_logger()

    def create_batch(self, payload: dict) -> str:
        inserted = self.collection.insert_one(payload)
        batch_id = str(inserted.inserted_id)
        bind_log_context(batch_id=batch_id)
        self.audit_logger.batch_event(
            action="batch_created",
            outcome="success",
            service="batch_repository",
            resource={"batch_id": batch_id, "status": payload.get("status", "")},
        )
        return batch_id

    def update_batch(self, batch_id: str, payload: dict) -> None:
        if not ObjectId.is_valid(batch_id):
            return
        self.collection.update_one({"_id": ObjectId(batch_id)}, {"$set": payload})
        if payload.get("status"):
            bind_log_context(batch_id=batch_id)
            self.audit_logger.batch_event(
                action="batch_status_updated",
                outcome="success",
                service="batch_repository",
                resource={"batch_id": batch_id, "status": payload.get("status")},
            )

    def get_batch(self, batch_id: str) -> dict | None:
        if not ObjectId.is_valid(batch_id):
            return None
        return _serialize_document(self.collection.find_one({"_id": ObjectId(batch_id)}))

    def list_user_batches(self, username: str, *, limit: int = 20) -> list[dict]:
        normalized_limit = max(1, min(int(limit or 20), 100))
        docs = list(
            self.collection.find({"usuario": username}).sort([("updated_at", -1)]).limit(normalized_limit)
        )
        return [_serialize_document(doc) for doc in docs if doc]

    def acquire_cases_refresh_lock(self, batch_id: str, owner: str, ttl_seconds: int) -> bool:
        if not ObjectId.is_valid(batch_id) or not owner:
            return False
        now = datetime.now(UTC)
        expired_before = (now - timedelta(seconds=max(int(ttl_seconds or 0), 1))).isoformat()
        result = self.collection.update_one(
            {
                "_id": ObjectId(batch_id),
                "$or": [
                    {"cases_refresh_lock_owner": {"$exists": False}},
                    {"cases_refresh_lock_owner": ""},
                    {"cases_refresh_lock_owner": owner},
                    {"cases_refresh_locked_at": {"$lt": expired_before}},
                ],
            },
            {
                "$set": {
                    "cases_refresh_lock_owner": owner,
                    "cases_refresh_locked_at": now.isoformat(),
                }
            },
        )
        return bool(getattr(result, "matched_count", 0))

    def release_cases_refresh_lock(self, batch_id: str, owner: str) -> None:
        if not ObjectId.is_valid(batch_id) or not owner:
            return
        self.collection.update_one(
            {"_id": ObjectId(batch_id), "cases_refresh_lock_owner": owner},
            {"$unset": {"cases_refresh_lock_owner": "", "cases_refresh_locked_at": ""}},
        )


class MongoBatchFileRepository:
    """Persistencia por archivo del lote en MongoDB."""

    def __init__(self, database: Any | None = None) -> None:
        db = database if database is not None else get_mongo_runtime().sync_database
        self.collection = InstrumentedMongoCollection(db["processing_batch_files"], service="batch_file_repository")
        self.audit_logger = get_audit_logger()

    def create_file(self, payload: dict) -> str:
        inserted = self.collection.insert_one(payload)
        file_id = str(inserted.inserted_id)
        bind_log_context(batch_id=payload.get("batch_id", ""), file_id=file_id)
        self.audit_logger.batch_event(
            action="batch_file_created",
            outcome="success",
            service="batch_file_repository",
            resource={
                "batch_id": payload.get("batch_id", ""),
                "file_id": file_id,
                "status": payload.get("status", ""),
            },
        )
        return file_id

    def update_file(self, file_id: str, payload: dict) -> None:
        if not ObjectId.is_valid(file_id):
            return
        self.collection.update_one({"_id": ObjectId(file_id)}, {"$set": payload})
        if payload.get("status") or payload.get("clinical_status"):
            bind_log_context(batch_id=payload.get("batch_id", ""), file_id=file_id, case_key=payload.get("case_key", ""))
            self.audit_logger.batch_event(
                action="batch_file_status_updated",
                outcome="success",
                service="batch_file_repository",
                resource={
                    "file_id": file_id,
                    "status": payload.get("status", ""),
                    "clinical_status": payload.get("clinical_status", ""),
                    "case_key": payload.get("case_key", ""),
                },
            )

    def get_file(self, file_id: str) -> dict | None:
        if not ObjectId.is_valid(file_id):
            return None
        return _serialize_document(self.collection.find_one({"_id": ObjectId(file_id)}))

    def list_files(self, batch_id: str, *, status: str | None = None) -> list[dict]:
        query: dict[str, Any] = {"batch_id": batch_id}
        if status:
            query["status"] = status
        docs = list(
            self.collection.find(query).sort([("created_at", 1), ("original_name", 1)])
        )
        return [_serialize_document(doc) for doc in docs if doc]


class MongoBatchCaseRepository:
    """Persistencia de casos consolidados a partir de la asociación automática."""

    def __init__(self, database: Any | None = None) -> None:
        db = database if database is not None else get_mongo_runtime().sync_database
        self.collection = InstrumentedMongoCollection(db["processing_batch_cases"], service="batch_case_repository")
        self.audit_logger = get_audit_logger()

    def replace_cases(self, batch_id: str, cases: list[dict]) -> None:
        deduped_cases: dict[str, dict[str, Any]] = {}
        for case in cases:
            case_key = str(case.get("case_key") or "").strip()
            if not case_key:
                continue
            deduped_cases[case_key] = {**case, "batch_id": batch_id, "case_key": case_key}

        if deduped_cases:
            for case_key, payload in deduped_cases.items():
                self.collection.replace_one(
                    {"batch_id": batch_id, "case_key": case_key},
                    payload,
                    upsert=True,
                )
            self.collection.delete_many(
                {"batch_id": batch_id, "case_key": {"$nin": list(deduped_cases)}}
            )
        else:
            self.collection.delete_many({"batch_id": batch_id})

        bind_log_context(batch_id=batch_id)
        self.audit_logger.batch_event(
            action="batch_cases_replaced",
            outcome="success",
            service="batch_case_repository",
            resource={"batch_id": batch_id},
            metrics={"case_count": len(deduped_cases)},
        )

    def list_cases(self, batch_id: str) -> list[dict]:
        docs = list(
            self.collection.find({"batch_id": batch_id}).sort(
                [("patient_name", 1), ("case_number", 1), ("case_key", 1)]
            )
        )
        return [_serialize_document(doc) for doc in docs if doc]

    def get_case(self, batch_id: str, case_key: str) -> dict | None:
        return _serialize_document(
            self.collection.find_one({"batch_id": batch_id, "case_key": case_key})
        )

    def get_user_case(self, username: str, case_key: str) -> dict | None:
        return _serialize_document(
            self.collection.find_one(
                {"usuario": username, "case_key": case_key},
                sort=[("updated_at", -1)],
            )
        )

    def upsert_user_case(self, username: str, case_key: str, payload: dict) -> dict:
        current = self.get_user_case(username, case_key) or {}
        current_case_id = str(current.get("_id") or "").strip()

        preserved = dict(current)
        preserved.pop("_id", None)
        document = {
            **preserved,
            **payload,
            "usuario": username,
            "case_key": case_key,
        }

        if current_case_id and ObjectId.is_valid(current_case_id):
            self.collection.update_one({"_id": ObjectId(current_case_id)}, {"$set": document})
            updated = _serialize_document(self.collection.find_one({"_id": ObjectId(current_case_id)})) or {
                **document,
                "_id": current_case_id,
            }
        else:
            inserted = self.collection.insert_one(document)
            updated = _serialize_document(self.collection.find_one({"_id": inserted.inserted_id})) or {
                **document,
                "_id": str(inserted.inserted_id),
            }

        if payload.get("epicrisis_status"):
            bind_log_context(
                batch_id=updated.get("batch_id", ""),
                case_key=case_key,
                job_id=payload.get("epicrisis_job_id", updated.get("epicrisis_job_id", "")),
            )
            self.audit_logger.batch_event(
                action="case_epicrisis_status_updated",
                outcome="success",
                service="batch_case_repository",
                resource={
                    "batch_id": updated.get("batch_id", ""),
                    "case_key": case_key,
                    "epicrisis_status": payload.get("epicrisis_status", ""),
                },
            )

        return updated

    def update_case(self, batch_id: str, case_key: str, payload: dict) -> None:
        self.collection.update_one(
            {"batch_id": batch_id, "case_key": case_key},
            {"$set": payload},
        )
        if payload.get("epicrisis_status"):
            bind_log_context(batch_id=batch_id, case_key=case_key, job_id=payload.get("epicrisis_job_id", ""))
            self.audit_logger.batch_event(
                action="case_epicrisis_status_updated",
                outcome="success",
                service="batch_case_repository",
                resource={
                    "batch_id": batch_id,
                    "case_key": case_key,
                    "epicrisis_status": payload.get("epicrisis_status", ""),
                },
            )


class LocalBatchArchiveStore:
    """Guarda el ZIP original localmente para que el worker lo procese después."""

    def __init__(self, base_dir: Path) -> None:
        self.base_dir = Path(base_dir) / "_batch_uploads"
        self.base_dir.mkdir(parents=True, exist_ok=True)

    def save_archive(self, batch_id: str, filename: str, data: bytes) -> str:
        batch_dir = self.base_dir / batch_id
        batch_dir.mkdir(parents=True, exist_ok=True)
        archive_path = batch_dir / filename
        archive_path.write_bytes(data)
        return str(archive_path)
