"""Checkpoint Sync Service 공용 베이스."""
from __future__ import annotations

import json
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from sqlalchemy import text as sql_text
from sqlalchemy.orm import Session as OrmSession

from app.core.config import settings


def is_cp_syncable(cp: Optional[Dict[str, Any]], data_keys: List[str]) -> bool:
    """체크포인트가 sync 가능한 상태인지 판단.

    상태별 분기 (Codex P2-2 — 2026-05-01):
    - cp 없음 → False (pre-analysis)
    - status == "completed" → True (authoritative; 빈 data도 정당한 zero rows로 의미 있음)
    - status == "partial" + data 핵심 list 모두 비음 → False (cascade 직후 가드)
    - status == "partial" + data 있음 → True (오늘 사고: 1 shot fail이 cascade 차단 X)
    - 그 외 (running/error/None/없음) → False

    Args:
        cp: load_cp() 결과
        data_keys: 검사할 data 내부 list 키
                   (예: ["scenes"], ["characters", "locations", "props"], ["outlooks"]).
                   하나라도 비어있지 않으면 syncable.

    배경:
        - M2 Fix 2 (2026-05-01) — scene_detail 1 shot fail → status="partial" →
          loader가 거부 → scene_still 0 row sync → scene_image_pipeline 차단.
          partial+데이터 있음을 syncable로 통일.
        - Codex P2-2 — completed+empty data가 정당한 zero rows로 처리되어야 한다.
          이전 가드는 fan-out 0 결과를 not syncable로 판단해 sync skip → 옛 row 잔존.
          completed는 authoritative이므로 항상 syncable이어야 cleanup 진행 가능.

    Note (Track B P1-2 partial 정책):
        호출자(EntitySyncService 등)는 partial 상태에서 stale 제거를 skip하여 데이터
        손상을 방지한다. is_cp_syncable=True여도 partial이면 cleanup 보류.
    """
    if not cp:
        return False
    status = cp.get("status")
    if status == "completed":
        return True  # authoritative — 빈 데이터도 의미 있음 (정당한 zero rows)
    if status != "partial":
        return False
    # partial: cascade 가드 (모든 핵심 list 비음이면 cascade 직후로 간주)
    data = cp.get("data", {})
    if not isinstance(data, dict):
        return False
    for key in data_keys:
        val = data.get(key)
        if isinstance(val, list) and len(val) > 0:
            return True
    return False


class BaseSyncService:
    """각 도메인 Sync Service의 공용 베이스.

    - `_load_cp(step_id)`: 체크포인트 JSON 로드 (없으면 None). 손상 시 raise (baseline 동작 보존).
    - `_is_step_completed(step_id)`: step_run에서 완료 여부 확인.
    - `self.now`: 현재 시각 (ISO). 오케스트레이터가 주입하면 같은 sync 내에서 모든 Service가 공유.
    - 서브클래스는 `sync_from_checkpoint()` 공통 인터페이스 제공.
    - 커밋은 하지 않음 (오케스트레이터가 최종 커밋).
    """

    def __init__(
        self,
        db: OrmSession,
        project_id: str,
        episode_id: str,
        *,
        now: Optional[str] = None,
    ):
        self.db = db
        self.project_id = project_id
        self.episode_id = episode_id
        # Codex Phase 2 Item 1: 한 sync 내 모든 Service가 동일 now 공유 (baseline 동작).
        self.now = now or datetime.now(timezone.utc).isoformat()
        self.logger = logging.getLogger(self.__class__.__module__)

    def _load_cp(self, step_id: str) -> Optional[Dict[str, Any]]:
        """체크포인트 로드. 없으면 None, 손상 시 raise (baseline 동작).

        baseline `_sync_checkpoints_to_db`의 `_load_cp`가 `json.loads`를 직접 호출하여
        손상된 JSON은 `JSONDecodeError` 전파. Codex Phase 2 Item 2 반영.
        """
        cp = (
            Path(settings.projects_dir) / self.project_id
            / "checkpoints" / "episodes" / self.episode_id / step_id / "manifest.json"
        )
        if not cp.exists():
            return None
        return json.loads(cp.read_text(encoding="utf-8"))

    def _is_step_completed(self, step_id: str) -> bool:
        row = self.db.execute(sql_text(
            "SELECT status FROM step_run WHERE project_id = :pid AND episode_id = :eid AND step_id = :sid"
        ), {"pid": self.project_id, "eid": self.episode_id, "sid": step_id}).fetchone()
        return bool(row and row[0] == "completed")
