"""image_asset.file_path 절대 경로 차단 CHECK constraint.

ImagePathType TypeDecorator 가 ORM bind 시 자동 상대화 하지만, raw SQL/Core
insert/외부 도구 우회 시 절대 경로가 들어갈 수 있다. DB 차원의 마지막 안전망으로
``file_path`` 가 '/' 로 시작하면 INSERT/UPDATE 거부.

배포 안전성 — 절대 경로(``/Users/...``)는 다른 머신에서 invalid 가 된다. 이 invariant
가 깨지면 e2e false-positive partial / consumer cannot open ref 사고가 재발한다.

사전 backfill: 절대 경로 row 가 있으면 ``to_relative_image_path`` 로 자동 상대화.
projects root 외부 row 가 있으면 abort (운영자가 수동 정리 후 재시도).

Revision ID: 005_file_path_relative_check
Revises: 004_variant_label_extend
Create Date: 2026-05-02
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


revision: str = "005_file_path_relative_check"
down_revision: Union[str, None] = "004_variant_label_extend"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


CHECK_NAME = "ck_image_asset_file_path_relative"
# 동일 표현식이 다음 3 곳에 hardcode 됨 — 한쪽 변경 시 모두 동기화 필요:
#   1) alembic 005 (이 파일)
#   2) backend/app/core/database.py `_migrations` (DO block, idempotent startup)
#   3) backend/app/models/project.py ImageAsset.__table_args__ (CheckConstraint, fresh metadata.create_all)
CHECK_EXPR = "file_path = '' OR file_path NOT LIKE '/%'"


def upgrade() -> None:
    bind = op.get_bind()

    # 운영자 가시성 — 어떤 root 기준으로 backfill 시도하는지 명시 출력.
    # working dir / env 가 production 과 다르면 false-positive abort 가능, 사전에 확인.
    from app.core.file_paths import _resolve_root
    print(f"[alembic 005] image_asset.file_path resolve root = {_resolve_root()}")

    # 1. 절대 경로 row 카운트 — backfill 필요 여부 판단.
    abs_count = bind.execute(
        sa.text("SELECT COUNT(*) FROM image_asset WHERE file_path LIKE '/%'")
    ).scalar()

    if abs_count and int(abs_count) > 0:
        # 2. backfill: settings.projects_dir.parent 기준 상대화. helper 가
        #    root 외부 경로면 절대 그대로 반환하므로 (legacy 호환), 그 row 는
        #    잔존하고 다음 단계에서 abort 한다.
        from app.core.file_paths import to_relative_image_path

        rows = bind.execute(
            sa.text("SELECT id, file_path FROM image_asset WHERE file_path LIKE '/%'")
        ).fetchall()
        normalized = 0
        residual = []
        for row in rows:
            new_path = to_relative_image_path(row.file_path)
            if new_path and not new_path.startswith("/"):
                bind.execute(
                    sa.text("UPDATE image_asset SET file_path = :p WHERE id = :i"),
                    {"p": new_path, "i": row.id},
                )
                normalized += 1
            else:
                residual.append(row.id)

        if residual:
            from app.core.file_paths import _resolve_root
            raise RuntimeError(
                f"alembic 005 abort: {len(residual)} image_asset rows have file_path "
                f"outside resolve root (backfill not possible).\n"
                f"  Resolve root: {_resolve_root()}\n"
                f"  Sample IDs (first 5): {residual[:5]}\n"
                f"  Inspect all: SELECT id, file_path FROM image_asset WHERE file_path LIKE '/%';\n"
                f"  Cleanup options: UPDATE to relative under root, or DELETE if obsolete.\n"
                f"  Then retry: alembic upgrade 005_file_path_relative_check"
            )

    # 3. CHECK constraint 추가 — inspector 기반 idempotent (Phase 4 iter 7
    #    review B1 carry). 이전 부팅 시 startup `_migrations` idempotent 경로
    #    가 이미 같은 constraint 를 만들었을 수 있어 alembic 만 stamp 안 된
    #    상태에서 upgrade 시 duplicate-constraint 로 abort 되던 사고 차단.
    inspector = sa.inspect(bind)
    existing = {c["name"] for c in inspector.get_check_constraints("image_asset")}
    if CHECK_NAME not in existing:
        op.create_check_constraint(
            CHECK_NAME,
            "image_asset",
            CHECK_EXPR,
        )


def downgrade() -> None:
    op.drop_constraint(CHECK_NAME, "image_asset", type_="check")
