# 元数据存储 - 定义 doc_metadata 表结构、存储与更新方法

from datetime import datetime, timezone
from enum import Enum
from typing import Any, Optional, Union
import os
from pymongo.collection import Collection
from pymongo.cursor import Cursor
from pymongo.results import DeleteResult, InsertOneResult, UpdateResult

from .mongo_repository_base import MongoRepositoryBase

# ---------------------------------------------------------------------------
# 集合名与表结构：字段名常量与说明
# ---------------------------------------------------------------------------

# 数据库名称
DATABASE_NAME = "nutricare"

# 元数据集合名（MongoDB collection）
METADATA_COLLECTION = "doc_metadata"

# 唯一键：存储子目录 + 相对路径，用于 upsert
KEY_STORAGE_SUBDIR = "storage_subdir"
KEY_RELATIVE_PATH = "relative_path"

# 文档字段
KEY_TICKET_ID = "ticket_id"
KEY_TASK_ID = "task_id"
# 此字段用于给用户展示文件标题  如果存在
KEY_TITLE = "title"
KEY_SIZE = "size"
KEY_CONTENT_HASH = "content_hash"
KEY_MD5 = "md5"
KEY_PHASH = "phash"
KEY_MIME_TYPE = "mime_type"
KEY_STATUS = "status"
KEY_METADATA = "metadata"
KEY_CREATED_AT = "created_at"
KEY_UPDATED_AT = "updated_at"


class MimeType(str, Enum):
    """受支持的文件类型枚举（按扩展名存储）。"""

    PDF = "pdf"
    DOC = "doc"
    DOCX = "docx"
    MD = "md"
    TXT = "txt"
    XLS = "xls"
    XLSX = "xlsx"
    PPT = "ppt"
    PPTX = "pptx"


_ALLOWED_MIME_TYPES = {t.value for t in MimeType}


def _normalize_and_validate_mime_type(mime_type: Optional[str]) -> Optional[str]:
    """
    规范化并校验 mime_type，只允许枚举中的类型；
    入参可以大小写混合，内部统一为小写扩展名。
    """
    if mime_type is None:
        return None
    value = str(mime_type).strip().lower()
    if not value:
        return None
    if value not in _ALLOWED_MIME_TYPES:
        raise ValueError(f"不支持的文件类型: {mime_type!r}，仅支持: {sorted(_ALLOWED_MIME_TYPES)}")
    return value

# 建议唯一索引：(storage_subdir, relative_path)
INDEX_KEYS = [KEY_STORAGE_SUBDIR, KEY_RELATIVE_PATH]


def _now_utc() -> datetime:
    """当前 UTC 时间。"""
    return datetime.now(timezone.utc)


def make_document(
    *,
    storage_subdir: str,
    relative_path: str,
    title: Optional[str] = None,
    size: Optional[int] = None,
    content_hash: Optional[str] = None,
    md5: Optional[str] = None,
    phash: Optional[str] = None,
    mime_type: Optional[str] = None,
    ticket_id: Optional[str] = None,
    task_id: Optional[str] = None,
    status: Optional[str] = None,
    metadata: Optional[dict[str, Any]] = None,
    created_at: Optional[datetime] = None,
    updated_at: Optional[datetime] = None,
) -> dict[str, Any]:
    """
    构造符合元数据表结构的文档（不包含 _id）。
    用于存储前校验与统一时间戳。
    """
    now = _now_utc()
    doc: dict[str, Any] = {
        KEY_STORAGE_SUBDIR: storage_subdir.strip(),
        KEY_RELATIVE_PATH: relative_path.replace("\\", "/").strip().lstrip("/"),
        KEY_CREATED_AT: created_at or now,
        KEY_UPDATED_AT: updated_at or now,
    }
    if title is not None and str(title).strip():
        doc[KEY_TITLE] = title.strip()
    if size is not None:
        doc[KEY_SIZE] = size
    if content_hash is not None and str(content_hash).strip():
        doc[KEY_CONTENT_HASH] = content_hash.strip()
    if md5 is not None and str(md5).strip():
        doc[KEY_MD5] = md5.strip()
    if phash is not None and str(phash).strip():
        doc[KEY_PHASH] = phash.strip()
    normalized_mime_type = _normalize_and_validate_mime_type(mime_type)
    if normalized_mime_type is not None:
        doc[KEY_MIME_TYPE] = normalized_mime_type
    if ticket_id is not None and str(ticket_id).strip():
        doc[KEY_TICKET_ID] = ticket_id.strip()
    if task_id is not None and str(task_id).strip():
        doc[KEY_TASK_ID] = task_id.strip()
    if status is not None and str(status).strip():
        doc[KEY_STATUS] = status.strip()
    if metadata is not None and isinstance(metadata, dict):
        doc[KEY_METADATA] = metadata
    return doc


def make_filter(storage_subdir: str, relative_path: str) -> dict[str, Any]:
    """按存储子目录 + 相对路径构造查询条件。"""
    return {
        KEY_STORAGE_SUBDIR: storage_subdir.strip(),
        KEY_RELATIVE_PATH: relative_path.replace("\\", "/").strip().lstrip("/"),
    }


def make_md5_filter(storage_subdir: str, relative_path: str, md5: str) -> dict[str, Any]:
    """按存储子目录、相对路径和 md5 构造查询条件。"""
    flt = make_filter(storage_subdir, relative_path)
    flt[KEY_MD5] = md5.strip()
    return flt


class MetadataStore:
    """
    元数据存储：基于 METADATA_COLLECTION 的增删改查封装。
    通过连接参数在内部实例化 MongoRepositoryBase。
    """

    def __init__(
        self,
        *,
        mongodb_url: Optional[str] = None,
        auth_source: Optional[str] = None,
    ) -> None:
        if mongodb_url is None:
            mongodb_url = os.environ.get("mongodb_url") or os.environ.get("MONGODB_URL")
        if not mongodb_url or not str(mongodb_url).strip():
            raise ValueError("mongodb_url 不能为空，请通过环境变量 mongodb_url 提供")
        self._repo = MongoRepositoryBase(
            mongodb_url=mongodb_url.strip(),
            db_name=DATABASE_NAME,
            collection_name=METADATA_COLLECTION,
            auth_source=auth_source,
        )
        self._coll = self._repo.collection

    @property
    def collection(self) -> Collection:
        """底层集合。"""
        return self._coll

    def save(
        self,
        *,
        storage_subdir: str,
        relative_path: str,
        title: Optional[str] = None,
        size: Optional[int] = None,
        content_hash: Optional[str] = None,
        md5: Optional[str] = None,
        phash: Optional[str] = None,
        mime_type: Optional[str] = None,
        ticket_id: Optional[str] = None,
        task_id: Optional[str] = None,
        status: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
    ) -> Union[InsertOneResult, UpdateResult]:
        """
        存储一条元数据。若 (storage_subdir, relative_path) 已存在则更新，否则插入。
        更新时自动刷新 updated_at，插入时设置 created_at / updated_at。
        """
        doc = make_document(
            storage_subdir=storage_subdir,
            relative_path=relative_path,
            title=title,
            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,
        )
        flt = make_filter(storage_subdir, relative_path)
        existing = self._coll.find_one(flt)
        if existing:
            update_doc = {
                "$set": {
                    KEY_UPDATED_AT: _now_utc(),
                    **{k: v for k, v in doc.items() if k not in (KEY_CREATED_AT,)},
                }
            }
            return self._coll.update_one(flt, update_doc)
        return self._coll.insert_one(doc)

    def update_by_path(
        self,
        storage_subdir: str,
        relative_path: str,
        *,
        title: Optional[str] = None,
        size_bytes: Optional[int] = None,
        content_hash: Optional[str] = None,
        md5: Optional[str] = None,
        phash: Optional[str] = None,
        mime_type: Optional[str] = None,
        ticket_id: Optional[str] = None,
        task_id: Optional[str] = None,
        status: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
    ) -> UpdateResult:
        """
        按 (storage_subdir, relative_path) 更新部分字段。
        """
        flt = make_filter(storage_subdir, relative_path)
        set_payload: dict[str, Any] = {KEY_UPDATED_AT: _now_utc()}
        if title is not None:
            set_payload[KEY_TITLE] = title.strip() if title else None
        if size_bytes is not None:
            set_payload[KEY_SIZE] = size_bytes
        if content_hash is not None:
            set_payload[KEY_CONTENT_HASH] = content_hash.strip() if content_hash else None
        if md5 is not None:
            set_payload[KEY_MD5] = md5.strip() if md5 else None
        if phash is not None:
            set_payload[KEY_PHASH] = phash.strip() if phash else None
        if mime_type is not None:
            normalized_mime_type = _normalize_and_validate_mime_type(mime_type)
            set_payload[KEY_MIME_TYPE] = normalized_mime_type
        if ticket_id is not None:
            set_payload[KEY_TICKET_ID] = ticket_id.strip() if ticket_id else None
        if task_id is not None:
            set_payload[KEY_TASK_ID] = task_id.strip() if task_id else None
        if status is not None:
            set_payload[KEY_STATUS] = status.strip() if status else None
        if metadata is not None:
            set_payload[KEY_METADATA] = metadata
        return self._coll.update_one(flt, {"$set": set_payload})

    def find_by_path_and_md5(
        self, storage_subdir: str, relative_path: str, md5: str
    ) -> Optional[dict[str, Any]]:
        """按路径和 md5 查询单条元数据。"""
        return self._coll.find_one(make_md5_filter(storage_subdir, relative_path, md5))

    def update_one(
        self, filter: dict[str, Any], update: dict[str, Any], **kwargs: Any
    ) -> UpdateResult:
        """透传底层 update_one，便于自定义更新。"""
        return self._coll.update_one(filter, update, **kwargs)

    def find_by_path(
        self, storage_subdir: str, relative_path: str
    ) -> Optional[dict[str, Any]]:
        """按 (storage_subdir, relative_path) 查询单条元数据。"""
        return self._coll.find_one(make_filter(storage_subdir, relative_path))

    def list_by_storage_subdir(
        self, storage_subdir: str, **kwargs: Any
    ) -> Cursor[dict[str, Any]]:
        """按 storage_subdir 查询该子目录下所有元数据（返回 Cursor）。"""
        return self._coll.find({KEY_STORAGE_SUBDIR: storage_subdir.strip()}, **kwargs)

    def delete_by_path(
        self, storage_subdir: str, relative_path: str
    ) -> DeleteResult:
        """按 (storage_subdir, relative_path) 删除一条元数据。"""
        return self._coll.delete_one(make_filter(storage_subdir, relative_path))
