"""
订阅数据访问。

本模块只收敛针对订阅表的读写。把 MediaInfo / MusicInfo 翻译成一行订阅是订阅业务的
规则，住在 app/application/subscription/write.py；这里收到的 payload 已经是纯粹的持久化字段，
因此不 import 任何领域对象。

留在这一层的只有列类型强转与建库时间戳——它们跟着订阅表的列走，换谁来调都一样。
"""
import time
from collections.abc import Awaitable, Callable
from typing import Any, Tuple, List, Optional

from sqlalchemy import delete as sqlalchemy_delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session

from app.application.subscription.delete import SubscribeDeletionCandidate
from app.db.base import DbOper
from app.db.models.subscribe import Subscribe
from app.db.models.subscribehistory import SubscribeHistory
from app.schemas.types import MediaSource

INTEGER_FLAG_FIELDS = ("best_version", "best_version_full", "search_imdbid", "manual_total_episode")

AfterCommitEffect = Callable[[int], None]
AsyncAfterCommitEffect = Callable[[int], Awaitable[None]]


class SubscribeStageResult:
    """Oper 暂存结果，按 Application 端口需要暴露最小只读状态。"""

    __slots__ = ("_subscribe_id", "_message", "_created")

    def __init__(self, subscribe_id: int, message: str, created: bool) -> None:
        """保存写入后的订阅 ID、消息和是否创建标志。"""
        self._subscribe_id = subscribe_id
        self._message = message
        self._created = created

    @property
    def subscribe_id(self) -> int:
        """返回已暂存或已存在的订阅 ID。"""
        return self._subscribe_id

    @property
    def message(self) -> str:
        """返回暂存结果的人类可读消息。"""
        return self._message

    @property
    def created(self) -> bool:
        """返回本次暂存是否创建了新记录。"""
        return self._created


def _normalize_integer_flags(payload: dict, fields: Tuple[str, ...] = INTEGER_FLAG_FIELDS) -> dict:
    """
    将历史兼容的布尔开关转换为整型值，避免 PostgreSQL 严格类型检查失败。
    """
    normalized_payload = dict(payload)
    for field in fields:
        if isinstance(normalized_payload.get(field), bool):
            normalized_payload[field] = int(normalized_payload[field])
    return normalized_payload


def _normalize_year(year: Optional[int | str]) -> Optional[str]:
    """
    订阅表的 year 列为字符串类型，而识别链路的媒体年份可能是数字
    （音乐等来源），写库前统一转换为字符串避免数据库类型错误。
    """
    if year is None:
        return None
    return str(year)


def _persistable(payload: dict) -> dict:
    """
    把应用层给的写入字段落成订阅表能收的一行。

    做两件事。一是列类型强转：PostgreSQL 的整型列拒收布尔值、字符串列拒收数字，而
    SQLite 会靠类型亲和悄悄替我们转好——漏了只在生产库上炸，所以放在紧挨建模的地方。
    二是盖建库时间戳：调用方传进来的 date 不作数，否则订阅列表的默认排序与过期清理
    都会读到一个假的建库时间。
    :param payload: 应用层翻译好的写入字段
    :return: 可直接建模的字段字典
    """
    persistable = _normalize_integer_flags(payload)
    persistable["year"] = _normalize_year(persistable.get("year"))
    # search_imdbid 参与搜索分支判定，None 与真值都要归一到 0/1，否则同一列在不同
    # 订阅上会存出三种形态，PG 上还会直接拒写
    persistable["search_imdbid"] = 1 if persistable.get("search_imdbid") else 0
    persistable["date"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    return persistable


class SubscribeOper(DbOper):
    """
    订阅管理
    """

    def _exists(self, identity: dict, username: Optional[str]) -> Optional[Any]:
        """
        按身份查重。
        :param identity: 查重身份
        :param username: 非空时只在该用户的订阅内查
        :return: 命中的订阅行，未命中为 None
        """
        if username == "":
            return None
        if username:
            return self._execute_sync_query(
                lambda session: Subscribe.exists_by_username(
                    session,
                    username=username,
                    **identity,
                )
            )
        return self._execute_sync_query(
            lambda session: Subscribe.exists(session, **identity)
        )

    async def _async_exists(self, identity: dict, username: Optional[str]) -> Optional[Any]:
        """
        按身份查重（异步）。
        :param identity: 查重身份
        :param username: 非空时只在该用户的订阅内查
        :return: 命中的订阅行，未命中为 None
        """
        async def query(session: AsyncSession) -> Optional[Subscribe]:
            """在调用方或组合根异步会话中执行订阅查重。"""
            if username == "":
                return None
            if username:
                return await Subscribe.async_exists_by_username(
                    session,
                    username=username,
                    **identity,
                )
            return await Subscribe.async_exists(session, **identity)
        return await self._execute_async_query(query)

    def stage_add(
        self,
        identity: dict,
        payload: dict,
        username: Optional[str] = None,
    ) -> SubscribeStageResult:
        """暂存同步新增并 flush 主键，不提交调用方拥有的事务。"""
        if not isinstance(self._db, Session):
            raise RuntimeError("同步订阅新增需要调用方提供 Session")
        subscribe = self._exists(identity, username)
        if subscribe:
            return SubscribeStageResult(
                subscribe_id=subscribe.id,
                message="订阅已存在",
                created=False,
            )
        subscribe = Subscribe(**_persistable(payload))
        self._db.add(subscribe)
        self._db.flush()
        if not subscribe.id:
            return SubscribeStageResult(0, "新增订阅失败", True)
        return SubscribeStageResult(subscribe.id, "新增订阅成功", True)

    async def async_stage_add(
        self,
        identity: dict,
        payload: dict,
        username: Optional[str] = None,
    ) -> SubscribeStageResult:
        """暂存异步新增并 flush 主键，不提交调用方拥有的事务。"""
        if not isinstance(self._db, AsyncSession):
            raise RuntimeError("异步订阅新增需要调用方提供 AsyncSession")
        subscribe = await self._async_exists(identity, username)
        if subscribe:
            return SubscribeStageResult(
                subscribe_id=subscribe.id,
                message="订阅已存在",
                created=False,
            )
        subscribe = Subscribe(**_persistable(payload))
        self._db.add(subscribe)
        await self._db.flush()
        if not subscribe.id:
            return SubscribeStageResult(0, "新增订阅失败", True)
        return SubscribeStageResult(subscribe.id, "新增订阅成功", True)

    def add(self, identity: dict, payload: dict,
            username: Optional[str] = None,
            after_commit: Optional[AfterCommitEffect] = None) -> Tuple[int, str]:
        """
        新增订阅：命中既有订阅则原样返回，否则落库后回读。

        回读不是多余的一次查询——写入可能被唯一约束或事务回滚吞掉，此时若报成功，
        调用方会继续按订阅已建立往下走，用户看到「订阅成功」却永远等不到资源。
        :param identity: 查重身份（media_source/media_id/music_type/season/episode_group）
        :param payload: 订阅表的写入字段，媒体翻译由 application/subscription/write.py 完成
        :param username: 非空时把查重限定在该用户的订阅内
        :param after_commit: 兼容旧调用方的提交后副作用；新入口由 Application Command 调用
        :return: (订阅 ID, 结果说明)；ID 为 0 表示未新增
        """
        subscribe = self._exists(identity, username)
        if subscribe:
            if after_commit:
                after_commit(subscribe.id)
            return subscribe.id, "订阅已存在"
        self._stage_create(Subscribe(**_persistable(payload)))
        subscribe = self._exists(identity, username)
        if not subscribe:
            return 0, "新增订阅失败"
        if after_commit:
            after_commit(subscribe.id)
        return subscribe.id, "新增订阅成功"

    async def async_add(self, identity: dict, payload: dict,
                        username: Optional[str] = None,
                        after_commit: Optional[AsyncAfterCommitEffect] = None) -> Tuple[int, str]:
        """
        异步新增订阅，语义与 add 完全一致。
        :param identity: 查重身份（media_source/media_id/music_type/season/episode_group）
        :param payload: 订阅表的写入字段，媒体翻译由 application/subscription/write.py 完成
        :param username: 非空时把查重限定在该用户的订阅内
        :param after_commit: 兼容旧调用方的异步提交后副作用
        :return: (订阅 ID, 结果说明)；ID 为 0 表示未新增
        """
        subscribe = await self._async_exists(identity, username)
        if subscribe:
            if after_commit:
                await after_commit(subscribe.id)
            return subscribe.id, "订阅已存在"
        await self._stage_async_create(Subscribe(**_persistable(payload)))
        subscribe = await self._async_exists(identity, username)
        if not subscribe:
            return 0, "新增订阅失败"
        if after_commit:
            await after_commit(subscribe.id)
        return subscribe.id, "新增订阅成功"

    def exists(
            self, media_source: MediaSource, media_id: str,
            season: Optional[int] = None, episode_group: Optional[str] = None,
            music_type: Optional[str] = None,
    ) -> bool:
        """
        按媒体身份、季号及可选剧集组判断订阅是否存在。
        """
        identity_params = {
            "media_source": media_source,
            "media_id": media_id,
            "music_type": music_type,
            "season": season,
            "episode_group": episode_group,
        }
        return bool(self._exists(identity_params, username=None))

    async def async_exists(
            self, media_source: MediaSource, media_id: str,
            season: Optional[int] = None, episode_group: Optional[str] = None,
            music_type: Optional[str] = None,
    ) -> Optional[Subscribe]:
        """异步按媒体身份、季号及可选剧集组读取命中的订阅。"""
        return await self._async_exists(
            {
                "media_source": media_source,
                "media_id": media_id,
                "music_type": music_type,
                "season": season,
                "episode_group": episode_group,
            },
            username=None,
        )

    def get(self, sid: int) -> Optional[Subscribe]:
        """
        获取订阅
        """
        return self._execute_sync_query(lambda session: Subscribe.get(session, sid))

    async def async_get(self, sid: int) -> Optional[Subscribe]:
        """
        获取订阅
        """
        return await self._execute_async_query(
            lambda session: Subscribe.async_get(session, sid)
        )

    async def async_list_by_media_identity(
        self,
        media_source: MediaSource,
        media_id: str,
        music_type: Optional[str] = None,
    ) -> List[Subscribe]:
        """异步按规范媒体身份读取订阅。"""
        return await self._execute_async_query(
            lambda session: Subscribe.async_list_by_media_identity(
                session,
                media_source=media_source,
                media_id=media_id,
                music_type=music_type,
            )
        )

    def list_by_media_identity(
        self,
        media_source: MediaSource,
        media_id: str,
        music_type: Optional[str] = None,
    ) -> List[Subscribe]:
        """同步按规范媒体身份读取订阅。"""
        return self._execute_sync_query(
            lambda session: Subscribe.list_by_media_identity(
                session,
                media_source=media_source,
                media_id=media_id,
                music_type=music_type,
            )
        )

    async def get_candidate(
        self,
        subscribe_id: int,
    ) -> Optional[SubscribeDeletionCandidate]:
        """读取订阅删除用例需要的权限字段与完整事件快照。"""
        subscribe = await self.async_get(subscribe_id)
        return self._deletion_candidate(subscribe_id, subscribe)

    def get_candidate_sync(
        self,
        subscribe_id: int,
    ) -> Optional[SubscribeDeletionCandidate]:
        """同步读取订阅删除用例需要的权限字段与完整事件快照。"""
        return self._deletion_candidate(subscribe_id, self.get(subscribe_id))

    @staticmethod
    def _deletion_candidate(
        subscribe_id: int,
        subscribe: Optional[Subscribe],
    ) -> Optional[SubscribeDeletionCandidate]:
        """把 ORM 行投影为同步和异步删除命令共用的稳定快照。"""
        if not subscribe:
            return None
        values = subscribe.__dict__
        return SubscribeDeletionCandidate(
            subscribe_id=subscribe_id,
            username=subscribe.username,
            event_payload={
                column.name: values.get(column.name)
                for column in subscribe.__table__.columns
            },
        )

    async def list_candidates_by_identity(
            self,
            media_source: MediaSource,
            media_id: str,
            season: Optional[int],
            music_type: Optional[str],
    ) -> List[SubscribeDeletionCandidate]:
        """按媒体身份读取去重后的订阅删除快照。"""
        subscribes = await self.async_list_by_media_identity(
            media_source, media_id, music_type
        )
        candidates = []
        seen_ids = set()
        for subscribe in subscribes or []:
            subscribe_music_type = getattr(subscribe, "music_type", None)
            if music_type and not (
                    subscribe_music_type == music_type
                    or (music_type == "recording" and subscribe_music_type is None)
            ):
                continue
            if season is not None and subscribe.season != season:
                continue
            if not subscribe.id or subscribe.id in seen_ids:
                continue
            seen_ids.add(subscribe.id)
            values = subscribe.__dict__
            candidates.append(
                SubscribeDeletionCandidate(
                    subscribe_id=subscribe.id,
                    username=subscribe.username,
                    event_payload={
                        column.name: values.get(column.name)
                        for column in subscribe.__table__.columns
                    },
                )
            )
        return candidates

    async def list_search_ids(self, username: str, state: str) -> List[int]:
        """返回用户指定状态的订阅编号，不向应用用例暴露 ORM 列表。"""
        subscribes = await self.async_list_by_username(username, state=state)
        return [subscribe.id for subscribe in subscribes if subscribe.id]

    def get_by(
            self, type: str, media_source: MediaSource, media_id: str,
            season: Optional[str] = None,
            music_type: Optional[str] = None,
    ) -> Optional[Subscribe]:
        """
        根据条件查询订阅
        """
        return self._execute_sync_query(
            lambda session: Subscribe.get_by(
                session,
                type=type,
                media_source=media_source,
                media_id=media_id,
                season=season,
                music_type=music_type,
            )
        )

    async def async_get_by(
            self, type: str, media_source: MediaSource, media_id: str,
            season: Optional[str] = None,
            music_type: Optional[str] = None,
    ) -> Optional[Subscribe]:
        """
        根据条件查询订阅
        """
        return await self._execute_async_query(
            lambda session: Subscribe.async_get_by(
                session,
                type=type,
                media_source=media_source,
                media_id=media_id,
                season=season,
                music_type=music_type,
            )
        )

    def list(self, state: Optional[str] = None) -> List[Subscribe]:
        """
        获取订阅列表
        """
        return self._execute_sync_query(
            lambda session: Subscribe.get_by_state(session, state)
        )

    async def async_list(self, state: Optional[str] = None) -> List[Subscribe]:
        """
        异步获取订阅列表
        """
        if state:
            return await self._execute_async_query(
                lambda session: Subscribe.async_get_by_state(session, state)
            )
        return await self._execute_async_query(Subscribe.async_list)

    async def async_list_by_username(
        self,
        username: str,
        state: Optional[str] = None,
        mtype: Optional[str] = None,
    ) -> List[Subscribe]:
        """异步按用户获取订阅。"""
        return await self._execute_async_query(
            lambda session: Subscribe.async_list_by_username(
                session,
                username=username,
                state=state,
                mtype=mtype,
            )
        )

    async def async_list_by_title(
        self,
        title: str,
        season: Optional[int] = None,
    ) -> List[Subscribe]:
        """在 Oper 会话边界内异步按标题获取订阅。"""
        return await self._execute_async_query(
            lambda session: Subscribe.async_list_by_title(
                session,
                title=title,
                season=season,
            )
        )

    def delete(self, sid: int):
        """
        删除订阅
        """
        self._stage_delete(Subscribe, sid)

    async def async_delete(self, sid: int):
        """
        异步删除订阅。
        """
        await self._stage_async_delete(Subscribe, sid)

    async def stage_delete(self, sid: int) -> None:
        """登记订阅删除但不提交，由 Application UnitOfWork 控制事务边界。"""
        await self._db.execute(
            sqlalchemy_delete(Subscribe).where(Subscribe.id == sid)
        )

    def stage_delete_sync(self, sid: int) -> None:
        """同步登记订阅删除但不提交，由 Application UnitOfWork 控制事务边界。"""
        if not isinstance(self._db, Session):
            raise RuntimeError("同步订阅删除需要调用方提供 Session")
        self._db.execute(sqlalchemy_delete(Subscribe).where(Subscribe.id == sid))

    async def async_update(self, sid: int, payload: dict) -> Optional[Subscribe]:
        """
        异步更新订阅。
        """
        subscribe = await self.async_get(sid)
        if subscribe:
            payload = _normalize_integer_flags(payload)
            await self._stage_async_update(subscribe, payload)
        return subscribe

    async def async_stage_update(
        self,
        sid: int,
        payload: dict,
    ) -> Optional[Subscribe]:
        """在调用方 AsyncSession 中暂存订阅更新并 flush，不提交事务。"""
        if not isinstance(self._db, AsyncSession):
            raise RuntimeError("异步订阅修改需要调用方提供 AsyncSession")
        subscribe = await self.async_get(sid)
        if not subscribe:
            return None
        for key, value in _normalize_integer_flags(payload).items():
            setattr(subscribe, key, value)
        await self._db.flush()
        return subscribe

    async def async_update_filter_groups(
            self, sid: int, filter_groups: List[str]
    ) -> Optional[Subscribe]:
        """
        异步更新订阅使用的过滤规则组。
        """
        return await self.async_update(sid, {"filter_groups": filter_groups})

    def update(self, sid: int, payload: dict) -> Optional[Subscribe]:
        """
        更新订阅
        """
        subscribe = self.get(sid)
        if subscribe:
            payload = _normalize_integer_flags(payload)
            self._stage_update(subscribe, payload)
        return subscribe

    def list_by_username(self, username: str, state: Optional[str] = None,
                         mtype: Optional[str] = None) -> List[Subscribe]:
        """
        获取指定用户的订阅
        """
        return self._execute_sync_query(
            lambda session: Subscribe.list_by_username(
                session,
                username=username,
                state=state,
                mtype=mtype,
            )
        )

    def list_by_type(self, mtype: str, days: int = 7) -> List[Subscribe]:
        """
        获取指定类型的订阅
        """
        return self._execute_sync_query(
            lambda session: Subscribe.list_by_type(session, mtype, days)
        )

    def add_history(self, **kwargs):
        """
        新增订阅
        """
        # 去除kwargs中 SubscribeHistory 没有的字段
        kwargs = {k: v for k, v in kwargs.items() if hasattr(SubscribeHistory, k)}
        kwargs = _normalize_integer_flags(kwargs)
        # 更新完成订阅时间
        kwargs.update({"date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())})
        # 去掉主键
        if "id" in kwargs:
            kwargs.pop("id")
        subscribe = SubscribeHistory(**kwargs)
        self._stage_create(subscribe)

    def exist_history(
            self, media_source: MediaSource, media_id: str,
            season: Optional[int] = None, episode_group: Optional[str] = None,
            music_type: Optional[str] = None,
    ) -> bool:
        """
        按媒体身份、季号及可选剧集组判断订阅历史是否存在。
        """
        identity_params = {
            "media_source": media_source,
            "media_id": media_id,
            "music_type": music_type,
            "season": season,
            "episode_group": episode_group,
        }
        return bool(self._execute_sync_query(
            lambda session: SubscribeHistory.exists(session, **identity_params)
        ))
