#!/usr/bin/env python3
"""create_handoff.py — 봇 간 작업 인계 JSON 생성 스크립트 (task-2454 Phase 1 MVP)

사용법:
    ./scripts/create_handoff.py \\
        --task <task-id> \\
        --reason <interrupt|complete|takeover_request> \\
        [--pending "잔여 작업 요약"] \\
        [--pending-file PATH] \\
        [--failures "에러 요약"] \\
        [--failures-file PATH] \\
        [--bot devN]
"""

from __future__ import annotations

import argparse
import json
import os
import re
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path

# ---------------------------------------------------------------------------
# jsonschema 가용성 확인
# ---------------------------------------------------------------------------
try:
    import jsonschema
except ImportError:
    print(
        "오류: jsonschema 패키지가 필요합니다. pip install jsonschema",
        file=sys.stderr,
    )
    sys.exit(1)

# ---------------------------------------------------------------------------
# 상수
# ---------------------------------------------------------------------------
PENDING_MAX_LEN = 4000
FAILURES_MAX_LEN = 4000
SUBPROCESS_TIMEOUT = 10  # seconds

# ---------------------------------------------------------------------------
# 워크스페이스 루트 결정
# ---------------------------------------------------------------------------

def get_workspace_root() -> Path:
    """WORKSPACE_ROOT 환경변수 우선, 없으면 이 스크립트 기준 상위 디렉토리."""
    env_root = os.environ.get("WORKSPACE_ROOT")
    if env_root:
        return Path(env_root).resolve()
    # scripts/create_handoff.py → 상위가 워크스페이스 루트
    return Path(__file__).resolve().parent.parent


WORKSPACE_ROOT = get_workspace_root()

# ---------------------------------------------------------------------------
# Git 유틸
# ---------------------------------------------------------------------------

def run_git(*args: str) -> str:
    """git 명령 실행 후 stdout 반환. 실패 시 RuntimeError."""
    result = subprocess.run(
        ["git", *args],
        cwd=WORKSPACE_ROOT,
        capture_output=True,
        text=True,
        timeout=SUBPROCESS_TIMEOUT,
    )
    if result.returncode != 0:
        raise RuntimeError(
            f"git {' '.join(args)} 실패 (exit {result.returncode}): {result.stderr.strip()}"
        )
    return result.stdout.strip()


def get_base_sha() -> str:
    """origin/main HEAD SHA 반환."""
    return run_git("rev-parse", "origin/main")


def get_head_sha() -> str:
    """현재 브랜치 HEAD SHA 반환."""
    return run_git("rev-parse", "HEAD")


def get_current_branch() -> str:
    """현재 브랜치 이름 반환."""
    return run_git("branch", "--show-current")


def get_changed_paths() -> list[str]:
    """origin/main..HEAD 사이 변경된 파일 목록."""
    output = run_git("diff", "--name-only", "origin/main..HEAD")
    if not output:
        return []
    return [p for p in output.splitlines() if p]


def extract_bot_from_branch(branch: str) -> str | None:
    """브랜치 이름 task/task-X-devN 에서 devN 추출."""
    m = re.search(r"-(dev\d+|design|qc)$", branch)
    return m.group(1) if m else None


# ---------------------------------------------------------------------------
# Task 파일 파싱 (allowed_resources YAML 블록)
# ---------------------------------------------------------------------------

def parse_task_paths(task_id: str) -> tuple[list[str], list[str]]:
    """memory/tasks/<task-id>.md 에서 allowed_resources 파싱.
    실패 시 빈 배열 반환 (오류 발생 안 함).
    """
    task_file = WORKSPACE_ROOT / "memory" / "tasks" / f"{task_id}.md"
    if not task_file.exists():
        return [], []

    try:
        content = task_file.read_text(encoding="utf-8")
    except OSError:
        return [], []

    allowed: list[str] = []
    forbidden: list[str] = []

    # 간단한 YAML 블록 파싱: allowed_resources: 섹션 아래 "- path" 항목
    in_allowed = False
    in_forbidden = False
    for line in content.splitlines():
        stripped = line.strip()
        if stripped.startswith("allowed_resources:"):
            in_allowed = True
            in_forbidden = False
            continue
        if stripped.startswith("forbidden_paths:") or stripped.startswith("forbidden_resources:"):
            in_forbidden = True
            in_allowed = False
            continue
        if stripped and not stripped.startswith("-") and ":" in stripped:
            # 새 키 시작 → 섹션 종료
            if in_allowed or in_forbidden:
                in_allowed = False
                in_forbidden = False
            continue
        if stripped.startswith("-"):
            item = stripped[1:].strip().strip('"').strip("'")
            if in_allowed and item:
                allowed.append(item)
            elif in_forbidden and item:
                forbidden.append(item)

    return allowed, forbidden


# ---------------------------------------------------------------------------
# 4000자 초과 처리
# ---------------------------------------------------------------------------

def handle_long_text(
    text: str,
    field_name: str,
    task_id: str,
    max_len: int,
) -> tuple[str | None, str | None]:
    """텍스트가 max_len 이하면 (text, None) 반환.
    초과 시 memory/handoffs/<task>-<field>.txt 에 저장 후 (None, path_str) 반환.
    """
    if len(text) <= max_len:
        return text, None

    handoffs_dir = WORKSPACE_ROOT / "memory" / "handoffs"
    handoffs_dir.mkdir(parents=True, exist_ok=True)
    out_file = handoffs_dir / f"{task_id}-{field_name}.txt"
    out_file.write_text(text, encoding="utf-8")
    rel_path = str(out_file.relative_to(WORKSPACE_ROOT))
    return None, rel_path


# ---------------------------------------------------------------------------
# Atomic write
# ---------------------------------------------------------------------------

def atomic_write_json(path: Path, data: dict) -> None:
    """tempfile + os.replace 로 atomic JSON 저장."""
    path.parent.mkdir(parents=True, exist_ok=True)
    with tempfile.NamedTemporaryFile(
        mode="w",
        encoding="utf-8",
        dir=path.parent,
        delete=False,
        suffix=".tmp",
    ) as tmp:
        json.dump(data, tmp, ensure_ascii=False, indent=2)
        tmp_path = tmp.name
    os.replace(tmp_path, path)


# ---------------------------------------------------------------------------
# 검증 실패 evidence 저장
# ---------------------------------------------------------------------------

def save_validation_fail_evidence(task_id: str, data: dict, error: str) -> None:
    events_dir = WORKSPACE_ROOT / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)
    evidence_path = events_dir / f"{task_id}.handoff-validation-fail.json"
    evidence = {
        "task_id": task_id,
        "error": error,
        "data_snapshot": data,
        "created_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
    }
    atomic_write_json(evidence_path, evidence)
    print(f"검증 실패 evidence 저장: {evidence_path}", file=sys.stderr)


# ---------------------------------------------------------------------------
# 메인 로직
# ---------------------------------------------------------------------------

def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        description="봇 간 작업 인계 JSON 생성 스크립트",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    p.add_argument("--task", required=True, help="task-id (예: task-2454)")
    p.add_argument(
        "--reason",
        required=True,
        choices=["interrupt", "complete", "takeover_request"],
        help="인계 사유",
    )
    p.add_argument("--pending", default=None, help="잔여 작업 요약 텍스트")
    p.add_argument("--pending-file", default=None, metavar="PATH", help="잔여 작업 외부 파일 경로")
    p.add_argument("--failures", default=None, help="알려진 실패/에러 요약 텍스트")
    p.add_argument("--failures-file", default=None, metavar="PATH", help="알려진 실패 외부 파일 경로")
    p.add_argument("--bot", default=None, help="봇 ID (예: dev4). 미지정 시 브랜치에서 추출")
    return p


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()

    task_id: str = args.task
    reason: str = args.reason

    # ------------------------------------------------------------------
    # 1. Git 정보 수집
    # ------------------------------------------------------------------
    try:
        base_sha = get_base_sha()
    except RuntimeError as e:
        print(f"오류: origin/main SHA 조회 실패 — {e}", file=sys.stderr)
        return 1

    try:
        head_sha = get_head_sha()
        current_branch = get_current_branch()
        changed_paths = get_changed_paths()
    except RuntimeError as e:
        print(f"오류: git 정보 조회 실패 — {e}", file=sys.stderr)
        return 1

    # ------------------------------------------------------------------
    # 2. 봇 ID 결정
    # ------------------------------------------------------------------
    previous_bot: str | None = args.bot
    if not previous_bot:
        previous_bot = extract_bot_from_branch(current_branch)
    if not previous_bot:
        print(
            f"오류: --bot 미지정, 브랜치 '{current_branch}'에서 봇 ID 추출 실패. "
            "--bot devN 형식으로 명시하세요.",
            file=sys.stderr,
        )
        return 1

    # ------------------------------------------------------------------
    # 3. pending_work 처리
    # ------------------------------------------------------------------
    pending_inline: str | None = None
    pending_path: str | None = None

    if args.pending_file is not None:
        pf = Path(args.pending_file)
        if not pf.exists():
            print(f"오류: --pending-file 경로가 존재하지 않습니다: {pf}", file=sys.stderr)
            return 1
        pending_path = str(pf)
    elif args.pending is not None:
        pending_inline, pending_path = handle_long_text(
            args.pending, "pending", task_id, PENDING_MAX_LEN
        )

    # ------------------------------------------------------------------
    # 4. known_failures 처리
    # ------------------------------------------------------------------
    failures_inline: str | None = None
    failures_path: str | None = None

    if args.failures_file is not None:
        ff = Path(args.failures_file)
        if not ff.exists():
            print(f"오류: --failures-file 경로가 존재하지 않습니다: {ff}", file=sys.stderr)
            return 1
        failures_path = str(ff)
    elif args.failures is not None:
        failures_inline, failures_path = handle_long_text(
            args.failures, "failures", task_id, FAILURES_MAX_LEN
        )

    # ------------------------------------------------------------------
    # 5. allowed/forbidden paths
    # ------------------------------------------------------------------
    allowed_paths, forbidden_paths = parse_task_paths(task_id)

    # ------------------------------------------------------------------
    # 6. JSON 구성
    # ------------------------------------------------------------------
    created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

    handoff_data: dict = {
        "task_id": task_id,
        "schema_version": "1.0",
        "previous_bot": previous_bot,
        "current_branch": current_branch,
        "base_sha": base_sha,
        "head_sha": head_sha,
        "changed_paths": changed_paths,
        "allowed_paths": allowed_paths,
        "forbidden_paths": forbidden_paths,
        "test_results": {},
        "handoff_reason": reason,
        "created_at": created_at,
    }

    if pending_inline is not None:
        handoff_data["pending_work"] = pending_inline
    if pending_path is not None:
        handoff_data["pending_work_path"] = pending_path

    if failures_inline is not None:
        handoff_data["known_failures"] = failures_inline
    if failures_path is not None:
        handoff_data["known_failures_path"] = failures_path

    # ------------------------------------------------------------------
    # 7. 스키마 검증
    # ------------------------------------------------------------------
    schema_path = WORKSPACE_ROOT / "memory" / "specs" / "handoff-schema.json"
    if not schema_path.exists():
        print(f"오류: 스키마 파일을 찾을 수 없습니다: {schema_path}", file=sys.stderr)
        return 1

    try:
        schema = json.loads(schema_path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as e:
        print(f"오류: 스키마 파일 읽기 실패 — {e}", file=sys.stderr)
        return 1

    try:
        validator = jsonschema.Draft202012Validator(schema)
        errors = list(validator.iter_errors(handoff_data))
        if errors:
            error_msg = "; ".join(str(e.message) for e in errors)
            raise jsonschema.ValidationError(error_msg)
    except jsonschema.ValidationError as e:
        error_str = str(e.message)
        print(f"오류: 핸드오프 JSON 스키마 검증 실패 — {error_str}", file=sys.stderr)
        save_validation_fail_evidence(task_id, handoff_data, error_str)
        return 1

    # ------------------------------------------------------------------
    # 8. Atomic write
    # ------------------------------------------------------------------
    handoffs_dir = WORKSPACE_ROOT / "memory" / "handoffs"
    out_path = handoffs_dir / f"{task_id}.json"
    atomic_write_json(out_path, handoff_data)

    # ------------------------------------------------------------------
    # 9. 성공 출력
    # ------------------------------------------------------------------
    print(f"핸드오프 생성 완료: {out_path}")
    print(f"handoff_reason: {reason}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
