"""제작자 정정(creator corrections) 채널 — 프로젝트 단위 세계 사실 override.

W-발명경계 wave3 (2026-07-08). 발명 경계 계약(prompt pack: shot_staging v16 /
scene_detail v37 등)은 "제작자 정정(주어진 경우) > 씬 텍스트 > 저작 재량"
우선순위를 선언하지만 production 에는 정정을 주입할 채널이 없었다
(실험 lane 의 override 파일은 scratchpad 전용). 본 모듈이 그 채널이다.

저장: ``projects/<project_id>/creator_corrections.json``
    {"corrections": [{"id": "<slug>", "text": "<정정 문장>", "active": true}]}

설계 원칙:
  - 코드=구조 검증·조립만. 정정의 **내용**은 전부 데이터 (시나리오 의존
    코드 금지 — 어떤 작품에도 범용).
  - 정정이 없으면 블록은 빈 문자열 → 소비 스텝의 프롬프트는 기존과
    byte-identical (default 무영향, flag 불요).
  - malformed 파일은 조용히 무시하지 않고 AppError fail-fast (silent skip
    금지 — problems.md #2 패턴).
"""
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any, Dict, List

from app.core.config import settings
from app.core.errors import AppError

FILENAME = "creator_corrections.json"

# 소비 스텝 프롬프트에 붙는 공통 블록 헤더 — 계약(pack)의
# "CREATOR CORRECTIONS(제작자 정정) 블록" 문구와 짝을 이룬다.
_BLOCK_HEADER = (
    "\n\n## CREATOR CORRECTIONS (제작자 정정 — 최우선)\n"
    "아래 항목은 제작자가 확정한 작품 사실이다. 시나리오 텍스트·요약·"
    "이전 단계 산출 등 상충하는 모든 다른 입력에 우선한다.\n"
    "★적용 범위: 각 정정은 자신이 언급하는 대상에만 적용된다. 그 대상이 "
    "현재 다루는 장소/장면/범위에 원래 존재하지 않으면 정정을 이유로 "
    "추가·발명하지 마라 — 정정은 '이미 있는 대상의 사실을 바로잡는' "
    "지시이지 '그 대상을 어디에나 넣으라'는 지시가 아니다:\n"
)


def corrections_path(project_id: str) -> Path:
    return Path(settings.projects_dir) / project_id / FILENAME


def _validate(raw: Any, path: Path) -> List[Dict[str, Any]]:
    """구조 검증 (fail-fast). 반환: 전체 항목 리스트 (active 필터 전)."""
    if not isinstance(raw, dict) or not isinstance(raw.get("corrections"), list):
        raise AppError(
            code="creator_corrections.malformed",
            message=f"creator_corrections.json 구조 위반: {path}"
                    " — {\"corrections\": [{id, text, active}]} 형식 필요",
            status_code=400,
        )
    out: List[Dict[str, Any]] = []
    for i, e in enumerate(raw["corrections"]):
        if (not isinstance(e, dict)
                or not isinstance(e.get("id"), str) or not e["id"].strip()
                or not isinstance(e.get("text"), str) or not e["text"].strip()
                or not isinstance(e.get("active", True), bool)):
            raise AppError(
                code="creator_corrections.malformed",
                message=f"creator_corrections.json 항목 {i} 구조 위반: {path}"
                        " — id/text 비어있지 않은 문자열 + active bool 필요",
                status_code=400,
            )
        out.append({"id": e["id"].strip(), "text": e["text"].strip(),
                    "active": e.get("active", True)})
    return out


def load_creator_corrections(project_id: str,
                             active_only: bool = True) -> List[Dict[str, Any]]:
    """정정 목록 로드 (단일 fail-fast reader — API GET 도 이 경로만 사용).

    파일 없음 = [] (정정 미설정은 정상 상태). active_only=False 는 관리
    UI 조회용 전체 목록.
    """
    path = corrections_path(project_id)
    if not path.exists():
        return []
    try:
        raw = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        raise AppError(
            code="creator_corrections.unreadable",
            message=f"creator_corrections.json 읽기 실패: {path} — {exc}",
            status_code=400,
        )
    entries = _validate(raw, path)
    return [e for e in entries if e["active"]] if active_only else entries


def save_creator_corrections(project_id: str,
                             raw: Dict[str, Any]) -> List[Dict[str, Any]]:
    """구조 검증 후 저장 (atomic replace). 반환: 전체 항목 (active 필터 전).

    분석 스텝들이 실행 중 이 파일을 직접 읽고 malformed 는 fail-fast 로
    step 을 멈추므로, partial write 가 관측되지 않도록 tmp → os.replace.
    """
    path = corrections_path(project_id)
    entries = _validate(raw, path)
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(".json.tmp")
    tmp.write_text(
        json.dumps({"corrections": entries}, ensure_ascii=False, indent=1),
        encoding="utf-8",
    )
    os.replace(tmp, path)
    return entries


def corrections_block(corrections: List[Dict[str, Any]]) -> str:
    """소비 스텝 프롬프트에 붙일 블록. 정정 없음 = "" (byte-identical 보장)."""
    if not corrections:
        return ""
    return _BLOCK_HEADER + "\n".join(f"- {e['text']}" for e in corrections)


def project_corrections_block(project_id: str) -> str:
    """load + block 조립 편의 함수 (소비 스텝 1줄 주입용)."""
    return corrections_block(load_creator_corrections(project_id))
