"""persist-all 이미지 영속화 acceptance 검증 — 6종 SQL/구조 invariant (Wave4 SOT).

Codex 합의 acceptance(policy-matrix §검증):
  1. input_image_ids dangling 0       — 모든 input UUID 가 image_asset.id 로 resolve.
  2. 최종물 pipeline_role rows         — pipeline_role NOT NULL AND asset_type<>'generated'
                                          → is_intermediate=false 100% (annotate 경계).
  3. intermediate rows asset_type      — is_intermediate=true → asset_type='generated' 100%.
  4. reference_composite 2 inputs      — pipeline_role='reference_composite' 행은 input 2개,
                                          둘 다 resolve(face+outlook lineage).
  5. scene source_image_id resolved    — scene_still 행 중 source_image_id 있으면 resolve.
  6. duplicate final/intermediate 0    — 같은 file_path 가 is_intermediate true/false 양쪽 0.

importable: ``run_acceptance_checks(db, project_id, episode_id=None) -> List[CheckResult]``.
CLI: ``python scripts/verify_persist_all_acceptance.py --project <pid> [--episode <eid>]``.

비파괴 read-only. 실패 시 offending count + sample id 노출(silent pass 금지).
"""
from __future__ import annotations

import json
from dataclasses import dataclass, field
from typing import List, Optional


@dataclass
class CheckResult:
    name: str
    passed: bool
    detail: str = ""
    offending_sample: List[str] = field(default_factory=list)


def _all_asset_ids(db, project_id: str) -> set:
    from app.models.project import ImageAsset

    rows = db.query(ImageAsset.id).filter(ImageAsset.project_id == project_id).all()
    return {r[0] for r in rows}


def _parse_ids(raw: Optional[str]) -> List[str]:
    if not raw:
        return []
    try:
        val = json.loads(raw)
    except (ValueError, TypeError):
        return []
    return [str(x) for x in val] if isinstance(val, list) else []


def run_acceptance_checks(
    db, project_id: str, episode_id: Optional[str] = None
) -> List[CheckResult]:
    from app.models.project import ImageAsset

    results: List[CheckResult] = []
    q = db.query(ImageAsset).filter(ImageAsset.project_id == project_id)
    if episode_id:
        q = q.filter(ImageAsset.episode_id == episode_id)
    assets = q.all()
    valid_ids = {a.id for a in assets} | _all_asset_ids(db, project_id)

    # 1. input_image_ids dangling 0
    dangling: List[str] = []
    for a in assets:
        for iid in _parse_ids(a.input_image_ids):
            if iid not in valid_ids:
                dangling.append(f"{a.id}->{iid}")
    results.append(CheckResult(
        "1_input_image_ids_dangling", not dangling,
        f"{len(dangling)} dangling refs", dangling[:5],
    ))

    # 2. 최종물 pipeline_role rows → is_intermediate=false
    bad_final = [
        a.id for a in assets
        if a.pipeline_role is not None and a.asset_type != "generated"
        and bool(a.is_intermediate)
    ]
    results.append(CheckResult(
        "2_final_role_is_intermediate_false", not bad_final,
        f"{len(bad_final)} final rows wrongly is_intermediate=true", bad_final[:5],
    ))

    # 3. intermediate rows → asset_type='generated'
    bad_inter = [
        a.id for a in assets
        if bool(a.is_intermediate) and a.asset_type != "generated"
    ]
    results.append(CheckResult(
        "3_intermediate_asset_type_generated", not bad_inter,
        f"{len(bad_inter)} intermediate rows with non-generated asset_type", bad_inter[:5],
    ))

    # 4. reference_composite 행은 input 2개 + 둘 다 resolve
    bad_comp: List[str] = []
    for a in assets:
        if a.pipeline_role == "reference_composite":
            ids = _parse_ids(a.input_image_ids)
            if len(ids) != 2 or any(i not in valid_ids for i in ids):
                bad_comp.append(f"{a.id}(n={len(ids)})")
    results.append(CheckResult(
        "4_reference_composite_two_resolved_inputs", not bad_comp,
        f"{len(bad_comp)} composite rows without 2 resolved inputs", bad_comp[:5],
    ))

    # 5. scene_still source_image_id resolve
    bad_src = [
        a.id for a in assets
        if a.pipeline_role == "scene_still" and a.source_image_id
        and a.source_image_id not in valid_ids
    ]
    results.append(CheckResult(
        "5_scene_source_image_id_resolved", not bad_src,
        f"{len(bad_src)} scene rows with dangling source_image_id", bad_src[:5],
    ))

    # 6. duplicate file_path (is_intermediate true & false 양쪽)
    by_path: dict[str, set] = {}
    for a in assets:
        if a.file_path:
            by_path.setdefault(a.file_path, set()).add(bool(a.is_intermediate))
    dup_paths = [p for p, flags in by_path.items() if len(flags) > 1]
    results.append(CheckResult(
        "6_no_final_intermediate_file_path_dup", not dup_paths,
        f"{len(dup_paths)} file_paths in both is_intermediate states", dup_paths[:5],
    ))

    return results


def _format(results: List[CheckResult]) -> str:
    lines = []
    for r in results:
        status = "PASS" if r.passed else "FAIL"
        lines.append(f"[{status}] {r.name}: {r.detail}")
        if not r.passed and r.offending_sample:
            lines.append(f"        sample: {r.offending_sample}")
    n_fail = sum(1 for r in results if not r.passed)
    lines.append("")
    lines.append(f"=== {len(results)-n_fail}/{len(results)} PASS"
                 + ("" if n_fail == 0 else f" — {n_fail} FAIL") + " ===")
    return "\n".join(lines)


def main() -> int:
    import argparse
    import os
    import sys as _sys

    # self-sufficient: backend/ 를 sys.path 에 넣어 PYTHONPATH 없이도 실행 가능.
    _backend = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    if _backend not in _sys.path:
        _sys.path.insert(0, _backend)

    parser = argparse.ArgumentParser(description="persist-all acceptance 검증")
    parser.add_argument("--project", required=True, help="project_id")
    parser.add_argument("--episode", default=None, help="episode_id (optional)")
    args = parser.parse_args()

    from app.core.database import SessionLocal

    db = SessionLocal()
    try:
        results = run_acceptance_checks(db, args.project, args.episode)
    finally:
        db.close()

    print(_format(results))
    return 0 if all(r.passed for r in results) else 1


if __name__ == "__main__":
    import sys

    sys.exit(main())
