# 本地存储基类 - 单入口 write + 内部编排

from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Optional

from pymongo.collection import Collection

from .internal.hash_utils import calculate_md5
from .internal.path_policy import (
    normalize_relative_path,
    resolve_under_root,
    validate_relative_path,
)
from .metadata_storage import (
    KEY_MD5,
    MetadataStore,
    infer_file_type_from_relative_path,
)

DOCUMENTS_DIR = "/data/documents"

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

    约定：
    1. 存储位置：根路径为 DOCUMENTS_DIR，子类通过 storage_subdir 指定子目录。
    2. 存储规则：所有路径均为相对于「根路径/storage_subdir」的相对路径，禁止路径穿越（..）、绝对路径及非法字符。
    3. 公开入口为 write，路径校验、元数据与完整性字段由基类自动编排。
    4. 子类必须实现 storage_subdir，指定存储子目录名。
    5. 基类中实例化元数据集合 METADATA_COLLECTION，子类可通过 metadata_collection 或 _metadata_repo 访问。
    """

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

    def __init__(
        self,
        *,
        auth_source: Optional[str] = None,
    ) -> None:
        self._root = self._init_root()
        self._metadata_repo = MetadataStore(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) -> tuple[Path, str]:
        """
        将相对路径解析为绝对路径，并校验存储规则。

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

        :param relative_path: 相对于存储根的路径，如 "20260228_013509_pubmed/main.py"
        :return: (解析后的绝对路径, 校验后的规范相对路径)
        :raises ValueError: 路径非法（穿越根目录或不符合命名规则）
        """
        normalized = normalize_relative_path(relative_path)
        self._check_relative_path(normalized)
        resolved = resolve_under_root(self._root, normalized)
        return resolved, normalized

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

        默认：不允许空、不允许 ".."、不允许绝对路径形态、仅允许安全字符。
        """
        validate_relative_path(relative_path)

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

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

    def write(
        self,
        relative_path: str,
        data: bytes,
        *,
        title: Optional[str] = None,
        file_type: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
    ) -> bool:
        """
        统一写入入口：自动处理路径校验、md5 计算、元数据同步与幂等判断。

        :param relative_path: 相对于存储根的路径
        :param data: 字节内容
        :param title: 可选；供前端展示的文档标题
        :param file_type: 可选；小写扩展名（如 pdf、txt）；未提供时从路径推断（须在允许列表内）
        :return: 写入是否成功
        """
        # 解析路径：同时获取绝对路径与校验后的规范相对路径
        target, normalized = self._resolve(relative_path)
        effective_file_type = file_type
        if not (effective_file_type and str(effective_file_type).strip()):
            effective_file_type = infer_file_type_from_relative_path(normalized)
        # 查询元数据
        doc = self._metadata_repo.find_by_path(self.storage_subdir, normalized)
        # 计算md5
        md5 = calculate_md5(data)
        # 幂等判断
        if doc and doc.get(KEY_MD5) == md5 and target.exists():
            return True

        size = len(data)
        composed_metadata = dict(metadata or {})
        try:
            written = self._write_impl(normalized, data)
        except Exception:
            # 写入失败不抛异常，避免上层线程被异常打断
            return False
        status = "success" if written else "failed"
        self._metadata_repo.save(
            storage_subdir=self.storage_subdir,
            relative_path=normalized,
            title=title,
            size=size,
            md5=md5,
            file_type=effective_file_type,
            status=status,
            metadata=composed_metadata or None,
        )
        return written

    @abstractmethod
    def _write_impl(self, relative_path: str, data: bytes) -> bool:
        """子类实现：执行实际写入逻辑并返回成功/失败。"""
        ...

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