"""체크포인트 파일 I/O 공통 유틸.

Phase 1.4 (architecture-refactor-final/02-final-roadmap.md §Phase 1).

체크포인트는 크래시 시 부분 쓰기 방지를 위해 항상 원자적으로 기록해야 한다.
여러 소비자(api/v1/steps.py, core/step_runner.py, core/steps/t2i_review_step.py 등)에서
동일 패턴이 중복되어 있었던 것을 단일 헬퍼로 통합.

tmp 파일명에 uuid 8자 추가하여 같은 경로에 동시 쓰기가 있어도 tmp가 충돌하지 않도록 함
(Codex Phase 1 리뷰 P1 반영).
"""
from __future__ import annotations

import json
import os
import uuid
from pathlib import Path
from typing import Any, Dict, Optional


def atomic_write_json(
    path: Path,
    payload: Dict[str, Any],
    *,
    indent: int = 2,
    ensure_ascii: bool = False,
) -> None:
    """JSON을 대상 경로에 원자적으로 쓴다.

    per-call unique tmp 파일(`{path}.{uuid8}.tmp`)에 먼저 기록한 뒤 `os.replace`로 교체.
    호출 전에 부모 디렉토리를 생성한다.

    Args:
        path: 최종 저장 경로 (파일명은 임의, 확장자 무관).
        payload: JSON 직렬화 가능한 dict.
        indent: json.dumps indent. 기본 2.
        ensure_ascii: False면 UTF-8 그대로 저장 (한글 유지).

    Raises:
        OSError: 파일 쓰기 실패 시.
        TypeError: payload가 직렬화 불가일 때.
    """
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp_suffix = f".{uuid.uuid4().hex[:8]}.tmp"
    tmp = path.with_suffix(path.suffix + tmp_suffix)
    try:
        tmp.write_text(
            json.dumps(payload, ensure_ascii=ensure_ascii, indent=indent),
            encoding="utf-8",
        )
        os.replace(str(tmp), str(path))
    except Exception:
        # 부분 실패 시 tmp 정리 (남기지 않음)
        try:
            if tmp.exists():
                tmp.unlink()
        except OSError:
            # 의도적: tmp 정리 실패는 원래 예외를 가리지 않기 위해 swallow. raise는 아래에서 원본 예외 처리.
            pass
        raise


def read_json_safe(path: Path) -> Optional[Dict[str, Any]]:
    """경로에서 JSON을 읽고 실패 시 None 반환.

    파일 미존재 / 파싱 오류 / OS 오류 모두 None으로 수렴.
    호출자는 `None` 분기로 처리.

    Args:
        path: 읽을 파일 경로.

    Returns:
        파싱된 dict 또는 None.
    """
    path = Path(path)
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return None
