#!/usr/bin/env python3
"""bot-activity.json 실시간 감시 → 완료 즉시 감지 시스템

bot-activity.json의 processing → idle 전환을 실시간 감지하여:
1. .done 파일이 존재하면 → 즉시 텔레그램 알림
2. done-protocol.log에 이벤트 기록

Usage:
    python3 scripts/activity-watcher.py
"""

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

import requests

# scripts/ 디렉토리를 sys.path에 추가 (report_utils import용)
_SCRIPTS_DIR = Path(__file__).resolve().parent
if str(_SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(_SCRIPTS_DIR))

from report_utils import format_notification_message as _format_notification_message  # noqa: E402

# 설정
WORKSPACE_ROOT = os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")
BOT_ACTIVITY_FILE = Path(f"{WORKSPACE_ROOT}/memory/events/bot-activity.json")
EVENTS_DIR = Path(f"{WORKSPACE_ROOT}/memory/events")
DONE_PROTOCOL_LOG = Path(f"{WORKSPACE_ROOT}/logs/done-protocol.log")
PID_FILE = Path(f"{WORKSPACE_ROOT}/logs/activity-watcher.pid")
POLL_INTERVAL = 3  # 초
WATCHDOG_CHECK_INTERVAL = 300  # 워치독 체크 간격 (5분)
TIMEOUT_MINUTES = 30  # processing 타임아웃 (30분)

KST = timezone(timedelta(hours=9))

# 봇 → 팀 매핑 (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 build_bot_team_map as _build_bot_team_map

    BOT_TEAM_MAP: dict[str, str | None] = {
        **_build_bot_team_map(),
        "anu-direct": None,
    }
except ImportError:
    BOT_TEAM_MAP = {
        "dev1": "dev1",
        "dev2": "dev2",
        "dev3": "dev3",
        "dev4": "dev4",
        "dev5": "dev5",
        "dev6": "dev6",
        "dev7": "dev7",
        "dev8": "dev8",
        "anu": None,
        "anu-direct": None,
    }


def log_protocol(message: str) -> None:
    """done-protocol.log에 기록"""
    ts = datetime.now(KST).isoformat()
    line = f"[{ts}] [activity-watcher] {message}\n"
    try:
        DONE_PROTOCOL_LOG.parent.mkdir(parents=True, exist_ok=True)
        with open(str(DONE_PROTOCOL_LOG), "a", encoding="utf-8") as f:
            f.write(line)
    except OSError:
        pass  # 로그 실패는 무시


def parse_since_time(since_str: str) -> datetime | None:
    """since 필드 파싱 (ISO 8601)"""
    try:
        if since_str.endswith("Z"):
            return datetime.strptime(since_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
        return datetime.fromisoformat(since_str)
    except (ValueError, TypeError):
        return None


def check_and_recover_stuck_bots(data: dict) -> int:
    """stuck 상태 봇 확인 및 자동 복구 (워치독 로직)

    Args:
        data: bot-activity.json 데이터

    Returns:
        복구된 봇 수
    """
    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:
            continue

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

        # 30분 초과 시 idle로 전환
        if elapsed_minutes > TIMEOUT_MINUTES:
            log_protocol(f"[WATCHDOG] {bot_name}: processing → idle (timeout {int(elapsed_minutes)}m)")
            data["bots"][bot_name]["status"] = "idle"
            data["bots"][bot_name]["since"] = now.strftime("%Y-%m-%dT%H:%M:%SZ")
            recovered_count += 1

    return recovered_count


def save_bot_activity(data: dict) -> bool:
    """bot-activity.json 저장 (원자적 쓰기)"""
    try:
        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_protocol(f"ERROR: bot-activity.json 저장 실패: {e}")
        return False


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_protocol(f"ERROR: bot-activity.json 로드 실패: {e}")
        return {"bots": {}}


def update_bot_since(bot_name: str) -> None:
    """봇의 since 필드를 현재 UTC 시각으로 갱신."""
    try:
        # 현재 데이터 로드
        data = load_bot_activity()
        if bot_name not in data.get("bots", {}):
            return

        # since를 현재 UTC 시각으로 갱신
        utc_now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        data["bots"][bot_name]["since"] = utc_now

        # 파일에 저장
        BOT_ACTIVITY_FILE.parent.mkdir(parents=True, exist_ok=True)
        with open(BOT_ACTIVITY_FILE, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, ensure_ascii=False)

        log_protocol(f"{bot_name}: since 갱신 → {utc_now}")
    except Exception as e:
        log_protocol(f"ERROR: since 갱신 실패 (bot={bot_name}): {e}")


def get_active_task_for_team(team: str) -> str | None:
    """task-timers.json에서 해당 팀의 활성 task_id 조회"""
    try:
        timer_file = Path(f"{WORKSPACE_ROOT}/memory/task-timers.json")
        if not timer_file.exists():
            return None
        data = json.loads(timer_file.read_text(encoding="utf-8"))
        for task_id, task_data in data.get("tasks", {}).items():
            if task_data.get("team_id") == f"{team}-team" and task_data.get("status") == "running":
                return task_id
        # dev1 형태로도 매칭 시도
        for task_id, task_data in data.get("tasks", {}).items():
            if task_data.get("team_id") == team and task_data.get("status") == "running":
                return task_id
    except Exception:
        pass
    return None


def find_done_file(bot_name: str) -> Path | None:
    """해당 봇의 팀 기반 .done 파일 찾기"""
    team = BOT_TEAM_MAP.get(bot_name)
    if not team:
        return None
    processed_exts = [".acked", ".clear", ".notified", ".escalated", ".processing"]
    active_task = get_active_task_for_team(team)
    if active_task:
        done_file = EVENTS_DIR / f"{active_task}.done"
        if (
            done_file.exists()
            and not done_file.is_symlink()
            and not any((EVENTS_DIR / f"{active_task}.done{ext}").exists() for ext in processed_exts)
        ):
            return done_file
    return None


def send_telegram_notification(message: str, chat_id: str) -> bool:
    """직접 Telegram Bot API 호출 (토큰 0, Claude 세션 미생성)"""
    bot_token = os.environ.get("ANU_BOT_TOKEN")
    if not bot_token:
        log_protocol("WARN: ANU_BOT_TOKEN 미설정, 알림 스킵")
        return False

    url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
    payload = {"chat_id": chat_id, "text": message}

    for attempt in range(3):
        try:
            resp = requests.post(url, json=payload, timeout=10)
            if resp.status_code == 200:
                return True
            if resp.status_code == 429:
                retry_after = int(resp.headers.get("Retry-After", 5))
                time.sleep(retry_after)
                continue
            log_protocol(f"WARN: Telegram API {resp.status_code}")
        except requests.RequestException as e:
            log_protocol(f"WARN: Telegram 전송 실패 (attempt {attempt + 1}): {e}")
            time.sleep(2**attempt)

    return False


def check_already_notified(task_id: str) -> bool:
    """.done.notified 마커 파일 존재 확인"""
    notified_file = EVENTS_DIR / f"{task_id}.done.notified"
    return notified_file.exists()


def write_pid_file() -> None:
    """PID 파일 작성"""
    try:
        PID_FILE.parent.mkdir(parents=True, exist_ok=True)
        with open(PID_FILE, "w") as f:
            f.write(str(os.getpid()))
    except OSError:
        pass


def main() -> None:
    """메인 루프"""
    # 환경변수 로드
    env_file = Path(f"{WORKSPACE_ROOT}/.env.keys")
    if env_file.exists():
        # .env.keys 파일을 읽어서 환경변수로 설정
        try:
            with open(env_file, "r") as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith("#") and "=" in line:
                        key, _, value = line.partition("=")
                        os.environ[key.strip()] = value.strip().strip('"').strip("'")
        except OSError:
            pass

    chat_id = os.environ.get("COKACDIR_CHAT_ID", "6937032012")

    if not os.environ.get("ANU_BOT_TOKEN"):
        print(
            "[FATAL] ANU_BOT_TOKEN 환경변수가 설정되지 않았습니다. "
            "해결: 'source /home/jay/workspace/.env.keys'를 실행하거나 start-activity-watcher.sh를 사용하세요.",
            file=sys.stderr,
        )
        sys.exit(1)

    # PID 파일 작성
    write_pid_file()

    log_protocol("activity-watcher 시작")

    # 초기 상태 로드 (시작 즉시 오알림 방지)
    prev_activity = load_bot_activity()
    prev_statuses = {}
    for bot_name, bot_data in prev_activity.get("bots", {}).items():
        prev_statuses[bot_name] = bot_data.get("status", "idle")

    log_protocol(f"초기 상태 로드 완료: {prev_statuses}")

    # 메인 루프
    last_watchdog_check = time.time()

    while True:
        try:
            # 워치독 체크 (5분마다)
            current_time = time.time()
            if current_time - last_watchdog_check >= WATCHDOG_CHECK_INTERVAL:
                curr_activity = load_bot_activity()
                recovered = check_and_recover_stuck_bots(curr_activity)
                if recovered > 0:
                    save_bot_activity(curr_activity)
                    log_protocol(f"[WATCHDOG] {recovered}개 봇 복구 완료")
                last_watchdog_check = current_time

            # bot-activity.json 로드
            curr_activity = load_bot_activity()
            curr_statuses = {}
            for bot_name, bot_data in curr_activity.get("bots", {}).items():
                curr_statuses[bot_name] = bot_data.get("status", "idle")

            # processing → idle 전환 감지
            for bot_name, curr_status in curr_statuses.items():
                prev_status = prev_statuses.get(bot_name, "idle")

                # 아누는 무시
                if bot_name == "anu":
                    continue

                # processing → idle 전환 감지
                if prev_status == "processing" and curr_status == "idle":
                    log_protocol(f"{bot_name}: processing → idle 전환 감지")

                    # since 필드를 현재 UTC 시각으로 갱신
                    update_bot_since(bot_name)

                    # .done 파일 검색
                    done_file = find_done_file(bot_name)
                    if done_file:
                        task_id = done_file.stem  # .done 제외한 파일명

                        # 이미 알림 보냈는지 확인
                        if check_already_notified(task_id):
                            log_protocol(f"{task_id}: 이미 알림 전송됨, 스킵")
                        else:
                            # .done 파일에서 done_data 파싱
                            team = BOT_TEAM_MAP.get(bot_name, bot_name)
                            done_data = None
                            done_file_path = done_file
                            try:
                                done_data = json.loads(done_file_path.read_text(encoding="utf-8"))
                            except (json.JSONDecodeError, OSError):
                                pass
                            # team_id를 done_data에 포함
                            if done_data is None:
                                done_data = {}
                            if "team_id" not in done_data:
                                done_data["team_id"] = f"{team}-team" if team else None
                            report_path = Path(f"{WORKSPACE_ROOT}/memory/reports/{task_id}.md")
                            message = _format_notification_message(task_id, report_path, done_data)

                            # 텔레그램 알림 전송
                            log_protocol(f"{task_id}: 완료 감지 (bot={bot_name}, idle 전환)")
                            send_telegram_notification(message, chat_id)
                    else:
                        log_protocol(f"{bot_name}: idle 전환, .done 없음 (비정상 종료?)")

            # 현재 상태를 이전 상태로 저장
            prev_statuses = curr_statuses.copy()

            # 다음 폴링까지 대기
            time.sleep(POLL_INTERVAL)

        except KeyboardInterrupt:
            log_protocol("activity-watcher 종료 (SIGINT)")
            break
        except Exception as e:
            log_protocol(f"ERROR: 예외 발생: {e}")
            time.sleep(POLL_INTERVAL)  # 에러 후에도 계속 실행


if __name__ == "__main__":
    main()
