"""변환 자산의 출처 기록을 **실제 호출값으로** 되돌린다 (감사 0-B backfill).

## 무엇을 고치나

`_cine_applied` 자산이 이렇게 기록돼 있다.

    generation_model : 설정의 grok 모델   ← 실제는 reve/2.1/edit 일 수 있다
    prompt_used      : 빈 문자열          ← 실제는 변환 문안 전문

`generation_call_id` 는 **올바르다** — 그 호출 기록(`llm_call_log`)에
실제 모델과 실제 프롬프트가 있다. 여기서 그것을 옮겨 적는다.

## 안전

- **기본은 dry-run.** 쓰려면 `--apply` 를 준다.
- 대상은 `generation_call_id` 가 가리키는 호출의 `operation_type` 이
  `still_cine_transform` 인 자산만. 다른 계열은 손대지 않는다.
- `prompt_used` 는 **비어 있을 때만** 채운다 — 있는 값을 덮지 않는다.
- `generation_model` 은 호출 기록의 모델과 다를 때만 고친다.
- 그림은 다시 사지 않는다. 이 스크립트는 **기록만** 만진다.

    python -m scripts.backfill_cine_asset_provenance            # 미리 보기
    python -m scripts.backfill_cine_asset_provenance --apply    # 실제 적용
    python -m scripts.backfill_cine_asset_provenance --project <id>
"""
import argparse
import sys

from sqlalchemy import text

from app.core.database import SessionLocal

_TARGET_OP = "still_cine_transform"

_SELECT = """
SELECT a.id            AS asset_id,
       a.still_id      AS still_id,
       a.generation_model AS asset_model,
       coalesce(a.prompt_used, '') AS asset_prompt,
       l.model_name    AS call_model,
       coalesce(l.user_prompt, '') AS call_prompt,
       l.operation_type AS call_op
  FROM image_asset a
  JOIN llm_call_log l ON l.id = a.generation_call_id
 WHERE a.generation_call_id IS NOT NULL
   AND l.operation_type = :op
   {project_clause}
 ORDER BY a.created_at DESC
"""


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--apply", action="store_true",
                    help="실제로 쓴다 (없으면 미리 보기만)")
    ap.add_argument("--project", default="", help="프로젝트 id 한정")
    args = ap.parse_args()

    clause = "AND a.project_id = :pid" if args.project else ""
    sql = _SELECT.format(project_clause=clause)
    params = {"op": _TARGET_OP}
    if args.project:
        params["pid"] = args.project

    db = SessionLocal()
    try:
        rows = db.execute(text(sql), params).mappings().all()
        print(f"대상 자산 {len(rows)}건 (operation_type={_TARGET_OP})")
        fixes = []
        skipped_truncated: list = []
        for r in rows:
            new_model = (r["call_model"] or "").strip()
            model_wrong = bool(new_model) and r["asset_model"] != new_model
            # ★있는 값을 덮지 않는다 — 비어 있을 때만 채운다.
            call_prompt = r["call_prompt"] or ""
            # ★★잘린 프롬프트는 **쓰지 않는다** (2026-08-26 자체 리뷰).
            #  `log_llm_call` 이 10,000자에서 자르고 `...[truncated]` 를
            #  붙인다. 그것을 복사하면 빈 칸이 **미묘하게 틀린 값**으로
            #  바뀌고, 읽는 사람은 그것을 실제 문안으로 믿는다 — 이 스크립트가
            #  고치려는 병과 같은 부류다. 모르면 비운 채로 둔다.
            truncated = call_prompt.endswith("...[truncated]")
            prompt_missing = (not (r["asset_prompt"] or "").strip()
                              and bool(call_prompt.strip())
                              and not truncated)
            if truncated and not (r["asset_prompt"] or "").strip():
                skipped_truncated.append(r["asset_id"])
            if not (model_wrong or prompt_missing):
                continue
            fixes.append((r, new_model, model_wrong, prompt_missing))
            print(f"\n  asset={r['asset_id']}  still={r['still_id']}")
            if model_wrong:
                print(f"    모델   : {r['asset_model']!r} → {new_model!r}")
            if prompt_missing:
                head = (r["call_prompt"] or "")[:60].replace("\n", " ")
                print(f"    프롬프트: 빈 문자열 → {head!r}… "
                      f"({len(r['call_prompt'])}자)")

        if skipped_truncated:
            print(f"\n★프롬프트가 잘려 있어 **건너뛴** 자산 "
                  f"{len(skipped_truncated)}건 — 로그 상한(10,000자)에 걸린 "
                  f"것들이다. 잘린 문안을 실제 문안으로 적지 않는다.")
        print(f"\n고칠 것 {len(fixes)}건 / 전체 {len(rows)}건")
        if not fixes:
            return 0
        if not args.apply:
            print("\n미리 보기다 — 실제로 쓰려면 --apply 를 준다.")
            return 0

        # ★쓰기 전에 **현재 값을 파일로** 남긴다. 이 저장소는 기록을 날린
        #  이력이 있다 — 되돌릴 길 없이 덮어쓰지 않는다.
        import json
        import os
        from datetime import datetime, timezone

        stamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
        out_dir = os.path.join("scratchpad", "backfill_backup")
        os.makedirs(out_dir, exist_ok=True)
        out = os.path.join(out_dir, f"cine_provenance_{stamp}.json")
        with open(out, "w", encoding="utf-8") as fh:
            json.dump([{
                "asset_id": r["asset_id"],
                "generation_model": r["asset_model"],
                "prompt_used": r["asset_prompt"],
            } for r, *_ in fixes], fh, ensure_ascii=False, indent=1)
        print(f"\n되돌릴 값을 남겼다: {out}")

        for r, new_model, model_wrong, prompt_missing in fixes:
            sets, p = [], {"aid": r["asset_id"]}
            if model_wrong:
                sets.append("generation_model = :m")
                p["m"] = new_model
            if prompt_missing:
                sets.append("prompt_used = :pr")
                p["pr"] = r["call_prompt"]
            db.execute(
                text(f"UPDATE image_asset SET {', '.join(sets)} "
                     f"WHERE id = :aid"), p)
        db.commit()
        print(f"\n적용했다 — {len(fixes)}건. 그림은 다시 사지 않았다.")
        return 0
    finally:
        db.close()


if __name__ == "__main__":
    sys.exit(main())
