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

from datetime import datetime, timedelta, 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_MD5 = "md5"
KEY_FILE_TYPE = "file_type"
KEY_STATUS = "status"
KEY_METADATA = "metadata"
KEY_CREATED_AT = "created_at"
KEY_UPDATED_AT = "updated_at"


class FileType(str, Enum):
    """受支持的文件类型枚举（小写扩展名）。"""

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


_ALLOWED_FILE_TYPES = {t.value for t in FileType}


ENV_TICKET_ID = "NUTRICARE_TICKET_ID"
ENV_TASK_ID = "NUTRICARE_TASK_ID"

def get_ticket_and_task_from_env() -> tuple[str, str]:
    ticket_id = os.environ.get(ENV_TICKET_ID) or None
    task_id = os.environ.get(ENV_TASK_ID) or None
    if not ticket_id or not str(ticket_id).strip():
        raise ValueError("环境变量缺少 ticket_id（%s）" % ENV_TICKET_ID)
    if not task_id or not str(task_id).strip():
        raise ValueError("环境变量缺少 task_id（%s）" % ENV_TASK_ID)
    return str(ticket_id).strip(), str(task_id).strip()


def _normalize_and_validate_file_type(file_type: Optional[str]) -> Optional[str]:
    """
    规范化并校验文件类型（扩展名），只允许枚举中的类型；
    入参可带或不带前导点，内部统一为小写扩展名。
    """
    if file_type is None:
        return None
    value = str(file_type).strip().lower()
    if value.startswith("."):
        value = value[1:]
    if not value:
        return None
    if value not in _ALLOWED_FILE_TYPES:
        raise ValueError(
            f"不支持的文件类型: {file_type!r}，仅支持: {sorted(_ALLOWED_FILE_TYPES)}"
        )
    return value


def infer_file_type_from_relative_path(relative_path: str) -> Optional[str]:
    """从相对路径末段扩展名推断文件类型；不在允许列表中则返回 None。"""
    normalized = relative_path.replace("\\", "/").strip().lstrip("/")
    if "." not in normalized:
        return None
    ext = normalized.rsplit(".", 1)[-1].strip().lower()
    if not ext or ext not in _ALLOWED_FILE_TYPES:
        return None
    return ext

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


_UTC_PLUS_8 = timezone(timedelta(hours=8))


def _now_utc_plus_8() -> datetime:
    """当前 UTC+8（中国标准时间）的带时区时间。"""
    return datetime.now(_UTC_PLUS_8)


def make_document(
    *,
    storage_subdir: str,
    relative_path: str,
    title: Optional[str] = None,
    size: Optional[int] = None,
    md5: Optional[str] = None,
    file_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_plus_8()
    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 md5 is not None and str(md5).strip():
        doc[KEY_MD5] = md5.strip()
    normalized_file_type = _normalize_and_validate_file_type(file_type)
    if normalized_file_type is not None:
        doc[KEY_FILE_TYPE] = normalized_file_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
        self._ticket_id, self._task_id = get_ticket_and_task_from_env()
        if not self._ticket_id or not self._task_id:
            raise ValueError("ticket_id 和 task_id 不能为空")

    @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,
        md5: Optional[str] = None,
        file_type: 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,
            md5=md5,
            file_type=file_type,
            ticket_id=self._ticket_id,
            task_id=self._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_plus_8(),
                    **{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,
        md5: Optional[str] = None,
        file_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_plus_8()}
        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 md5 is not None:
            set_payload[KEY_MD5] = md5.strip() if md5 else None
        if file_type is not None:
            normalized_file_type = _normalize_and_validate_file_type(file_type)
            set_payload[KEY_FILE_TYPE] = normalized_file_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))
