"""上传文件暂存、类型识别和安全落盘。"""

from __future__ import annotations

import hashlib
import uuid
import zipfile
from dataclasses import dataclass
from pathlib import Path, PurePosixPath

import olefile
from flask import current_app
from werkzeug.datastructures import FileStorage

from dms.common.errors import PayloadTooLargeError, UnsupportedFileTypeError
from dms.storage.paths import ensure_storage_directories

ALLOWED_EXTENSIONS = {"doc", "docx", "pdf", "xls", "xlsx"}
MIME_TYPES = {
    "doc": "application/msword",
    "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "pdf": "application/pdf",
    "xls": "application/vnd.ms-excel",
    "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}
OLE_HEADER = bytes.fromhex("D0CF11E0A1B11AE1")


@dataclass(frozen=True, slots=True)
class StagedUpload:
    temporary_path: Path
    final_path: Path
    relative_path: str
    original_file_name: str
    extension: str
    mime_type: str
    file_size: int
    file_hash: str

    def cleanup(self) -> None:
        self.temporary_path.unlink(missing_ok=True)

    def cleanup_all(self) -> None:
        self.temporary_path.unlink(missing_ok=True)
        self.final_path.unlink(missing_ok=True)

    def promote(self) -> None:
        self.final_path.parent.mkdir(parents=True, exist_ok=True)
        self.temporary_path.replace(self.final_path)


def _safe_zip(path: Path, required: set[str]) -> None:
    try:
        with zipfile.ZipFile(path) as archive:
            if len(archive.infolist()) > 10000:
                raise UnsupportedFileTypeError("Office容器条目数量异常")
            uncompressed_size = sum(item.file_size for item in archive.infolist())
            if uncompressed_size > current_app.config["DMS_MAX_FILE_SIZE"] * 20:
                raise UnsupportedFileTypeError("Office容器解压规模异常")
            names = set(archive.namelist())
            for name in names:
                normalized = PurePosixPath(name.replace("\\", "/"))
                if normalized.is_absolute() or ".." in normalized.parts:
                    raise UnsupportedFileTypeError("Office容器包含不安全路径")
            if not required <= names:
                raise UnsupportedFileTypeError("Office容器结构与扩展名不一致")
            if archive.testzip() is not None:
                raise UnsupportedFileTypeError("Office ZIP容器损坏")
    except (zipfile.BadZipFile, OSError, RuntimeError) as exc:
        raise UnsupportedFileTypeError("Office ZIP容器损坏") from exc


def _validate(path: Path, extension: str) -> None:
    prefix = path.read_bytes()[:8]
    if extension == "pdf":
        if not prefix.startswith(b"%PDF-"):
            raise UnsupportedFileTypeError("PDF文件头与扩展名不一致")
        return
    if extension == "docx":
        _safe_zip(path, {"[Content_Types].xml", "word/document.xml"})
        return
    if extension == "xlsx":
        _safe_zip(path, {"[Content_Types].xml", "xl/workbook.xml"})
        return
    if not prefix.startswith(OLE_HEADER):
        raise UnsupportedFileTypeError("OLE文件头与扩展名不一致")
    try:
        with olefile.OleFileIO(path) as container:
            streams = {"/".join(parts) for parts in container.listdir()}
    except (OSError, IOError) as exc:
        raise UnsupportedFileTypeError("OLE/CFB容器损坏") from exc
    if extension == "doc" and "WordDocument" not in streams:
        raise UnsupportedFileTypeError("OLE容器不是有效DOC")
    if extension == "xls" and not ({"Workbook", "Book"} & streams):
        raise UnsupportedFileTypeError("OLE容器不是有效XLS")


def stage_upload(file: FileStorage) -> StagedUpload:
    original_name = Path(file.filename or "").name
    if not original_name or "." not in original_name:
        raise UnsupportedFileTypeError("文件名缺少允许的扩展名")
    extension = original_name.rsplit(".", 1)[1].lower()
    if extension not in ALLOWED_EXTENSIONS:
        raise UnsupportedFileTypeError()
    root = ensure_storage_directories(current_app.config["DMS_STORAGE_ROOT"])
    temporary = root / "temporary" / f"{uuid.uuid4()}.upload"
    final_name = f"{uuid.uuid4()}.{extension}"
    final = root / "original" / final_name
    digest = hashlib.sha256()
    size = 0
    try:
        with temporary.open("xb") as stream:
            while chunk := file.stream.read(1024 * 1024):
                size += len(chunk)
                if size > current_app.config["DMS_MAX_FILE_SIZE"]:
                    raise PayloadTooLargeError("单个文件超过允许大小")
                digest.update(chunk)
                stream.write(chunk)
        if size == 0:
            raise UnsupportedFileTypeError("不允许上传空文件")
        _validate(temporary, extension)
        return StagedUpload(
            temporary_path=temporary,
            final_path=final,
            relative_path=f"original/{final_name}",
            original_file_name=original_name,
            extension=extension,
            mime_type=MIME_TYPES[extension],
            file_size=size,
            file_hash=digest.hexdigest(),
        )
    except Exception:
        temporary.unlink(missing_ok=True)
        raise
