"""Оркестрация индексации RAG (очередь задач, события)."""

import asyncio
import logging
import os
from collections import defaultdict

from app.schemas.common import ErrorPayload, ModuleName
from app.schemas.indexing import IndexJobStatus
from app.core.contracts import RagIndexer
from app.core.rag.indexing.common.path_filter import (
    count_indexable_change_upserts,
    filter_changes_for_indexing,
    filter_snapshot_files,
)
from app.core.rag.indexing.job_store import IndexJob, IndexJobStore
from app.core.shared.messaging import EventBus
from app.core.shared.resilience import RetryExecutor

LOGGER = logging.getLogger(__name__)


class IndexingOrchestrator:
    def __init__(
        self,
        store: IndexJobStore,
        rag: RagIndexer,
        events: EventBus,
        retry: RetryExecutor,
    ) -> None:
        self._store = store
        self._rag = rag
        self._events = events
        self._retry = retry
        self._locks: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock)

    async def enqueue_snapshot(self, rag_session_id: str, files: list[dict]) -> IndexJob:
        job = self._store.create(rag_session_id)
        LOGGER.warning(
            "rag index snapshot queued: job_id=%s rag_session_id=%s files=%s",
            job.index_job_id,
            rag_session_id,
            len(files),
        )
        asyncio.create_task(self._process_snapshot(job.index_job_id, rag_session_id, files))
        return job

    async def enqueue_changes(self, rag_session_id: str, changed_files: list[dict]) -> IndexJob:
        job = self._store.create(rag_session_id)
        LOGGER.warning(
            "rag index changes queued: job_id=%s rag_session_id=%s changes=%s",
            job.index_job_id,
            rag_session_id,
            len(changed_files),
        )
        asyncio.create_task(self._process_changes(job.index_job_id, rag_session_id, changed_files))
        return job

    async def _process_snapshot(self, job_id: str, rag_session_id: str, files: list[dict]) -> None:
        filtered_files = filter_snapshot_files(files)
        await self._run_with_project_lock(
            job_id=job_id,
            rag_session_id=rag_session_id,
            total_files=len(filtered_files),
            operation=lambda progress_cb: self._rag.index_snapshot(
                rag_session_id=rag_session_id,
                files=filtered_files,
                progress_cb=progress_cb,
            ),
        )

    async def _process_changes(self, job_id: str, rag_session_id: str, changed_files: list[dict]) -> None:
        filtered_changes = filter_changes_for_indexing(changed_files)
        await self._run_with_project_lock(
            job_id=job_id,
            rag_session_id=rag_session_id,
            total_files=count_indexable_change_upserts(filtered_changes),
            operation=lambda progress_cb: self._rag.index_changes(
                rag_session_id=rag_session_id,
                changed_files=filtered_changes,
                progress_cb=progress_cb,
            ),
        )

    async def _run_with_project_lock(self, job_id: str, rag_session_id: str, total_files: int, operation) -> None:
        lock = self._locks[rag_session_id]
        async with lock:
            job = self._store.get(job_id)
            if not job:
                LOGGER.warning("rag index job missing in store before start: job_id=%s", job_id)
                return
            job.status = IndexJobStatus.RUNNING
            self._store.save(job)
            LOGGER.warning(
                "rag index job running: job_id=%s rag_session_id=%s total_files=%s",
                job_id,
                rag_session_id,
                total_files,
            )
            await self._events.publish(
                job_id,
                "index_status",
                {"index_job_id": job_id, "status": job.status.value, "total_files": total_files},
            )
            try:
                async def progress_cb(current_file_index: int, total: int, current_file_name: str) -> None:
                    await self._events.publish(
                        job_id,
                        "index_progress",
                        {
                            "index_job_id": job_id,
                            "current_file_index": current_file_index,
                            "total_files": total,
                            "processed_files": current_file_index,
                            "current_file_path": current_file_name,
                            "current_file_name": current_file_name,
                        },
                    )

                timeout_sec = max(1, int(os.getenv("RAG_INDEX_JOB_TIMEOUT_SEC", "15")))
                indexed, failed, cache_hits, cache_misses = await asyncio.wait_for(
                    operation(progress_cb),
                    timeout=timeout_sec,
                )
                job.status = IndexJobStatus.DONE
                job.indexed_files = indexed
                job.failed_files = failed
                job.cache_hit_files = cache_hits
                job.cache_miss_files = cache_misses
                self._store.save(job)
                LOGGER.warning(
                    "rag index job done: job_id=%s indexed=%s failed=%s cache_hits=%s cache_misses=%s",
                    job_id,
                    indexed,
                    failed,
                    cache_hits,
                    cache_misses,
                )
                await self._events.publish(
                    job_id,
                    "index_status",
                    {
                        "index_job_id": job_id,
                        "status": job.status.value,
                        "indexed_files": indexed,
                        "failed_files": failed,
                        "cache_hit_files": cache_hits,
                        "cache_miss_files": cache_misses,
                        "total_files": total_files,
                    },
                )
                await self._events.publish(
                    job_id,
                    "terminal",
                    {
                        "index_job_id": job_id,
                        "status": "done",
                        "indexed_files": indexed,
                        "failed_files": failed,
                        "cache_hit_files": cache_hits,
                        "cache_miss_files": cache_misses,
                        "total_files": total_files,
                    },
                )
            except (TimeoutError, ConnectionError, OSError) as exc:
                job.status = IndexJobStatus.ERROR
                job.failed_files = max(1, job.failed_files)
                job.error = ErrorPayload(
                    code="index_runtime_error",
                    desc=f"Indexing failed: {exc}",
                    module=ModuleName.RAG,
                )
                self._store.save(job)
                LOGGER.exception("rag index job runtime-error: job_id=%s", job_id)
                await self._events.publish(
                    job_id,
                    "index_status",
                    {"index_job_id": job_id, "status": job.status.value, "total_files": total_files},
                )
                await self._events.publish(
                    job_id,
                    "terminal",
                    {
                        "index_job_id": job_id,
                        "status": "error",
                        "total_files": total_files,
                        "error": {
                            "code": job.error.code,
                            "desc": job.error.desc,
                            "module": job.error.module.value,
                        },
                    },
                )
            except asyncio.TimeoutError as exc:
                job.status = IndexJobStatus.ERROR
                job.failed_files = max(1, job.failed_files)
                job.error = ErrorPayload(
                    code="index_timeout",
                    desc=f"Indexing timed out while processing snapshot/changes: {exc}",
                    module=ModuleName.RAG,
                )
                self._store.save(job)
                LOGGER.exception("rag index job timed out: job_id=%s", job_id)
                await self._events.publish(
                    job_id,
                    "index_status",
                    {"index_job_id": job_id, "status": job.status.value, "total_files": total_files},
                )
                await self._events.publish(
                    job_id,
                    "terminal",
                    {
                        "index_job_id": job_id,
                        "status": "error",
                        "total_files": total_files,
                        "error": {
                            "code": job.error.code,
                            "desc": job.error.desc,
                            "module": job.error.module.value,
                        },
                    },
                )
            except Exception as exc:
                job.status = IndexJobStatus.ERROR
                job.failed_files = max(1, job.failed_files)
                job.error = ErrorPayload(
                    code="index_unexpected_error",
                    desc=f"Unexpected indexing failure: {exc}",
                    module=ModuleName.RAG,
                )
                self._store.save(job)
                LOGGER.exception("rag index job failed unexpectedly: job_id=%s", job_id)
                await self._events.publish(
                    job_id,
                    "index_status",
                    {"index_job_id": job_id, "status": job.status.value, "total_files": total_files},
                )
                await self._events.publish(
                    job_id,
                    "terminal",
                    {
                        "index_job_id": job_id,
                        "status": "error",
                        "total_files": total_files,
                        "error": {
                            "code": job.error.code,
                            "desc": job.error.desc,
                            "module": job.error.module.value,
                        },
                    },
                )
