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

task-2471 1차 hardening 회귀 가드의 일환으로 헤임달이 작성.
task-2472 보강: schema 6필드 → 10필드 확장. 기존 코드 backward-compatible 유지.

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

Schema (each JSONL line) — task-2472 확장 10필드::

    {
      "task_id": str,
      "actor": str,
      "action": str,
      "input_state": str,       # 기존 from_state 호환
      "output_state": str,      # 기존 to_state 호환
      "approval_evidence": dict | None,
      "result": str,            # "ok" | "rejected"
      "reason": str,
      "timestamp": str,         # 기존 ts 호환
      "evidence_hash": str,     # sha256
      # 하위호환 필드
      "from_state": str,
      "to_state": str,
      "ts": str,
      "evidence_paths": [str],
      "detail": {"sha256_before": ..., "sha256_after": ...},
    }

Usage (기존 append_recovery 호환)::

    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"],
    )

Usage (신규 record_recovery_audit)::

    from utils.audit_chairman_recovery import record_recovery_audit
    p = record_recovery_audit(
        task_id="task-2472",
        actor="thor",
        action="manual_recovery",
        input_state="RECOVERABLE_BLOCKED",
        output_state="MERGING",
        approval_evidence={"approved_by": "chairman", "ts": "...", "evidence_path": "..."},
        result="ok",
        reason="branch protection 해제 확인",
        sha256_before="abc...",
        sha256_after="def...",
    )

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

import hashlib
import json
import os
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")

# task-2472 신규 default path (state-recovery.jsonl)
STATE_RECOVERY_JSONL_PATH = Path("memory/orchestration-audit/state-recovery.jsonl")

# Schema 키 (검증용) — 기존 6필드 유지.
RECOVERY_RECORD_KEYS: tuple[str, ...] = (
    "task_id",
    "ts",
    "from_state",
    "to_state",
    "reason",
    "evidence_paths",
)

# task-2472 확장 10필드
EXTENDED_RECORD_KEYS: tuple[str, ...] = (
    "task_id",
    "actor",
    "action",
    "input_state",
    "output_state",
    "approval_evidence",
    "result",
    "reason",
    "timestamp",
    "evidence_hash",
)


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


# ---------------------------------------------------------------------------
# task-2472 신규 API — 10필드 확장 schema
# ---------------------------------------------------------------------------


def evidence_hash(record: dict) -> str:
    """sha256(json.dumps(record, sort_keys=True, ensure_ascii=False)).hexdigest() 반환."""
    serialized = json.dumps(record, sort_keys=True, ensure_ascii=False)
    return hashlib.sha256(serialized.encode("utf-8")).hexdigest()


def record_recovery_audit(
    *,
    task_id: str,
    actor: str,
    action: str,
    input_state: str,
    output_state: str,
    approval_evidence: Optional[dict] = None,
    result: str = "ok",
    reason: str = "",
    sha256_before: Optional[str] = None,
    sha256_after: Optional[str] = None,
    audit_path: Optional[Path] = None,
    workspace: Optional[Path] = None,
) -> Path:
    """10필드 schema로 chairman recovery audit 기록.

    기존 append_recovery() backward-compatible 유지.
    신규 필드: approval_evidence, result, reason, sha256_before/after, evidence_hash.

    Parameters
    ----------
    task_id:
        ``task-NNNN`` 형식.
    actor:
        요청자 식별자.
    action:
        수행한 action (예: "manual_recovery", "state_repair").
    input_state, output_state:
        상태 전이 출발/도착.
    approval_evidence:
        chairman approval evidence dict.
        형식: {"approved_by": str, "evidence_path": str, "ts": str} 또는 None.
    result:
        "ok" | "rejected".
    reason:
        복구 사유 (인간 가독).
    sha256_before, sha256_after:
        state 파일 sha256 (before/after repair). detail로 묶여 evidence_hash에 포함.
    audit_path:
        미지정 시 STATE_RECOVERY_JSONL_PATH (state-recovery.jsonl).
        chairman-manual-recovery.jsonl 도 호환 유지.
    workspace:
        Workspace root.

    Returns
    -------
    Path
        실제 append 된 파일 경로.
    """
    if not isinstance(task_id, str) or not task_id.strip():
        raise ValueError("task_id 필수")
    if not isinstance(actor, str) or not actor.strip():
        raise ValueError("actor 필수")
    if not isinstance(action, str) or not action.strip():
        raise ValueError("action 필수")
    if not isinstance(input_state, str) or not input_state.strip():
        raise ValueError("input_state 필수")
    if not isinstance(output_state, str) or not output_state.strip():
        raise ValueError("output_state 필수")

    work_root = workspace or Path(
        os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")
    )
    # audit_path 결정
    if audit_path is None:
        target = work_root / STATE_RECOVERY_JSONL_PATH
    else:
        target = work_root / audit_path if not audit_path.is_absolute() else audit_path
    target.parent.mkdir(parents=True, exist_ok=True)

    timestamp = _now_iso()
    detail: dict = {}
    if sha256_before is not None:
        detail["sha256_before"] = sha256_before
    if sha256_after is not None:
        detail["sha256_after"] = sha256_after

    # evidence_hash 계산 전 base record
    base_record: dict = {
        "task_id": task_id.strip(),
        "actor": actor.strip(),
        "action": action.strip(),
        "input_state": input_state.strip(),
        "output_state": output_state.strip(),
        "approval_evidence": approval_evidence,
        "result": result,
        "reason": reason if isinstance(reason, str) else str(reason),
        "timestamp": timestamp,
        # 하위호환 필드
        "from_state": input_state.strip(),
        "to_state": output_state.strip(),
        "ts": timestamp,
        "evidence_paths": (
            [approval_evidence.get("evidence_path", "")]
            if isinstance(approval_evidence, dict)
            else []
        ),
    }
    if detail:
        base_record["detail"] = detail

    ev_hash = evidence_hash(base_record)
    record = {**base_record, "evidence_hash": ev_hash}

    line = json.dumps(record, ensure_ascii=False) + "\n"
    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
