from __future__ import annotations

import hashlib
import mimetypes
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from uuid import uuid4

from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.core.config import get_settings
from app.core.logging import get_logger
from app.schemas.knowledge import (
    KnowledgeDocumentDetailRead,
    KnowledgeDocumentRead,
    KnowledgeFolderRead,
    KnowledgeLibraryRead,
    KnowledgeOnlyOfficeConfigRead,
    KnowledgePreviewPageRead,
)
from app.services.knowledge_constants import (
    FIXED_KNOWLEDGE_FOLDERS,
    ICON_BY_TYPE,
    KNOWLEDGE_INGEST_STATUS_INGESTED,
    KNOWLEDGE_INGEST_STATUS_META,
    KNOWLEDGE_INGEST_STATUS_PUBLISHED,
    KNOWLEDGE_SEARCH_RESULT_LIMIT,
)
from app.services.knowledge_constants import (
    KNOWLEDGE_INGEST_STATUS_FAILED as KNOWLEDGE_INGEST_STATUS_FAILED,
)
from app.services.knowledge_constants import (
    KNOWLEDGE_INGEST_STATUS_SYNCING as KNOWLEDGE_INGEST_STATUS_SYNCING,
)
from app.services.knowledge_document_extractors import (
    _extract_document_text_from_path,
    _extract_docx_text,
    _extract_pdf_text,
    _extract_pptx_slides,
    _extract_text_with_ocr,
    _extract_xlsx_sheets,
    _normalize_extracted_text,
    _read_text_preview,
)
from app.services.knowledge_file_utils import (
    can_preview,
    extract_extension,
    format_size,
    format_time,
    normalize_filename,
    normalize_folder,
    parse_stored_name,
    resolve_file_type,
    resolve_file_type_label,
)
from app.services.knowledge_index_state import KnowledgeIndexStateMixin
from app.services.knowledge_ingest_status import (
    normalize_ingest_status_code,
)
from app.services.knowledge_onlyoffice import (
    build_onlyoffice_config as build_onlyoffice_config_payload,
)
from app.services.knowledge_onlyoffice import (
    build_onlyoffice_document_key,
    resolve_onlyoffice_document_type,
)
from app.services.knowledge_preview import build_preview
from app.services.knowledge_rag import KnowledgeRagService
from app.services.knowledge_tenant_scope import (
    PLATFORM_KNOWLEDGE_SCOPE,
    TENANT_KNOWLEDGE_SCOPE,
    KnowledgeStorageScope,
    migrate_legacy_library_to_platform,
    require_knowledge_tenant_id,
)

logger = get_logger("app.services.knowledge")


def prepare_knowledge_library() -> None:
    KnowledgeService(scope=PLATFORM_KNOWLEDGE_SCOPE).ensure_library_ready()


class KnowledgeService(KnowledgeIndexStateMixin):
    def __init__(
        self,
        storage_root: Path | None = None,
        db: Session | None = None,
        *,
        tenant_id: str | None = None,
        scope: str | None = None,
    ) -> None:
        settings = get_settings()
        self.db = db
        self.storage_root = Path(storage_root or settings.resolved_storage_root_dir)
        if scope == PLATFORM_KNOWLEDGE_SCOPE:
            self.storage_scope = KnowledgeStorageScope.platform(self.storage_root)
        elif scope in {None, TENANT_KNOWLEDGE_SCOPE} and tenant_id is not None:
            self.storage_scope = KnowledgeStorageScope.tenant(self.storage_root, tenant_id)
        else:
            raise ValueError("知识库操作必须显式提供可信 tenant_id 或 platform scope。")
        self.scope = self.storage_scope.scope
        self.tenant_id = self.storage_scope.tenant_id
        self.library_root = self.storage_scope.library_root
        self.index_path = self.storage_scope.index_path

    def ensure_library_ready(self) -> None:
        if self.scope == PLATFORM_KNOWLEDGE_SCOPE:
            migrate_legacy_library_to_platform(self.storage_scope)
        self.library_root.mkdir(parents=True, exist_ok=True)
        for folder_name in FIXED_KNOWLEDGE_FOLDERS:
            (self.library_root / folder_name).mkdir(parents=True, exist_ok=True)

        if not self.index_path.exists():
            self._save_index({"version": 1, "documents": []})

        index = self._load_index()
        if self._reconcile_index(index):
            self._save_index(index)

    def list_library(self) -> KnowledgeLibraryRead:
        documents = self._load_documents()
        if self.scope == TENANT_KNOWLEDGE_SCOPE:
            platform_documents = self._platform_service()._load_documents()
            tenant_ids = {item.id for item in documents}
            documents.extend(item for item in platform_documents if item.id not in tenant_ids)
            documents.sort(key=lambda item: item.time, reverse=True)
        folders = [
            KnowledgeFolderRead(
                name=folder_name,
                count=sum(1 for item in documents if item.folder == folder_name),
                icon="mdi mdi-folder",
            )
            for folder_name in FIXED_KNOWLEDGE_FOLDERS
        ]
        return KnowledgeLibraryRead(folders=folders, documents=documents)

    def get_document_detail(self, document_id: str) -> KnowledgeDocumentDetailRead:
        owner = self._document_owner(document_id)
        if owner is not self:
            return owner.get_document_detail(document_id)
        self.ensure_library_ready()
        index = self._load_index()
        if self._reconcile_document_ingest_statuses(index, document_ids=[document_id]):
            self._save_index(index)
        entry = self._require_entry(index, document_id)
        preview_kind, preview_pages = self._build_preview(entry)
        document = self._serialize_document(entry)
        return KnowledgeDocumentDetailRead(
            **document.model_dump(),
            previewKind=preview_kind,
            previewPages=preview_pages,
        )

    def upload_document(
        self,
        folder: str,
        filename: str,
        content: bytes,
        current_user: CurrentUserContext,
    ) -> KnowledgeDocumentDetailRead:
        if self.storage_scope.read_only:
            raise ValueError("平台知识文档为只读资源，不能上传或覆盖。")
        if require_knowledge_tenant_id(current_user.tenant_id) != self.tenant_id:
            raise ValueError("当前用户不能写入其他租户的知识库。")
        self.ensure_library_ready()
        normalized_folder = self._normalize_folder(folder)
        normalized_name = self._normalize_filename(filename)

        if not content:
            raise ValueError("上传文件不能为空。")

        rag_service = KnowledgeRagService(
            db=self.db,
            storage_root=self.storage_root,
            tenant_id=self.tenant_id,
            scope=self.scope,
        )
        index = self._load_index()
        existing_entry = next(
            (
                item
                for item in index["documents"]
                if item["folder"] == normalized_folder
                and item["original_name"].lower() == normalized_name.lower()
            ),
            None,
        )

        document_id = existing_entry["id"] if existing_entry else uuid4().hex
        stored_name = f"{document_id}__{normalized_name}"
        target_path = self.library_root / normalized_folder / stored_name

        if existing_entry is not None:
            rag_service.delete_document(document_id)
            if existing_entry["stored_name"] != stored_name:
                old_path = (
                    self.library_root / existing_entry["folder"] / existing_entry["stored_name"]
                )
                if old_path.exists():
                    old_path.unlink()

        target_path.write_bytes(content)

        now = datetime.now(UTC).isoformat()
        mime_type = mimetypes.guess_type(normalized_name)[0] or "application/octet-stream"
        checksum = hashlib.sha256(content).hexdigest()
        extension = self._extract_extension(normalized_name)

        if existing_entry is None:
            entry = {
                "id": document_id,
                "folder": normalized_folder,
                "original_name": normalized_name,
                "stored_name": stored_name,
                "mime_type": mime_type,
                "extension": extension,
                "size_bytes": len(content),
                "sha256": checksum,
                "created_at": now,
                "updated_at": now,
                "uploaded_by": current_user.name,
                "version_number": 1,
                "ingest_status": KNOWLEDGE_INGEST_STATUS_PUBLISHED,
                "ingest_status_updated_at": now,
                "ingest_completed_at": "",
                "ingest_document_name": "",
                "ingest_document_updated_at": "",
                "ingest_document_sha256": "",
                "ingest_agent_run_id": "",
            }
            index["documents"].append(entry)
            logger.info(
                "Knowledge document uploaded id=%s folder=%s filename=%s by=%s",
                document_id,
                normalized_folder,
                normalized_name,
                current_user.name,
            )
        else:
            existing_entry.update(
                {
                    "stored_name": stored_name,
                    "mime_type": mime_type,
                    "extension": extension,
                    "size_bytes": len(content),
                    "sha256": checksum,
                    "updated_at": now,
                    "uploaded_by": current_user.name,
                    "version_number": int(existing_entry.get("version_number", 1)) + 1,
                    "ingest_status": KNOWLEDGE_INGEST_STATUS_PUBLISHED,
                    "ingest_status_updated_at": now,
                    "ingest_completed_at": "",
                    "ingest_document_name": "",
                    "ingest_document_updated_at": "",
                    "ingest_document_sha256": "",
                    "ingest_agent_run_id": "",
                }
            )
            entry = existing_entry
            logger.info(
                "Knowledge document updated id=%s folder=%s filename=%s by=%s",
                document_id,
                normalized_folder,
                normalized_name,
                current_user.name,
            )

        self._save_index(index)
        return self.get_document_detail(document_id)

    def delete_document(self, document_id: str) -> None:
        if self.storage_scope.read_only:
            raise ValueError("平台知识文档为只读资源，不能删除。")
        self.ensure_library_ready()
        index = self._load_index()
        entry = self._require_entry(index, document_id)
        file_path = self._resolve_document_path(entry)
        if file_path.exists():
            file_path.unlink()

        index["documents"] = [item for item in index["documents"] if item["id"] != document_id]
        self._save_index(index)
        KnowledgeRagService(
            db=self.db,
            storage_root=self.storage_root,
            tenant_id=self.tenant_id,
            scope=self.scope,
        ).delete_document(document_id)
        logger.info(
            "Knowledge document deleted id=%s filename=%s", document_id, entry["original_name"]
        )

    def get_document_content(self, document_id: str) -> tuple[Path, str, str]:
        owner = self._document_owner(document_id)
        if owner is not self:
            return owner.get_document_content(document_id)
        self.ensure_library_ready()
        index = self._load_index()
        entry = self._require_entry(index, document_id)
        file_path = self._resolve_document_path(entry)

        if not file_path.exists():
            raise FileNotFoundError(entry["original_name"])

        return file_path, entry["mime_type"], entry["original_name"]

    def list_folder_documents(self, folder: str | None = None) -> list[dict[str, Any]]:
        self.ensure_library_ready()
        index = self._load_index()
        if self._reconcile_document_ingest_statuses(index):
            self._save_index(index)
        documents = list(index.get("documents") or [])
        if folder is None:
            return documents
        normalized_folder = self._normalize_folder(folder)
        return [item for item in documents if item.get("folder") == normalized_folder]

    def list_documents_for_ingest(
        self,
        *,
        folder: str | None = None,
        document_ids: list[str] | None = None,
        changed_only: bool = False,
    ) -> list[dict[str, Any]]:
        documents = self.list_folder_documents(folder=folder)
        requested_ids = {str(item).strip() for item in document_ids or [] if str(item).strip()}
        if requested_ids:
            documents = [
                item for item in documents if str(item.get("id") or "").strip() in requested_ids
            ]
        if changed_only:
            documents = [item for item in documents if self._should_index_document(item)]
        return documents

    def get_document_entry(self, document_id: str) -> dict[str, Any]:
        self.ensure_library_ready()
        index = self._load_index()
        if self._reconcile_document_ingest_statuses(index, document_ids=[document_id]):
            self._save_index(index)
        return dict(self._require_entry(index, document_id))

    def set_document_ingest_statuses(
        self,
        document_ids: list[str],
        status_code: int,
        *,
        agent_run_id: str | None = None,
    ) -> None:
        self.ensure_library_ready()
        normalized_ids = {str(item).strip() for item in document_ids if str(item).strip()}
        if not normalized_ids:
            return

        index = self._load_index()
        changed = False
        updated_at = datetime.now(UTC).isoformat()
        for entry in index.get("documents", []):
            if str(entry.get("id") or "").strip() not in normalized_ids:
                continue
            changed = (
                self._apply_ingest_status_to_entry(
                    entry,
                    status_code=status_code,
                    updated_at=updated_at,
                    agent_run_id=agent_run_id,
                )
                or changed
            )

        if changed:
            self._save_index(index)

    def refresh_document_ingest_statuses(
        self,
        document_ids: list[str] | None = None,
        *,
        preserve_syncing: bool = True,
    ) -> None:
        self.ensure_library_ready()
        index = self._load_index()
        if self._reconcile_document_ingest_statuses(
            index,
            document_ids=document_ids,
            preserve_syncing=preserve_syncing,
        ):
            self._save_index(index)

    def search_knowledge(
        self,
        query: str,
        *,
        conversation_history: list[dict[str, str]] | None = None,
        limit: int = KNOWLEDGE_SEARCH_RESULT_LIMIT,
    ) -> dict[str, Any]:
        self.ensure_library_ready()
        tenant_result = self._rag_service().query_knowledge(
            query,
            conversation_history=conversation_history,
            limit=limit,
        )
        if self.scope == PLATFORM_KNOWLEDGE_SCOPE:
            return tenant_result
        platform_service = self._platform_service()
        platform_service.ensure_library_ready()
        platform_result = platform_service._rag_service().query_knowledge(
            query,
            conversation_history=conversation_history,
            limit=limit,
        )
        return self._merge_scoped_search_results(
            tenant_result,
            platform_result,
            limit=limit,
        )

    def extract_document_text(self, document_id: str) -> str:
        self.ensure_library_ready()
        entry = self.get_document_entry(document_id)
        file_path = self._resolve_document_path(entry)
        if not file_path.exists():
            raise FileNotFoundError(entry["original_name"])
        return self._extract_document_text_from_path(
            file_path=file_path,
            original_name=str(entry.get("original_name") or file_path.name),
            mime_type=str(entry.get("mime_type") or "application/octet-stream"),
        )

    def build_onlyoffice_config(
        self,
        document_id: str,
        current_user: CurrentUserContext,
        *,
        editable: bool = False,
    ) -> KnowledgeOnlyOfficeConfigRead:
        if self.db is None:
            raise ValueError("ONLYOFFICE 安全会话需要数据库连接。")
        owner = self._document_owner(document_id)
        owner.ensure_library_ready()
        index = owner._load_index()
        entry = owner._require_entry(index, document_id)
        return build_onlyoffice_config_payload(
            db=self.db,
            document_id=document_id,
            entry=entry,
            current_user=current_user,
            resource_scope=owner.scope,
            tenant_id=owner.tenant_id,
            editable=editable,
        )

    def _load_documents(self) -> list[KnowledgeDocumentRead]:
        self.ensure_library_ready()
        index = self._load_index()
        changed = self._reconcile_index(index)
        changed = self._reconcile_document_ingest_statuses(index) or changed
        if changed:
            self._save_index(index)

        documents = [self._serialize_document(entry) for entry in index["documents"]]
        return sorted(documents, key=lambda item: item.time, reverse=True)

    def _serialize_document(
        self,
        entry: dict[str, Any],
    ) -> KnowledgeDocumentRead:
        extension = entry.get("extension") or self._extract_extension(entry["original_name"])
        file_type = self._resolve_file_type(extension)
        size_bytes = int(entry.get("size_bytes") or 0)
        updated_at = self._format_time(entry.get("updated_at") or entry.get("created_at"))
        ingest_time = self._format_time(entry.get("ingest_completed_at"))
        state_code = normalize_ingest_status_code(entry.get("ingest_status"))
        state_label, state_tone = KNOWLEDGE_INGEST_STATUS_META.get(
            state_code,
            KNOWLEDGE_INGEST_STATUS_META[KNOWLEDGE_INGEST_STATUS_PUBLISHED],
        )

        return KnowledgeDocumentRead(
            id=entry["id"],
            scope=self.scope,
            readOnly=self.storage_scope.read_only,
            name=entry["original_name"],
            folder=entry["folder"],
            tag=f"{entry['folder']} / {extension.upper() or 'FILE'}",
            time=updated_at,
            ingestTime=ingest_time if state_code == KNOWLEDGE_INGEST_STATUS_INGESTED else "",
            version=f"v{int(entry.get('version_number', 1))}.0",
            stateCode=state_code,
            state=state_label,
            stateTone=state_tone,
            owner=entry.get("uploaded_by") or "????",
            icon=ICON_BY_TYPE.get(file_type, ICON_BY_TYPE["binary"]),
            fileType=file_type,
            fileTypeLabel=self._resolve_file_type_label(file_type),
            summary=(
                f"{entry['folder']} ? {extension.upper() or 'FILE'} ? "
                f"{self._format_size(size_bytes)}"
            ),
            mimeType=entry.get("mime_type") or "application/octet-stream",
            extension=extension,
            sizeBytes=size_bytes,
            canPreview=self._can_preview(extension),
            llmWikiAvailable=False,
            llmWikiQualityStatus="",
            llmWikiQualityNote="",
        )

    def _build_preview(self, entry: dict[str, Any]) -> tuple[str, list[KnowledgePreviewPageRead]]:
        return build_preview(entry, resolve_document_path=self._resolve_document_path)

    def _rag_service(self) -> KnowledgeRagService:
        return KnowledgeRagService(
            db=self.db,
            storage_root=self.storage_root,
            tenant_id=self.tenant_id,
            scope=self.scope,
        )

    def _platform_service(self) -> KnowledgeService:
        return KnowledgeService(
            storage_root=self.storage_root,
            db=self.db,
            scope=PLATFORM_KNOWLEDGE_SCOPE,
        )

    def _document_owner(self, document_id: str) -> KnowledgeService:
        self.ensure_library_ready()
        index = self._load_index()
        try:
            self._require_entry(index, document_id)
            return self
        except FileNotFoundError:
            if self.scope == PLATFORM_KNOWLEDGE_SCOPE:
                raise
        platform_service = self._platform_service()
        platform_service.ensure_library_ready()
        platform_service._require_entry(platform_service._load_index(), document_id)
        return platform_service

    @staticmethod
    def _merge_scoped_search_results(
        tenant_result: dict[str, Any],
        platform_result: dict[str, Any],
        *,
        limit: int,
    ) -> dict[str, Any]:
        merged_hits: list[dict[str, Any]] = []
        seen_codes: set[str] = set()
        for scope, payload in (
            (TENANT_KNOWLEDGE_SCOPE, tenant_result),
            (PLATFORM_KNOWLEDGE_SCOPE, platform_result),
        ):
            for raw_hit in list(payload.get("hits") or []):
                if not isinstance(raw_hit, dict):
                    continue
                code = str(raw_hit.get("code") or "").strip()
                if code and code in seen_codes:
                    continue
                if code:
                    seen_codes.add(code)
                merged_hits.append(
                    {
                        **raw_hit,
                        "scope": scope,
                        "read_only": scope == PLATFORM_KNOWLEDGE_SCOPE,
                    }
                )
        merged_hits.sort(key=lambda item: int(item.get("score") or 0), reverse=True)
        merged_hits = merged_hits[: max(1, limit)]
        references = [
            str(item.get("code") or "").strip()
            for item in merged_hits
            if str(item.get("code") or "").strip()
        ]
        return {
            "result_type": "knowledge_search",
            "query": str(tenant_result.get("query") or platform_result.get("query") or ""),
            "record_count": len(merged_hits),
            "hits": merged_hits,
            "references": references,
            "raw_references": list(tenant_result.get("raw_references") or [])
            + list(platform_result.get("raw_references") or []),
            "metadata": {
                "retrieval_strategy": "tenant_platform_isolated_fusion",
                "tenant_record_count": int(tenant_result.get("record_count") or 0),
                "platform_record_count": int(platform_result.get("record_count") or 0),
            },
            "message": (
                f"已从当前租户与平台只读知识库中联合检索到 {len(merged_hits)} 条相关内容。"
                if merged_hits
                else "当前租户与平台知识库中没有检索到直接匹配的内容。"
            ),
        }

    def _require_entry(self, index: dict[str, Any], document_id: str) -> dict[str, Any]:
        for entry in index["documents"]:
            if entry["id"] == document_id:
                return entry
        raise FileNotFoundError(document_id)

    def _resolve_document_path(self, entry: dict[str, Any]) -> Path:
        folder = self._normalize_folder(str(entry.get("folder") or ""))
        stored_name = str(entry.get("stored_name") or "").strip()
        if not stored_name or Path(stored_name).name != stored_name:
            raise ValueError("知识文档存储路径不合法。")
        folder_root = (self.library_root / folder).resolve()
        candidate = (folder_root / stored_name).resolve()
        if not candidate.is_relative_to(folder_root):
            raise ValueError("知识文档存储路径越界。")
        return candidate

    def replace_document_content_from_onlyoffice(
        self,
        document_id: str,
        content: bytes,
        *,
        actor_name: str,
        expected_document_key: str,
        expected_version: int,
    ) -> KnowledgeDocumentDetailRead:
        if self.storage_scope.read_only or self.tenant_id is None:
            raise ValueError("平台知识文档为只读资源，不能回写。")
        index = self._load_index()
        entry = self._require_entry(index, document_id)
        if (
            build_onlyoffice_document_key(entry) != expected_document_key
            or int(entry.get("version_number") or 1) != expected_version
        ):
            raise ValueError("ONLYOFFICE 编辑基线已过期，请重新打开文档后再编辑。")
        current_user = CurrentUserContext(
            username="onlyoffice",
            name=actor_name or "ONLYOFFICE",
            role_codes=["manager"],
            is_admin=True,
            tenant_id=self.tenant_id,
        )
        return self.upload_document(
            folder=entry["folder"],
            filename=entry["original_name"],
            content=content,
            current_user=current_user,
        )

    _build_onlyoffice_document_key = staticmethod(build_onlyoffice_document_key)
    _resolve_onlyoffice_document_type = staticmethod(resolve_onlyoffice_document_type)

    _normalize_filename = staticmethod(normalize_filename)
    _normalize_folder = staticmethod(normalize_folder)
    _extract_extension = staticmethod(extract_extension)
    _parse_stored_name = staticmethod(parse_stored_name)
    _format_time = staticmethod(format_time)
    _format_size = staticmethod(format_size)
    _resolve_file_type = staticmethod(resolve_file_type)
    _resolve_file_type_label = staticmethod(resolve_file_type_label)
    _can_preview = staticmethod(can_preview)
    _read_text_preview = staticmethod(_read_text_preview)
    _extract_docx_text = staticmethod(_extract_docx_text)
    _normalize_extracted_text = staticmethod(_normalize_extracted_text)
    _extract_pdf_text = staticmethod(_extract_pdf_text)
    _extract_text_with_ocr = staticmethod(_extract_text_with_ocr)
    _extract_xlsx_sheets = staticmethod(_extract_xlsx_sheets)
    _extract_pptx_slides = staticmethod(_extract_pptx_slides)

    def _extract_document_text_from_path(
        self,
        *,
        file_path: Path,
        original_name: str,
        mime_type: str,
    ) -> str:
        return _extract_document_text_from_path(
            file_path=file_path,
            original_name=original_name,
            mime_type=mime_type,
        )
