"""utils/state_repair.py — checksum mismatch 복구 코드 경로 + chairman approval evidence.

task-2472 구현 5: state 파일 수리는 반드시 이 모듈을 통해서만.
manual 편집 금지 — evidence + sha256 백업 + audit 없으면 fail-closed.

repair_action 종류:
  - "recompute_checksum": 기존 state 그대로 유지하고 _checksum 재계산
  - "rollback_to_backup": 가장 최신 backup 파일로 복원
  - "manual_fixup": new_state로 덮어쓰기 (위험, evidence 강력 검증)
"""
from __future__ import annotations

import hashlib
import json
import os
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

# --------------------------------------------------------------------------
# 상수 / 경로
# --------------------------------------------------------------------------

# state 파일 경로 (workspace root 상대)
STATE_DIR_REL = Path(".tasks/state")
BACKUP_DIR_REL = STATE_DIR_REL / ".backups"
VERIFY_PENDING_PREFIX = ".verify-pending-"

# audit jsonl 경로
AUDIT_JSONL_REL = Path("memory/orchestration-audit/checksum-repair.jsonl")

# --------------------------------------------------------------------------
# 내부 헬퍼
# --------------------------------------------------------------------------


def _now_iso() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _now_ts_compact() -> str:
    """파일명용 타임스탬프 (YYYYMMDDTHHMMSSZ)."""
    return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")


def _workspace_root(workspace: Optional[Path] = None) -> Path:
    return workspace or Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))


def _sha256_file(path: Path) -> str:
    """파일의 sha256 hex digest 반환."""
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


def _sha256_bytes(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


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


def _compute_checksum(state_dict: dict) -> str:
    """state dict에서 _checksum 제외한 내용의 sha256."""
    clean = {k: v for k, v in state_dict.items() if k != "_checksum"}
    serialized = json.dumps(clean, sort_keys=True, ensure_ascii=False)
    return hashlib.sha256(serialized.encode("utf-8")).hexdigest()


def _atomic_write_json(path: Path, data: dict) -> None:
    """tempfile + os.replace 방식 atomic write.

    Gemini 리뷰 medium: tempfile.mkstemp 기본 권한이 0o600이라 os.replace 후
    state 파일 권한이 제한됨. 0o644로 명시 chmod 후 replace.
    """
    path.parent.mkdir(parents=True, exist_ok=True)
    content = json.dumps(data, ensure_ascii=False, indent=2)
    fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(content)
        os.chmod(tmp, 0o644)
        os.replace(tmp, path)
    except Exception:
        try:
            os.unlink(tmp)
        except OSError:
            pass
        raise


# --------------------------------------------------------------------------
# 공개 API
# --------------------------------------------------------------------------


def inspect_state(task_id: str, *, workspace: Optional[Path] = None) -> dict:
    """.tasks/state/{task_id}.json 검사.

    Returns
    -------
    dict
        {
            "exists": bool,
            "readable": bool,
            "json_valid": bool,
            "checksum_present": bool,
            "checksum_match": bool,
            "current_state": str | None,
            "issues": [str, ...]
        }
    """
    work_root = _workspace_root(workspace)
    state_path = work_root / STATE_DIR_REL / f"{task_id}.json"
    issues: list[str] = []
    result: dict = {
        "exists": False,
        "readable": False,
        "json_valid": False,
        "checksum_present": False,
        "checksum_match": False,
        "current_state": None,
        "issues": issues,
    }

    if not state_path.exists():
        issues.append(f"state 파일 없음: {state_path}")
        return result

    result["exists"] = True

    try:
        content = state_path.read_text(encoding="utf-8")
        result["readable"] = True
    except Exception as exc:
        issues.append(f"state 파일 읽기 실패: {exc}")
        return result

    try:
        state = json.loads(content)
        result["json_valid"] = True
    except json.JSONDecodeError as exc:
        issues.append(f"state 파일 JSON 파싱 실패: {exc}")
        return result

    # current_state 추출
    result["current_state"] = state.get("state") or state.get("status")

    # checksum 검증
    stored_checksum = state.get("_checksum")
    if not stored_checksum:
        issues.append("_checksum 필드 없음")
        return result

    result["checksum_present"] = True
    computed = _compute_checksum(state)

    if computed != stored_checksum:
        issues.append(
            f"checksum mismatch: stored={stored_checksum[:12]}..., computed={computed[:12]}..."
        )
    else:
        result["checksum_match"] = True

    return result


def backup_state_file(task_id: str, *, workspace: Optional[Path] = None) -> dict:
    """.tasks/state/{task_id}.json의 sha256 + 원본 백업 보존.

    백업 위치: .tasks/state/.backups/{task_id}-{timestamp}.json

    Returns
    -------
    dict
        {"ok": bool, "backup_path": str, "sha256": str, "ts": str}
    """
    work_root = _workspace_root(workspace)
    state_path = work_root / STATE_DIR_REL / f"{task_id}.json"
    ts = _now_ts_compact()

    if not state_path.exists():
        return {
            "ok": False,
            "backup_path": "",
            "sha256": "",
            "ts": ts,
            "reason": f"state 파일 없음: {state_path}",
        }

    try:
        content = state_path.read_bytes()
    except Exception as exc:
        return {
            "ok": False,
            "backup_path": "",
            "sha256": "",
            "ts": ts,
            "reason": f"state 파일 읽기 실패: {exc}",
        }

    sha256 = _sha256_bytes(content)
    backup_dir = work_root / BACKUP_DIR_REL
    backup_dir.mkdir(parents=True, exist_ok=True)
    backup_path = backup_dir / f"{task_id}-{ts}.json"

    try:
        backup_path.write_bytes(content)
    except Exception as exc:
        return {
            "ok": False,
            "backup_path": str(backup_path),
            "sha256": sha256,
            "ts": ts,
            "reason": f"백업 파일 쓰기 실패: {exc}",
        }

    return {
        "ok": True,
        "backup_path": str(backup_path),
        "sha256": sha256,
        "ts": ts,
    }


def repair_state(
    task_id: str,
    *,
    approved_by_chairman: str,
    evidence_path: str,
    actor: str,
    repair_action: str,
    new_state: Optional[dict] = None,
    workspace: Optional[Path] = None,
) -> dict:
    """state 파일 수리 (chairman approval evidence 필수).

    fail-closed:
    - evidence_path 파일 부재 → reject
    - sha256 백업 실패 → reject
    - audit 기록 실패 → reject
    - repair 후 .verify-pending 마커 생성 (verify_consistency 호출 강제)

    Parameters
    ----------
    approved_by_chairman:
        chairman 승인자 식별자.
    evidence_path:
        approval evidence 파일 경로 (반드시 실제 파일 존재).
    actor:
        요청자.
    repair_action:
        "recompute_checksum" | "rollback_to_backup" | "manual_fixup"
    new_state:
        manual_fixup 시 새 state dict.

    Returns
    -------
    dict
        {"ok": bool, "reason": str, "audit_path": str, "backup_path": str}
    """
    work_root = _workspace_root(workspace)
    state_path = work_root / STATE_DIR_REL / f"{task_id}.json"

    # 1. evidence_path 파일 존재 확인 (fail-closed)
    ev_path = Path(evidence_path)
    if not ev_path.is_absolute():
        ev_path = work_root / ev_path
    if not ev_path.exists():
        return {
            "ok": False,
            "reason": f"evidence_path 파일 없음: {ev_path} — repair 거부 (fail-closed)",
            "audit_path": "",
            "backup_path": "",
        }

    # 2. sha256 백업 (fail-closed)
    backup_result = backup_state_file(task_id, workspace=workspace)
    if not backup_result["ok"]:
        return {
            "ok": False,
            "reason": f"sha256 백업 실패: {backup_result.get('reason', '')} — repair 거부 (fail-closed)",
            "audit_path": "",
            "backup_path": "",
        }

    input_sha256 = backup_result["sha256"]
    backup_path = backup_result["backup_path"]

    # Gemini 리뷰 medium: .verify-pending 마커를 state 파일 수정 BEFORE 생성.
    # 만약 state mutation 후 marker 생성 시점에 실패하면, state는 repair 완료된 상태인데
    # marker가 없어 verify-consistency를 스킵할 수 있다 (bypass 위험).
    # 따라서 marker를 먼저 생성 → 실패 시 repair 자체 중단. marker 성공 후에만 state 수정.
    marker_path = work_root / STATE_DIR_REL / f"{VERIFY_PENDING_PREFIX}{task_id}"
    try:
        marker_path.parent.mkdir(parents=True, exist_ok=True)
        marker_path.write_text(
            json.dumps(
                {
                    "task_id": task_id,
                    "repair_action": repair_action,
                    "actor": actor,
                    "ts": _now_iso(),
                    "stage": "pre-repair",
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )
    except Exception as exc:
        return {
            "ok": False,
            "reason": f"verify-pending 마커 사전 생성 실패: {exc} — repair 중단 (fail-closed)",
            "audit_path": "",
            "backup_path": backup_path,
        }

    # 3. repair_action 실행
    output_sha256 = ""
    repair_ok = False
    repair_reason = ""

    try:
        if repair_action == "recompute_checksum":
            # 기존 state 로드 → _checksum 재계산 → write
            if not state_path.exists():
                repair_reason = "state 파일 없음 (recompute_checksum 불가)"
            else:
                state = json.loads(state_path.read_text(encoding="utf-8"))
                state["_checksum"] = _compute_checksum(state)
                _atomic_write_json(state_path, state)
                output_sha256 = _sha256_file(state_path)
                repair_ok = True
                repair_reason = "checksum 재계산 완료"

        elif repair_action == "rollback_to_backup":
            # 가장 최신 backup 파일 복원
            backup_dir = work_root / BACKUP_DIR_REL
            backups = sorted(backup_dir.glob(f"{task_id}-*.json"), reverse=True)
            # 현재 방금 생성한 백업 제외하고 이전 백업 사용
            prev_backups = [b for b in backups if str(b) != backup_path]
            if not prev_backups:
                repair_reason = "복원할 이전 backup 파일 없음"
            else:
                latest_backup = prev_backups[0]
                content = latest_backup.read_bytes()
                state_path.write_bytes(content)
                output_sha256 = _sha256_file(state_path)
                repair_ok = True
                repair_reason = f"backup 복원 완료: {latest_backup.name}"

        elif repair_action == "manual_fixup":
            # new_state로 덮어쓰기 (위험: evidence 강력 검증)
            if new_state is None:
                repair_reason = "manual_fixup에 new_state 필수"
            else:
                # new_state에 _checksum 추가
                new_state["_checksum"] = _compute_checksum(new_state)
                _atomic_write_json(state_path, new_state)
                output_sha256 = _sha256_file(state_path)
                repair_ok = True
                repair_reason = "manual_fixup 완료 (chairman approval 적용)"
        else:
            repair_reason = f"알 수 없는 repair_action: '{repair_action}'"

    except Exception as exc:
        repair_reason = f"repair 실행 중 예외: {exc}"
        repair_ok = False

    # repair 실패 시 즉시 반환 (audit은 반드시 기록)
    result_str = "ok" if repair_ok else "rejected"

    # 4. .verify-pending 마커는 이미 사전(pre-repair) 단계에서 생성됨.
    # repair 성공 시 마커 메타데이터 update (stage 표시), 실패 시 그대로 유지.
    if repair_ok:
        try:
            marker_path.write_text(
                json.dumps(
                    {
                        "task_id": task_id,
                        "repair_action": repair_action,
                        "actor": actor,
                        "ts": _now_iso(),
                        "stage": "post-repair",
                    },
                    ensure_ascii=False,
                ),
                encoding="utf-8",
            )
        except Exception:
            # post-repair 메타 update 실패는 fail-closed 아님 — marker는 이미 존재 (verify-consistency 차단 유지).
            pass

    # 5. audit 기록 (성공/실패 모두 기록, 실패 시 fail-closed)
    try:
        audit_path = record_repair_audit(
            task_id=task_id,
            actor=actor,
            repair_action=repair_action,
            approved_by_chairman=approved_by_chairman,
            evidence_path=str(ev_path),
            input_state_sha256=input_sha256,
            output_state_sha256=output_sha256,
            backup_path=backup_path,
            result=result_str,
            reason=repair_reason,
            workspace=workspace,
        )
    except Exception as exc:
        # audit 기록 실패 → fail-closed
        return {
            "ok": False,
            "reason": f"audit 기록 실패: {exc} — fail-closed",
            "audit_path": "",
            "backup_path": backup_path,
        }

    return {
        "ok": repair_ok,
        "reason": repair_reason,
        "audit_path": str(audit_path),
        "backup_path": backup_path,
    }


def verify_consistency(task_id: str, *, workspace: Optional[Path] = None) -> dict:
    """repair 후 호출되어야 하는 후속 검증.

    체크:
    - state file checksum 일치
    - .verify-pending 마커 제거 (consistency 통과 시)
    - FAIL 시 마커 유지

    Returns
    -------
    dict
        {"ok": bool, "reason": str, "checks": {...}}
    """
    work_root = _workspace_root(workspace)
    marker_path = work_root / STATE_DIR_REL / f"{VERIFY_PENDING_PREFIX}{task_id}"
    checks: dict = {}

    # state inspect
    inspection = inspect_state(task_id, workspace=workspace)
    checks["state_inspect"] = inspection

    if not inspection["exists"]:
        return {
            "ok": False,
            "reason": "state 파일 없음 — verify-consistency FAIL",
            "checks": checks,
        }

    if not inspection["json_valid"]:
        return {
            "ok": False,
            "reason": "state 파일 JSON 파싱 실패 — verify-consistency FAIL",
            "checks": checks,
        }

    if not inspection["checksum_present"]:
        return {
            "ok": False,
            "reason": "_checksum 필드 없음 — verify-consistency FAIL",
            "checks": checks,
        }

    if not inspection["checksum_match"]:
        return {
            "ok": False,
            "reason": f"checksum mismatch — verify-consistency FAIL. issues: {inspection['issues']}",
            "checks": checks,
        }

    # 모든 검증 통과 → .verify-pending 마커 제거
    checks["checksum_ok"] = True
    if marker_path.exists():
        try:
            marker_path.unlink()
            checks["verify_pending_removed"] = True
        except Exception as exc:
            checks["verify_pending_removed"] = False
            return {
                "ok": False,
                "reason": f"verify-pending 마커 제거 실패: {exc}",
                "checks": checks,
            }
    else:
        checks["verify_pending_removed"] = False
        checks["verify_pending_note"] = "마커가 없어 제거 불필요 (정상)"

    return {
        "ok": True,
        "reason": "verify-consistency PASS: checksum 일치, verify-pending 마커 제거",
        "checks": checks,
    }


def record_repair_audit(
    *,
    task_id: str,
    actor: str,
    repair_action: str,
    approved_by_chairman: str,
    evidence_path: str,
    input_state_sha256: str,
    output_state_sha256: str,
    backup_path: str,
    result: str,
    reason: str,
    workspace: Optional[Path] = None,
) -> Path:
    """checksum repair audit 기록.

    memory/orchestration-audit/checksum-repair.jsonl 에 line append.
    필수 필드: task_id, actor, repair_action, approved_by_chairman, evidence_path,
    input_state_sha256, output_state_sha256, backup_path, result, reason, timestamp, evidence_hash
    """
    timestamp = _now_iso()
    base_record: dict = {
        "task_id": task_id,
        "actor": actor,
        "repair_action": repair_action,
        "approved_by_chairman": approved_by_chairman,
        "evidence_path": evidence_path,
        "input_state_sha256": input_state_sha256,
        "output_state_sha256": output_state_sha256,
        "backup_path": backup_path,
        "result": result,
        "reason": reason,
        "timestamp": timestamp,
    }
    ev_hash = _evidence_hash_str(base_record)
    record = {**base_record, "evidence_hash": ev_hash}

    work_root = _workspace_root(workspace)
    target = work_root / AUDIT_JSONL_REL
    target.parent.mkdir(parents=True, exist_ok=True)

    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
