#!/usr/bin/env python3
"""Stuck bot status watchdog — processing 상태가 30분 이상 지속되면 자동 idle 전환

프로세스 생존 체크 추가: 실제 claude 프로세스가 살아있으면 idle 전환 보류

Usage:
    python3 scripts/bot-status-watchdog.py          # 1회 실행
    python3 scripts/bot-status-watchdog.py --daemon # 5분마다 반복

Log: /home/jay/workspace/logs/bot-watchdog.log
"""

import argparse
import json
import os
import subprocess
import sys
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path

WORKSPACE_ROOT = os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")
BOT_ACTIVITY_FILE = Path(f"{WORKSPACE_ROOT}/memory/events/bot-activity.json")
WATCHDOG_LOG = Path(f"{WORKSPACE_ROOT}/logs/bot-watchdog.log")
EVENTS_DIR = Path(f"{WORKSPACE_ROOT}/memory/events")
REPORTS_DIR = Path(f"{WORKSPACE_ROOT}/memory/reports")
DAEMON_INTERVAL = 300  # 5분
TIMEOUT_MINUTES = 30  # 30분 타임아웃

KST = timezone(timedelta(hours=9))

# 봇별 workspace 경로 매핑 (org_loader에서 동적 로드)
try:
    import sys as _sys

    if WORKSPACE_ROOT not in _sys.path:
        _sys.path.insert(0, WORKSPACE_ROOT)
    from utils.org_loader import get_dev_short_ids as _get_dev_short_ids

    _patterns: dict[str, str] = {"anu": "workspace/autoset"}
    _patterns.update({sid: f"teams/{sid}" for sid in _get_dev_short_ids()})
    BOT_WORKSPACE_PATTERNS: dict[str, str] = _patterns
except ImportError:
    BOT_WORKSPACE_PATTERNS = {
        "anu": "workspace/autoset",
        "dev1": "teams/dev1",
        "dev2": "teams/dev2",
        "dev3": "teams/dev3",
    }


def log_watchdog(message: str) -> None:
    """워치독 로그 기록"""
    ts = datetime.now(KST).isoformat()
    line = f"[{ts}] {message}\n"
    try:
        WATCHDOG_LOG.parent.mkdir(parents=True, exist_ok=True)
        with open(WATCHDOG_LOG, "a", encoding="utf-8") as f:
            f.write(line)
    except OSError:
        pass


def load_bot_activity() -> dict:
    """bot-activity.json 로드"""
    try:
        if not BOT_ACTIVITY_FILE.exists():
            return {"bots": {}}
        with open(BOT_ACTIVITY_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError) as e:
        log_watchdog(f"ERROR: bot-activity.json 로드 실패: {e}")
        return {"bots": {}}


def save_bot_activity(data: dict) -> bool:
    """bot-activity.json 저장 (원자적 쓰기)"""
    try:
        BOT_ACTIVITY_FILE.parent.mkdir(parents=True, exist_ok=True)
        # 임시 파일에 쓰기
        temp_file = BOT_ACTIVITY_FILE.with_suffix(".tmp")
        with open(temp_file, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
        # 원자적 이동
        temp_file.replace(BOT_ACTIVITY_FILE)
        return True
    except OSError as e:
        log_watchdog(f"ERROR: bot-activity.json 저장 실패: {e}")
        return False


def parse_since_time(since_str: str | None) -> datetime | None:
    """since 필드 파싱 (ISO 8601)"""
    if since_str is None:
        return None
    try:
        # UTC 형식: 2026-03-17T06:54:35Z
        if since_str.endswith("Z"):
            return datetime.strptime(since_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
        # KST 형식: 2026-03-17T06:54:35+09:00
        return datetime.fromisoformat(since_str)
    except (ValueError, TypeError):
        return None


def find_bot_process(bot_name: str) -> list[int]:
    """해당 봇의 claude 프로세스 PID 목록 반환

    Args:
        bot_name: 봇 이름 (anu, dev1, dev2, dev3)

    Returns:
        PID 목록 (프로세스 없으면 빈 리스트)
    """
    pattern = BOT_WORKSPACE_PATTERNS.get(bot_name, "")
    if not pattern:
        return []

    try:
        result = subprocess.run(
            ["pgrep", "-f", f"claude.*{pattern}"],
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode != 0:
            return []

        pids = []
        for line in result.stdout.strip().split("\n"):
            line = line.strip()
            if line:
                try:
                    pids.append(int(line))
                except ValueError:
                    pass
        return pids
    except (subprocess.TimeoutExpired, OSError) as e:
        log_watchdog(f"WARN: {bot_name} 프로세스 체크 실패: {e}")
        return []


def find_recent_done_file(bot_name: str, since_time: datetime) -> Path | None:
    """해당 봇의 최근 .done 파일 찾기

    Args:
        bot_name: 봇 이름
        since_time: 작업 시작 시간

    Returns:
        .done 파일 경로 (없으면 None)
    """
    try:
        if not EVENTS_DIR.exists():
            return None

        # 해당 봇의 .done 파일 검색
        for done_file in EVENTS_DIR.glob(f"*.done"):
            # 파일 수정 시간이 since_time 이후인지 확인
            mtime = datetime.fromtimestamp(done_file.stat().st_mtime, tz=timezone.utc)
            if mtime >= since_time:
                # 파일명에서 봇 이름 확인 (task-XXX.Y.dev1.done 패턴)
                if f".{bot_name}." in done_file.name or bot_name == "anu":
                    return done_file

        return None
    except OSError:
        return None


def find_recent_report(bot_name: str, since_time: datetime) -> Path | None:
    """해당 봇의 최근 보고서 파일 찾기

    Args:
        bot_name: 봇 이름
        since_time: 작업 시작 시간

    Returns:
        보고서 파일 경로 (없으면 None)
    """
    try:
        if not REPORTS_DIR.exists():
            return None

        # 보고서 파일 검색
        for report_file in REPORTS_DIR.glob("*.md"):
            mtime = datetime.fromtimestamp(report_file.stat().st_mtime, tz=timezone.utc)
            if mtime >= since_time:
                return report_file

        return None
    except OSError:
        return None


def should_transition_to_idle(bot_name: str, since_time: datetime, elapsed_minutes: float) -> tuple[bool, str]:
    """idle 전환 여부 결정

    Args:
        bot_name: 봇 이름
        since_time: 작업 시작 시간
        elapsed_minutes: 경과 시간 (분)

    Returns:
        (idle 전환 여부, 사유)
    """
    # 30분 미만이면 전환 안 함
    if elapsed_minutes <= TIMEOUT_MINUTES:
        return False, "not_timeout"

    # 1. 프로세스 생존 확인
    pids = find_bot_process(bot_name)
    if pids:
        return False, f"still_running - 프로세스 살아있음 (PID: {', '.join(map(str, pids))})"

    # 2. .done 파일 확인
    done_file = find_recent_done_file(bot_name, since_time)
    if done_file:
        return True, f"completed - .done 발견 ({done_file.name})"

    # 3. 보고서 파일 확인
    report_file = find_recent_report(bot_name, since_time)
    if report_file:
        return True, f"completed - 보고서 발견 ({report_file.name})"

    # 4. 프로세스 없음 + 30분 초과 → idle 전환
    return True, f"timeout - 프로세스 없음, {int(elapsed_minutes)}분 초과"


def check_and_recover_stuck_bots() -> int:
    """stuck 상태 봇 확인 및 자동 복구

    Returns:
        복구된 봇 수
    """
    data = load_bot_activity()
    bots = data.get("bots", {})
    recovered_count = 0

    now = datetime.now(timezone.utc)

    for bot_name, bot_data in bots.items():
        status = bot_data.get("status", "idle")
        since_str = bot_data.get("since", "")

        # processing 상태만 확인
        if status != "processing":
            continue

        # since 시간 파싱
        since_time = parse_since_time(since_str)
        if not since_time:
            log_watchdog(f"WARN: {bot_name}: since 파싱 실패 ({since_str})")
            continue

        # 경과 시간 계산
        elapsed = now - since_time
        elapsed_minutes = elapsed.total_seconds() / 60

        # idle 전환 여부 결정
        should_idle, reason = should_transition_to_idle(bot_name, since_time, elapsed_minutes)

        if should_idle:
            # idle로 전환
            since_short = since_time.strftime("%H:%M")
            log_watchdog(f"[WATCHDOG] {bot_name}: processing → idle ({reason}, since {since_short})")

            data["bots"][bot_name]["status"] = "idle"
            data["bots"][bot_name]["since"] = now.strftime("%Y-%m-%dT%H:%M:%SZ")
            recovered_count += 1
        else:
            # idle 전환 보류 로그 (10분 단위로만 로그)
            if int(elapsed_minutes) % 10 == 0 and int(elapsed_minutes) > 0:
                since_short = since_time.strftime("%H:%M")
                log_watchdog(f"[WATCHDOG] {bot_name}: 30분 초과 (since {since_short}) — {reason}")

    # 변경사항 저장
    if recovered_count > 0:
        if save_bot_activity(data):
            log_watchdog(f"[WATCHDOG] {recovered_count}개 봇 복구 완료")
        else:
            log_watchdog("ERROR: 복구 후 저장 실패")
            return 0

    return recovered_count


def run_once() -> None:
    """1회 실행"""
    log_watchdog("[WATCHDOG] 1회 실행 시작")
    recovered = check_and_recover_stuck_bots()
    if recovered == 0:
        log_watchdog("[WATCHDOG] stuck 봇 없음")
    log_watchdog("[WATCHDOG] 1회 실행 완료")


def run_daemon() -> None:
    """데몬 모드 (5분마다 반복)"""
    log_watchdog("[WATCHDOG] 데몬 모드 시작")
    print(f"[WATCHDOG] 데몬 모드 시작 (간격: {DAEMON_INTERVAL}초)")

    while True:
        try:
            recovered = check_and_recover_stuck_bots()
            if recovered > 0:
                print(f"[WATCHDOG] {recovered}개 봇 복구")
        except Exception as e:
            log_watchdog(f"ERROR: 예외 발생: {e}")

        time.sleep(DAEMON_INTERVAL)


def main() -> None:
    parser = argparse.ArgumentParser(description="Bot status watchdog")
    parser.add_argument("--daemon", action="store_true", help="데몬 모드 (5분마다 반복)")
    args = parser.parse_args()

    if args.daemon:
        run_daemon()
    else:
        run_once()


if __name__ == "__main__":
    main()
