"""utils/audit_chairman_recovery.py — chairman 수동 복구 audit log.

task-2471 1차 hardening 회귀 가드의 일환으로 헤임달이 작성.

Chairman (회장) 가 RECOVERABLE_BLOCKED 상태의 task 를 수동으로 복구할 때
사용하는 ``memory/orchestration-audit/chairman-manual-recovery.jsonl`` 의
schema 정의 + atomic append 헬퍼.

Schema (each JSONL line)::

    {
      "task_id": str,
      "ts": ISO 8601 UTC,
      "from_state": str,
      "to_state": str,
      "reason": str,
      "evidence_paths": [str]
    }

Usage::

    from utils.audit_chairman_recovery import append_recovery
    p = append_recovery(
        task_id="task-2471",
        from_state="RECOVERABLE_BLOCKED",
        to_state="MERGING",
        reason="branch protection 해제 확인",
        evidence_paths=[".tasks/evidence/task-2471/recovery.json"],
    )

원자적 append (``os.O_APPEND``) — 동시 호출 race-safe.
디렉토리는 ``mkdir(parents=True, exist_ok=True)`` 로 생성.
"""
from __future__ import annotations

import json
import os
import os.path
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional, Sequence

# ---------------------------------------------------------------------------
# 경로 상수 — workspace root 상대.
# 실제 절대 경로는 ``append_recovery(workspace=...)`` 인자로 동적 결정.
# ---------------------------------------------------------------------------

AUDIT_JSONL_PATH = Path("memory/orchestration-audit/chairman-manual-recovery.jsonl")

# Schema 키 (검증용).
RECOVERY_RECORD_KEYS: tuple[str, ...] = (
    "task_id",
    "ts",
    "from_state",
    "to_state",
    "reason",
    "evidence_paths",
)


def _now_iso() -> str:
    """ISO 8601 UTC (``YYYY-MM-DDTHH:MM:SSZ``)."""
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def append_recovery(
    task_id: str,
    from_state: str,
    to_state: str,
    reason: str,
    evidence_paths: Sequence[str],
    *,
    ts: Optional[str] = None,
    workspace: Optional[Path] = None,
) -> Path:
    """JSONL 한 줄 atomic append.

    Parameters
    ----------
    task_id:
        ``task-NNNN`` 형식. 빈 문자열이면 ``ValueError``.
    from_state, to_state:
        상태 전이의 출발/도착. 빈 문자열이면 ``ValueError``.
    reason:
        복구 사유 (인간 가독). 빈 문자열도 허용 (회장 재량).
    evidence_paths:
        근거 파일 경로 list. 비어 있으면 빈 리스트로 기록.
    ts:
        ISO 8601 timestamp. 미지정 시 현재 UTC.
    workspace:
        Workspace root. 기본 ``Path(os.environ['WORKSPACE_ROOT'])`` 또는
        ``/home/jay/workspace``.

    Returns
    -------
    Path
        실제 append 된 파일 경로 (``workspace/AUDIT_JSONL_PATH``).
    """
    if not isinstance(task_id, str) or not task_id.strip():
        raise ValueError("task_id 필수")
    if not isinstance(from_state, str) or not from_state.strip():
        raise ValueError("from_state 필수")
    if not isinstance(to_state, str) or not to_state.strip():
        raise ValueError("to_state 필수")

    work_root = workspace or Path(
        os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")
    )
    target = work_root / AUDIT_JSONL_PATH
    target.parent.mkdir(parents=True, exist_ok=True)

    record = {
        "task_id": task_id.strip(),
        "ts": ts or _now_iso(),
        "from_state": from_state.strip(),
        "to_state": to_state.strip(),
        "reason": reason if isinstance(reason, str) else str(reason),
        "evidence_paths": list(evidence_paths) if evidence_paths else [],
    }
    line = json.dumps(record, ensure_ascii=False) + "\n"

    # Atomic append (O_APPEND). 동시 호출 race-safe (POSIX guarantees).
    fd = os.open(str(target), os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o644)
    try:
        os.write(fd, line.encode("utf-8"))
    finally:
        os.close(fd)

    return target


def read_recoveries(
    workspace: Optional[Path] = None,
) -> list[dict]:
    """JSONL 파일 전체 읽어서 dict list 반환.

    파일 부재 시 빈 리스트. 파싱 실패 라인은 skip (graceful).
    """
    work_root = workspace or Path(
        os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")
    )
    target = work_root / AUDIT_JSONL_PATH
    if not target.exists():
        return []

    out: list[dict] = []
    with target.open("r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                out.append(json.loads(line))
            except json.JSONDecodeError:
                continue
    return out
