# 本地存储基类 - 约定存储位置与规则，子类继承后按约束执行

from abc import ABC, abstractmethod
import re
import threading
from pathlib import Path
from typing import Optional, Union

from pymongo.collection import Collection
from pymongo.results import InsertOneResult, UpdateResult

from .metadata_storage import MetadataStore

DOCUMENTS_DIR = "/data/documents"

# 默认允许的相对路径：字母、数字、下划线、短横线、点、斜杠，禁止 ..
_RELATIVE_PATH_PATTERN = re.compile(r"^[a-zA-Z0-9_.\-/]+$")


class StorageBase(ABC):
    """
    本地存储的顶级基类。

    约定：
    1. 存储位置：根路径为 DOCUMENTS_DIR，子类通过 storage_subdir 指定子目录。
    2. 存储规则：所有路径均为相对于「根路径/storage_subdir」的相对路径，禁止路径穿越（..）、绝对路径及非法字符。
    3. 子类继承本类后，必须通过 self._resolve(relative_path) 解析路径，不得绕过约束直接写文件。
    4. 子类必须实现 storage_subdir，指定存储子目录名。
    5. 基类中实例化元数据集合 METADATA_COLLECTION，子类可通过 metadata_collection 或 _metadata_repo 访问。
    """

    @property
    @abstractmethod
    def storage_subdir(self) -> str:
        """子类必须实现：指定存储子目录名（不可为空）。"""
        ...

    def __init__(
        self,
        *,
        mongodb_url: str,
        auth_source: Optional[str] = None,
    ) -> None:
        self._root = self._init_root()
        self._metadata_repo = MetadataStore(
            mongodb_url=mongodb_url,
            auth_source=auth_source,
        )

    @property
    def metadata_collection(self) -> Collection:
        """元数据集合（METADATA_COLLECTION）实例，用于文档元数据的增删改查。"""
        return self._metadata_repo.collection

    def _init_root(self) -> Path:
        """根据配置与 storage_subdir 初始化存储根路径。"""
        if not (self.storage_subdir and self.storage_subdir.strip()):
            raise ValueError("子类必须指定非空的 storage_subdir")
        root = Path(DOCUMENTS_DIR).resolve()
        root = root / self.storage_subdir.strip()
        root.mkdir(parents=True, exist_ok=True)
        return root

    def _resolve(self, relative_path: str) -> Path:
        """
        将相对路径解析为绝对路径，并校验存储规则。

        规则：
        - 路径必须相对于当前存储根（_root），不得包含 .. 或绝对路径。
        - 仅允许字母、数字、下划线、短横线、点、斜杠（子类可覆盖 _check_relative_path 放宽或收紧）。

        :param relative_path: 相对于存储根的路径，如 "20260228_013509_pubmed/main.py"
        :return: 解析后的绝对路径
        :raises ValueError: 路径非法（穿越根目录或不符合命名规则）
        """
        relative_path = relative_path.replace("\\", "/").strip().lstrip("/")
        self._check_relative_path(relative_path)
        full = (self._root / relative_path).resolve()
        try:
            full.relative_to(self._root.resolve())
        except ValueError:
            raise ValueError("路径不允许超出存储根目录") from None
        return full

    def _check_relative_path(self, relative_path: str) -> None:
        """
        校验相对路径是否符合存储规则。子类可覆盖以自定义规则。

        默认：不允许空、不允许 ".."、不允许绝对路径形态、仅允许安全字符。
        """
        if not relative_path:
            raise ValueError("相对路径不能为空")
        if ".." in relative_path:
            raise ValueError("相对路径不允许包含 ..")
        if Path(relative_path).is_absolute() or relative_path.startswith("/"):
            raise ValueError("相对路径不能为绝对路径")
        if not _RELATIVE_PATH_PATTERN.match(relative_path):
            raise ValueError("相对路径仅允许字母、数字、下划线、短横线、点和斜杠")

    def get_root(self) -> Path:
        """返回当前存储根目录的绝对路径。"""
        return self._root

    def resolve_path(self, relative_path: str) -> Path:
        """
        公开方法：解析相对路径为绝对路径（遵守存储规则）。
        子类在实现读写时应使用此方法或 _resolve 获取目标路径。
        """
        return self._resolve(relative_path)

    def write_metadata(
        self,
        *,
        relative_path: str,
        size: Optional[int] = None,
        content_hash: Optional[str] = None,
        md5: Optional[str] = None,
        phash: Optional[str] = None,
        mime_type: Optional[str] = None,
        status: Optional[str] = None,
        metadata: Optional[dict[str, object]] = None,
    ) -> Union[InsertOneResult, UpdateResult]:
        """
        写入一条文件元数据（按 storage_subdir + relative_path upsert）。
        ticket_id 与 task_id 从当前线程上下文中获取，缺失则抛出异常。
        """
        thread = threading.current_thread()
        ticket_id = getattr(thread, "ticket_id", None)
        task_id = getattr(thread, "task_id", None)
        if (not ticket_id or not str(ticket_id).strip()) and (
            thread.name and thread.name.startswith("collection-")
        ):
            fallback_id = thread.name.split("collection-", 1)[1].strip()
            if fallback_id:
                ticket_id = fallback_id
        if not task_id or not str(task_id).strip():
            task_id = ticket_id
        if not ticket_id or not str(ticket_id).strip():
            raise ValueError("线程上下文缺少 ticket_id")
        if not task_id or not str(task_id).strip():
            raise ValueError("线程上下文缺少 task_id")

        normalized = relative_path.replace("\\", "/").strip().lstrip("/")
        self._check_relative_path(normalized)
        return self._metadata_repo.save(
            storage_subdir=self.storage_subdir,
            relative_path=normalized,
            size=size,
            content_hash=content_hash,
            md5=md5,
            phash=phash,
            mime_type=mime_type,
            ticket_id=ticket_id,
            task_id=task_id,
            status=status,
            metadata=metadata,
        )

    def _ensure_metadata_exists(self, relative_path: str) -> None:
        """
        写入文件前校验元数据是否存在，不存在则抛异常。
        """
        normalized = relative_path.replace("\\", "/").strip().lstrip("/")
        self._check_relative_path(normalized)
        doc = self._metadata_repo.find_by_path(self.storage_subdir, normalized)
        if not doc:
            raise ValueError(f"未找到对应元数据，禁止写入文件: {normalized}")

    def write(self, relative_path: str, data: bytes) -> Path:
        """
        将数据写入相对路径。
        基类默认在写入前校验对应元数据已存在，再委托给子类实现实际写入。

        :param relative_path: 相对于存储根的路径
        :param data: 字节内容
        :return: 写入后的绝对路径
        """
        self._ensure_metadata_exists(relative_path)
        return self._write_impl(relative_path, data)

    @abstractmethod
    def _write_impl(self, relative_path: str, data: bytes) -> Path:
        """子类实现：执行实际写入逻辑（无需重复元数据存在校验）。"""
        ...

    def exists(self, relative_path: str) -> bool:
        """判断相对路径对应的文件或目录是否存在。子类可直接使用，也可覆盖。"""
        try:
            return self._resolve(relative_path).exists()
        except ValueError:
            return False
