from __future__ import annotations

import json
import mimetypes
from datetime import UTC, datetime
from typing import Any

from app.core.agent_enums import AgentRunStatus
from app.services.knowledge_constants import (
    FIXED_KNOWLEDGE_FOLDERS,
    KNOWLEDGE_INGEST_STATUS_FAILED,
    KNOWLEDGE_INGEST_STATUS_INGESTED,
    KNOWLEDGE_INGEST_STATUS_PUBLISHED,
    KNOWLEDGE_INGEST_STATUS_SYNCING,
)
from app.services.knowledge_ingest_status import (
    is_syncing_status_stale,
    normalize_ingest_status_code,
    resolve_linked_ingest_run_status,
    should_preserve_syncing_status,
)


class KnowledgeIndexStateMixin:
    def _load_index(self) -> dict[str, Any]:
        try:
            payload = json.loads(self.index_path.read_text(encoding="utf-8"))
        except (FileNotFoundError, json.JSONDecodeError):
            payload = {"version": 1, "documents": []}
        payload.setdefault("documents", [])
        return payload

    def _save_index(self, index: dict[str, Any]) -> None:
        self.index_path.write_text(
            json.dumps(index, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )

    def _reconcile_index(self, index: dict[str, Any]) -> bool:
        changed = False
        documents = index.setdefault("documents", [])
        known_by_stored = {
            (item["folder"], item["stored_name"]): item
            for item in documents
            if item.get("folder") and item.get("stored_name")
        }
        existing_items: list[dict[str, Any]] = []
        for item in documents:
            file_path = self._resolve_document_path(item)
            if not file_path.exists():
                changed = True
                continue
            item["size_bytes"] = file_path.stat().st_size
            item["extension"] = self._extract_extension(item["original_name"])
            item["mime_type"] = item.get("mime_type") or (
                mimetypes.guess_type(item["original_name"])[0] or "application/octet-stream"
            )
            normalized_status = normalize_ingest_status_code(item.get("ingest_status"))
            if item.get("ingest_status") != normalized_status:
                item["ingest_status"] = normalized_status
                changed = True
            defaults = {
                "ingest_agent_run_id": "",
                "ingest_status_updated_at": item.get("updated_at")
                or item.get("created_at")
                or "",
                "ingest_completed_at": "",
                "ingest_document_name": "",
                "ingest_document_updated_at": "",
                "ingest_document_sha256": "",
            }
            for key, value in defaults.items():
                if key not in item:
                    item[key] = value
                    changed = True
            existing_items.append(item)

        for folder_name in FIXED_KNOWLEDGE_FOLDERS:
            folder_path = self.library_root / folder_name
            for file_path in folder_path.iterdir():
                if not file_path.is_file() or file_path.name.startswith("."):
                    continue
                key = (folder_name, file_path.name)
                if key in known_by_stored:
                    continue
                document_id, original_name = self._parse_stored_name(file_path.name)
                stat = file_path.stat()
                existing_items.append(
                    {
                        "id": document_id,
                        "folder": folder_name,
                        "original_name": original_name,
                        "stored_name": file_path.name,
                        "mime_type": mimetypes.guess_type(original_name)[0]
                        or "application/octet-stream",
                        "extension": self._extract_extension(original_name),
                        "size_bytes": stat.st_size,
                        "sha256": "",
                        "created_at": datetime.fromtimestamp(stat.st_ctime, tz=UTC).isoformat(),
                        "updated_at": datetime.fromtimestamp(stat.st_mtime, tz=UTC).isoformat(),
                        "uploaded_by": "系统导入",
                        "version_number": 1,
                        "ingest_status": KNOWLEDGE_INGEST_STATUS_PUBLISHED,
                        "ingest_status_updated_at": datetime.now(UTC).isoformat(),
                        "ingest_completed_at": "",
                        "ingest_document_name": "",
                        "ingest_document_updated_at": "",
                        "ingest_document_sha256": "",
                        "ingest_agent_run_id": "",
                    }
                )
                changed = True

        if changed or len(existing_items) != len(documents):
            index["documents"] = existing_items
            return True
        return False

    def _reconcile_document_ingest_statuses(
        self,
        index: dict[str, Any],
        *,
        document_ids: list[str] | None = None,
        preserve_syncing: bool = True,
    ) -> bool:
        changed = False
        target_ids = {str(item).strip() for item in document_ids or [] if str(item).strip()}
        status_map = self._rag_service().get_document_status_map(
            list(target_ids)
            if target_ids
            else [
                str(item.get("id") or "").strip()
                for item in index.get("documents", [])
                if str(item.get("id") or "").strip()
            ]
        )
        for entry in index.get("documents", []):
            document_id = str(entry.get("id") or "").strip()
            if target_ids and document_id not in target_ids:
                continue
            current_status = normalize_ingest_status_code(entry.get("ingest_status"))
            if entry.get("ingest_status") != current_status:
                entry["ingest_status"] = current_status
                changed = True
            if (
                current_status == KNOWLEDGE_INGEST_STATUS_SYNCING
                and preserve_syncing
                and should_preserve_syncing_status(entry, db=self.db)
            ):
                continue

            status_payload = status_map.get(document_id) or {}
            rag_status = str(status_payload.get("status") or "").strip().lower()
            linked_run_status = resolve_linked_ingest_run_status(entry, db=self.db)
            if not status_payload:
                if (
                    current_status == KNOWLEDGE_INGEST_STATUS_SYNCING
                    and linked_run_status == AgentRunStatus.FAILED.value
                ):
                    desired_status = KNOWLEDGE_INGEST_STATUS_FAILED
                else:
                    continue
            elif linked_run_status == AgentRunStatus.FAILED.value and rag_status in {
                "pending",
                "processing",
                "preprocessed",
            }:
                desired_status = KNOWLEDGE_INGEST_STATUS_FAILED
            elif bool(status_payload.get("query_ready")):
                desired_status = KNOWLEDGE_INGEST_STATUS_INGESTED
            elif rag_status in {"pending", "processing", "preprocessed"}:
                desired_status = KNOWLEDGE_INGEST_STATUS_SYNCING
            elif rag_status == "failed":
                desired_status = KNOWLEDGE_INGEST_STATUS_FAILED
            else:
                desired_status = KNOWLEDGE_INGEST_STATUS_PUBLISHED

            if (
                current_status == KNOWLEDGE_INGEST_STATUS_FAILED
                and desired_status == KNOWLEDGE_INGEST_STATUS_PUBLISHED
            ):
                continue
            if current_status != desired_status:
                entry["ingest_status"] = desired_status
                entry["ingest_status_updated_at"] = (
                    str(status_payload.get("updated_at") or "").strip()
                    or datetime.now(UTC).isoformat()
                )
                if desired_status == KNOWLEDGE_INGEST_STATUS_INGESTED:
                    self._mark_entry_ingested(
                        entry,
                        completed_at=entry.get("ingest_status_updated_at")
                        or datetime.now(UTC).isoformat(),
                    )
                changed = True
            elif desired_status == KNOWLEDGE_INGEST_STATUS_INGESTED:
                changed = self._mark_entry_ingested(entry) or changed
        return changed

    def _apply_ingest_status_to_entry(
        self,
        entry: dict[str, Any],
        *,
        status_code: int,
        updated_at: str,
        agent_run_id: str | None,
    ) -> bool:
        changed = False
        current_status = normalize_ingest_status_code(entry.get("ingest_status"))
        if current_status != status_code:
            entry["ingest_status"] = status_code
            changed = True
        if str(entry.get("ingest_status_updated_at") or "").strip() != updated_at:
            entry["ingest_status_updated_at"] = updated_at
            changed = True
        if agent_run_id is not None and entry.get("ingest_agent_run_id") != agent_run_id:
            entry["ingest_agent_run_id"] = agent_run_id
            changed = True
        if status_code == KNOWLEDGE_INGEST_STATUS_INGESTED:
            changed = self._mark_entry_ingested(entry, completed_at=updated_at) or changed
        return changed

    def _mark_entry_ingested(
        self,
        entry: dict[str, Any],
        *,
        completed_at: str | None = None,
    ) -> bool:
        completed_value = (
            str(completed_at or entry.get("ingest_completed_at") or "").strip()
            or datetime.now(UTC).isoformat()
        )
        expected_values = {
            "ingest_completed_at": completed_value,
            "ingest_document_name": str(entry.get("original_name") or "").strip(),
            "ingest_document_updated_at": str(entry.get("updated_at") or "").strip(),
            "ingest_document_sha256": str(entry.get("sha256") or "").strip(),
        }
        changed = False
        for key, value in expected_values.items():
            if str(entry.get(key) or "").strip() != value:
                entry[key] = value
                changed = True
        return changed

    @staticmethod
    def _should_index_document(entry: dict[str, Any]) -> bool:
        status_code = normalize_ingest_status_code(entry.get("ingest_status"))
        if status_code in {
            KNOWLEDGE_INGEST_STATUS_PUBLISHED,
            KNOWLEDGE_INGEST_STATUS_FAILED,
        }:
            return True
        if status_code == KNOWLEDGE_INGEST_STATUS_SYNCING:
            return is_syncing_status_stale(entry)
        return any(
            [
                not str(entry.get("ingest_completed_at") or "").strip(),
                str(entry.get("ingest_document_name") or "").strip()
                != str(entry.get("original_name") or "").strip(),
                str(entry.get("ingest_document_updated_at") or "").strip()
                != str(entry.get("updated_at") or "").strip(),
                str(entry.get("ingest_document_sha256") or "").strip()
                != str(entry.get("sha256") or "").strip(),
            ]
        )
