"""2.2.14
统一媒体身份字段并补齐 Bangumi/AniList ID

Revision ID: f7b2d5c9a301
Revises: e6a1c4b8d2f0
Create Date: 2026-07-21
"""

from typing import Iterable

from alembic import op
import sqlalchemy as sa


revision = "f7b2d5c9a301"
down_revision = "e6a1c4b8d2f0"
branch_labels = None
depends_on = None


def _has_column(
        inspector: sa.Inspector,
        table_name: str,
        column_name: str,
) -> bool:
    """检查数据表是否已存在指定字段。"""
    if table_name not in inspector.get_table_names():
        return False
    return any(
        column["name"] == column_name
        for column in inspector.get_columns(table_name)
    )


def _has_index(
        inspector: sa.Inspector,
        table_name: str,
        index_name: str,
) -> bool:
    """检查数据表是否已存在指定索引。"""
    if table_name not in inspector.get_table_names():
        return False
    return any(
        index["name"] == index_name
        for index in inspector.get_indexes(table_name)
    )


def _column_names(table_name: str) -> set[str]:
    """读取数据表当前全部字段名。"""
    inspector = sa.inspect(op.get_bind())
    if table_name not in inspector.get_table_names():
        return set()
    return {
        column["name"]
        for column in inspector.get_columns(table_name)
    }


def _add_columns(table_name: str, columns: Iterable[sa.Column]) -> None:
    """为指定表补充尚不存在的字段。"""
    for column in columns:
        inspector = sa.inspect(op.get_bind())
        if not _has_column(inspector, table_name, column.name):
            op.add_column(table_name, column)


def _create_index(table_name: str, index_name: str, columns: list[str]) -> None:
    """为指定表创建尚不存在的索引。"""
    inspector = sa.inspect(op.get_bind())
    if not _has_index(inspector, table_name, index_name):
        op.create_index(index_name, table_name, columns)


def _backfill_media_identity(table_name: str) -> None:
    """使用表中实际存在的兼容 ID 幂等回填统一媒体身份。"""
    existing_columns = _column_names(table_name)
    columns = [
        sa.column("media_source", sa.String()),
        sa.column("media_id", sa.String()),
    ]
    identity_columns = {
        "mediaid": sa.String,
        "tmdbid": sa.Integer,
        "doubanid": sa.String,
        "bangumiid": sa.Integer,
        "anilistid": sa.Integer,
    }
    columns.extend(
        sa.column(column_name, column_type())
        for column_name, column_type in identity_columns.items()
        if column_name in existing_columns
    )
    table = sa.table(table_name, *columns)
    connection = op.get_bind()

    if "mediaid" in existing_columns:
        for prefix, source in (
                ("tmdb", "themoviedb"),
                ("themoviedb", "themoviedb"),
                ("douban", "douban"),
                ("bangumi", "bangumi"),
                ("anilist", "anilist"),
        ):
            connection.execute(
                table.update()
                .where(table.c.media_id.is_(None))
                .where(table.c.mediaid.like(f"{prefix}:%"))
                .values(
                    media_source=source,
                    media_id=sa.func.substr(table.c.mediaid, len(prefix) + 2),
                )
            )

    for source, field in (
            ("themoviedb", "tmdbid"),
            ("douban", "doubanid"),
            ("bangumi", "bangumiid"),
            ("anilist", "anilistid"),
    ):
        if field not in existing_columns:
            continue
        identity_column = table.c[field]
        connection.execute(
            table.update()
            .where(table.c.media_id.is_(None))
            .where(identity_column.is_not(None))
            .values(
                media_source=source,
                media_id=sa.cast(identity_column, sa.String()),
            )
        )


def upgrade() -> None:
    """升级媒体身份字段并迁移存量数据。"""
    _add_columns("subscribe", (
        sa.Column("anilistid", sa.Integer(), nullable=True),
        sa.Column("media_source", sa.String(), nullable=True),
        sa.Column("media_id", sa.String(), nullable=True),
    ))
    _add_columns("subscribehistory", (
        sa.Column("anilistid", sa.Integer(), nullable=True),
        sa.Column("media_source", sa.String(), nullable=True),
        sa.Column("media_id", sa.String(), nullable=True),
    ))
    _add_columns("downloadhistory", (
        sa.Column("bangumiid", sa.Integer(), nullable=True),
        sa.Column("anilistid", sa.Integer(), nullable=True),
        sa.Column("media_source", sa.String(), nullable=True),
        sa.Column("media_id", sa.String(), nullable=True),
    ))
    _add_columns("transferhistory", (
        sa.Column("bangumiid", sa.Integer(), nullable=True),
        sa.Column("anilistid", sa.Integer(), nullable=True),
    ))
    _add_columns("downloadfailure", (
        sa.Column("bangumiid", sa.Integer(), nullable=True),
        sa.Column("anilistid", sa.Integer(), nullable=True),
        sa.Column("media_source", sa.String(), nullable=True),
        sa.Column("media_id", sa.String(), nullable=True),
    ))

    for table_name, fields in {
        "subscribe": ("anilistid", "media_source", "media_id"),
        "subscribehistory": ("anilistid", "media_source", "media_id"),
        "downloadhistory": ("bangumiid", "anilistid", "media_source", "media_id"),
        "transferhistory": ("bangumiid", "anilistid"),
    }.items():
        for field in fields:
            _create_index(table_name, f"ix_{table_name}_{field}", [field])

    for table_name in ("subscribe", "subscribehistory", "downloadhistory", "transferhistory"):
        _create_index(
            table_name,
            f"ix_{table_name}_media_identity",
            ["media_source", "media_id"],
        )
    _create_index(
        "downloadfailure",
        "ix_downloadfailure_media_identity_site",
        ["type", "media_source", "media_id", "site"],
    )

    _backfill_media_identity("subscribe")
    _backfill_media_identity("subscribehistory")
    _backfill_media_identity("downloadhistory")
    _backfill_media_identity("transferhistory")
    _backfill_media_identity("downloadfailure")


def downgrade() -> None:
    """回滚统一媒体身份及 Bangumi/AniList 字段。"""
    for table_name, index_names in {
        "subscribe": (
            "ix_subscribe_media_identity", "ix_subscribe_media_id",
            "ix_subscribe_media_source", "ix_subscribe_anilistid",
        ),
        "subscribehistory": (
            "ix_subscribehistory_media_identity", "ix_subscribehistory_media_id",
            "ix_subscribehistory_media_source", "ix_subscribehistory_anilistid",
        ),
        "downloadhistory": (
            "ix_downloadhistory_media_identity", "ix_downloadhistory_media_id",
            "ix_downloadhistory_media_source", "ix_downloadhistory_anilistid",
            "ix_downloadhistory_bangumiid",
        ),
        "transferhistory": (
            "ix_transferhistory_media_identity", "ix_transferhistory_anilistid",
            "ix_transferhistory_bangumiid",
        ),
        "downloadfailure": ("ix_downloadfailure_media_identity_site",),
    }.items():
        for index_name in index_names:
            inspector = sa.inspect(op.get_bind())
            if _has_index(inspector, table_name, index_name):
                op.drop_index(index_name, table_name=table_name)

    for table_name, fields in {
        "subscribe": ("media_id", "media_source", "anilistid"),
        "subscribehistory": ("media_id", "media_source", "anilistid"),
        "downloadhistory": ("media_id", "media_source", "anilistid", "bangumiid"),
        "transferhistory": ("anilistid", "bangumiid"),
        "downloadfailure": ("media_id", "media_source", "anilistid", "bangumiid"),
    }.items():
        for field in fields:
            inspector = sa.inspect(op.get_bind())
            if _has_column(inspector, table_name, field):
                op.drop_column(table_name, field)
