"""统一预览服务：PDF直接返回；DOC/DOCX按需转换为PDF并缓存。"""

from __future__ import annotations

import logging
import os
import tempfile
from pathlib import Path

import fitz
from flask import current_app

from dms.common.enums import ContentExtractStatus
from dms.common.errors import (
    PreviewConversionFailedError,
    PreviewConversionTimeoutError,
    PreviewConverterUnavailableError,
    PreviewUnavailableError,
)
from dms.services.document_converter import DocumentConverter, create_converter
from dms.storage.paths import (
    UnsafeStoragePathError,
    is_path_inside_preview,
    preview_cache_path,
    resolve_storage_path,
)

logger = logging.getLogger(__name__)

_CONVERTIBLE_EXTENSIONS = {"doc", "docx"}


def _get_converter() -> DocumentConverter:
    """从 Flask 应用配置获取当前转换器。"""
    config = current_app.config
    # 应用初始化时若已挂载转换器实例则优先复用，否则动态创建
    converter = current_app.extensions.get("dms_document_converter")
    if converter is None:
        converter = create_converter(config)
        current_app.extensions["dms_document_converter"] = converter
    return converter


class PreviewService:
    """文档预览服务：管理 PDF 直接预览与 Office 转换后预览。"""

    def __init__(self, storage_root: Path, converter: DocumentConverter) -> None:
        self.storage_root = storage_root
        self.converter = converter

    @classmethod
    def current(cls) -> "PreviewService":
        root = Path(current_app.config["DMS_STORAGE_ROOT"]).expanduser().resolve()
        return cls(root, _get_converter())

    def preview_path_for(self, document) -> Path:
        """返回最终用于发送文件的磁盘路径。

        - PDF: 原始文件路径
        - DOC/DOCX: 缓存的 PDF 路径（按需转换）
        - 其他: 直接抛出 PreviewUnavailableError
        """
        extension = document.file_extension.lower().lstrip(".")
        original = resolve_storage_path(
            document.file_relative_path,
            self.storage_root,
        )

        if extension == "pdf":
            return original

        if extension not in _CONVERTIBLE_EXTENSIONS:
            raise PreviewUnavailableError()

        return self._convertible_preview_path(
            original, document.file_hash, extension
        )

    def _convertible_preview_path(
        self, source_path: Path, file_hash: str, extension: str
    ) -> Path:
        if not self.converter.available:
            raise PreviewConverterUnavailableError()

        cache = preview_cache_path(file_hash, self.storage_root)
        if self.validate_cached_pdf(cache):
            return cache

        try:
            pdf_bytes = self.converter.convert_to_pdf(source_path, file_hash)
        except (PreviewConverterUnavailableError, PreviewConversionTimeoutError):
            raise
        except Exception as exc:
            logger.exception("文档转换失败")
            raise PreviewConversionFailedError() from exc

        if not pdf_bytes or not pdf_bytes.startswith(b"%PDF-"):
            raise PreviewConversionFailedError()

        # 原子写入：先写临时文件再重命名
        cache.parent.mkdir(parents=True, exist_ok=True)
        temp_file: Path | None = None
        try:
            with tempfile.NamedTemporaryFile(
                dir=cache.parent,
                prefix=f".{file_hash}.",
                suffix=".tmp",
                delete=False,
            ) as temp_stream:
                temp_stream.write(pdf_bytes)
                temp_file = Path(temp_stream.name)
            os.replace(temp_file, cache)
        except OSError as exc:
            if temp_file is not None:
                temp_file.unlink(missing_ok=True)
            logger.exception("写入预览缓存失败")
            raise PreviewConversionFailedError() from exc

        return cache

    def validate_cached_pdf(self, cache_path: Path) -> bool:
        """验证缓存文件是否为有效 PDF 且位于 preview 目录内。"""
        try:
            if not is_path_inside_preview(cache_path, self.storage_root):
                return False
            if not cache_path.is_file() or cache_path.stat().st_size == 0:
                return False
            header = cache_path.read_bytes()[:8]
            if not header.startswith(b"%PDF-"):
                return False
            with fitz.open(stream=cache_path.read_bytes(), filetype="pdf") as _:
                return True
        except Exception:
            return False


def extract_doc_content_via_pdf(
    source_path: Path,
    converter: DocumentConverter,
    file_hash: str,
) -> tuple[str | None, str]:
    """将 DOC 转换为 PDF 后提取正文。

    返回 (text, status)。status 为 SUCCESS、EMPTY 或 FAILED。
    """
    from dms.services.document_content_service import _pdf_text

    if not converter.available:
        return None, ContentExtractStatus.FAILED.value

    try:
        pdf_bytes = converter.convert_to_pdf(source_path, file_hash)
    except Exception:
        logger.exception("DOC 转 PDF 提取正文失败")
        return None, ContentExtractStatus.FAILED.value

    if not pdf_bytes or not pdf_bytes.startswith(b"%PDF-"):
        return None, ContentExtractStatus.FAILED.value

    temp_path: Path | None = None
    try:
        with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as temp_file:
            temp_file.write(pdf_bytes)
            temp_path = Path(temp_file.name)
        text = _pdf_text(temp_path)
    except Exception:
        logger.exception("DOC 转换后的 PDF 提取正文失败")
        return None, ContentExtractStatus.FAILED.value
    finally:
        if temp_path is not None:
            temp_path.unlink(missing_ok=True)

    return text or None, (
        ContentExtractStatus.SUCCESS.value if text
        else ContentExtractStatus.EMPTY.value
    )


__all__ = [
    "PreviewService",
    "extract_doc_content_via_pdf",
]
