#!/usr/bin/env python3
""".done 파일 + .failed 파일 감시 → bot-activity idle 전환 + FAIL 알림

done-watcher는 memory/events/*.done 파일을 감시하여
해당 팀의 bot-activity.json 상태를 idle로 전환합니다.

또한 memory/events/*.failed 파일을 감시하여
아누(개발실장)에게 QC FAIL 알림을 Telegram으로 전송합니다.

이는 finish-task.sh가 호출되지 않은 경우를 대비한 방어 코드입니다.

Usage:
    python3 scripts/done-watcher.py          # 1회 실행
    python3 scripts/done-watcher.py --daemon # 데몬 모드 (30초마다)
"""

import argparse
import hashlib
import json
import os
import re
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path

import requests

WORKSPACE_ROOT = os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")
EVENTS_DIR = Path(f"{WORKSPACE_ROOT}/memory/events")
BOT_ACTIVITY_FILE = Path(f"{WORKSPACE_ROOT}/memory/events/bot-activity.json")
DONE_PROTOCOL_LOG = Path(f"{WORKSPACE_ROOT}/logs/done-protocol.log")
DAEMON_INTERVAL = 30  # 30초

KST = timezone(timedelta(hours=9))


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


def send_telegram_notification(message: str) -> bool:
    """아누(개발실장)에게 Telegram 알림 전송

    notify-completion.py의 동일 패턴 차용.
    3회 재시도 + 429 rate limit 대응.
    Markdown parse_mode 사용 + 400 에러 시 plain text fallback.
    """
    bot_token = os.environ.get("ANU_BOT_TOKEN", "")
    chat_id = os.environ.get("COKACDIR_CHAT_ID", "6937032012")

    if not bot_token:
        log_protocol("WARN: ANU_BOT_TOKEN 환경변수 미설정 — Telegram 알림 생략")
        return False

    url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
    max_retries = 3

    for attempt in range(1, max_retries + 1):
        # Markdown으로 시도
        payload = {
            "chat_id": chat_id,
            "text": message,
            "parse_mode": "Markdown",
        }
        try:
            resp = requests.post(url, json=payload, timeout=10)
            if resp.status_code == 200:
                return True
            elif resp.status_code == 429:
                retry_after = int(resp.json().get("parameters", {}).get("retry_after", 5))
                log_protocol(f"WARN: Telegram 429 rate limit — {retry_after}초 대기 (시도 {attempt}/{max_retries})")
                time.sleep(retry_after)
                continue
            elif resp.status_code == 400:
                # Markdown 파싱 실패 → plain text fallback
                plain_payload = {
                    "chat_id": chat_id,
                    "text": message,
                }
                resp2 = requests.post(url, json=plain_payload, timeout=10)
                if resp2.status_code == 200:
                    return True
                log_protocol(f"ERROR: Telegram plain text fallback 실패: {resp2.status_code} {resp2.text}")
                return False
            else:
                log_protocol(f"ERROR: Telegram 전송 실패: {resp.status_code} {resp.text} (시도 {attempt}/{max_retries})")
                if attempt < max_retries:
                    time.sleep(2)
                continue
        except requests.RequestException as e:
            log_protocol(f"ERROR: Telegram 요청 예외: {e} (시도 {attempt}/{max_retries})")
            if attempt < max_retries:
                time.sleep(2)

    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 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 extract_team_from_done_file(done_file: Path) -> str | None:
    """.done 파일명에서 팀 ID 추출

    패턴:
    - task-648.1.dev1.done → dev1
    - task-648.1.dev2.done → dev2
    - task-648.1.dev3.done → dev3
    - task-648.1.done → task-timers.json에서 조회 필요
    """
    # .done 파일명에서 팀 추출 (예: task-648.1.dev1.done)
    name = done_file.name
    match = re.match(r"task-\d+\.\d+\.(\w+)\.done$", name)
    if match:
        return match.group(1)

    # 팀 정보가 없으면 task-timers.json에서 조회
    task_id = done_file.stem.replace(".done", "")
    try:
        timer_file = Path(f"{WORKSPACE_ROOT}/memory/task-timers.json")
        if timer_file.exists():
            data = json.loads(timer_file.read_text(encoding="utf-8"))
            task_data = data.get("tasks", {}).get(task_id, {})
            return task_data.get("team_id")
    except Exception:
        pass

    return None


def set_bot_idle(team_id: str) -> bool:
    """봇 상태를 idle로 전환"""
    data = load_bot_activity()
    bots = data.get("bots", {})

    if team_id not in bots:
        log_protocol(f"WARN: team_id '{team_id}'가 bot-activity.json에 없음")
        return False

    # 이미 idle이면 스킵
    if bots[team_id].get("status") == "idle":
        return True

    # idle로 전환
    utc_now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    data["bots"][team_id]["status"] = "idle"
    data["bots"][team_id]["since"] = utc_now

    if save_bot_activity(data):
        log_protocol(f"[DONE-WATCHER] {team_id}: → idle (.done 감지)")
        return True

    return False


def scan_done_files() -> list[Path]:
    """처리되지 않은 .done 파일 스캔

    .done 파일 중 .done.acked, .done.clear 등으로 처리된 파일은 제외
    """
    processed_exts = [".acked", ".clear", ".merging", ".escalated"]
    done_files = []
    for done_file in EVENTS_DIR.glob("*.done"):
        # .done으로 끝나는 파일만 (예: task-648.1.done, task-648.1.dev1.done)
        if done_file.suffix != ".done":
            continue
        # 이미 처리된 파일 제외 (.done.acked 등의 마커 파일이 있으면 제외)
        if any((EVENTS_DIR / (done_file.name + ext)).exists() for ext in processed_exts):
            continue
        done_files.append(done_file)
    return done_files


def validate_done_file(done_file: Path) -> tuple[bool, list[str]]:
    """V-3/V-7: .done 파일 무결성 + 스키마 검증"""
    warnings = []
    try:
        data = json.loads(done_file.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError) as e:
        return False, [f"JSON 파싱 실패: {e}"]

    # V-7: 필수 키 검증
    required_keys = ["task_id", "status"]
    for key in required_keys:
        if key not in data:
            warnings.append(f"필수 키 누락: {key}")

    # V-3: QC 해시 검증
    if "qc_hash" in data:
        stored_hash = data["qc_hash"]
        hash_payload = {k: v for k, v in data.items() if k != "qc_hash"}
        hash_str = json.dumps(hash_payload, sort_keys=True, ensure_ascii=False)
        computed_hash = hashlib.sha256(hash_str.encode("utf-8")).hexdigest()
        if stored_hash != computed_hash:
            warnings.append(f"QC 해시 불일치 — 수동 생성 의심: stored={stored_hash[:16]}... computed={computed_hash[:16]}...")
    else:
        warnings.append("qc_hash 필드 없음 — QC 게이트 미사용 또는 레거시 .done")

    is_valid = len([w for w in warnings if "불일치" in w or "필수 키" in w]) == 0
    return is_valid, warnings


def process_done_files() -> int:
    """.done 파일 처리 → bot idle 전환

    Returns:
        처리된 파일 수
    """
    done_files = scan_done_files()
    processed = 0

    for done_file in done_files:
        # V-3/V-7 검증
        is_valid, warnings = validate_done_file(done_file)
        for w in warnings:
            log_protocol(f"WARN [{done_file.name}]: {w}")
        if not is_valid:
            log_protocol(f"INTEGRITY_FAIL [{done_file.name}]: 무결성 검증 실패")

        team_id = extract_team_from_done_file(done_file)
        if team_id:
            if set_bot_idle(team_id):
                processed += 1

    return processed


def scan_failed_files() -> list[Path]:
    """처리되지 않은 .failed 파일 스캔

    .failed.acked 마커 파일이 이미 존재하면 제외 (중복 알림 방지)
    """
    failed_files = []
    for failed_file in EVENTS_DIR.glob("*.failed"):
        if failed_file.suffix != ".failed":
            continue
        # .failed.acked 마커 파일이 있으면 이미 처리된 파일 → 제외
        if (EVENTS_DIR / (failed_file.name + ".acked")).exists():
            continue
        failed_files.append(failed_file)
    return failed_files


def process_failed_files() -> int:
    """.failed 파일 처리 → 아누에게 QC FAIL 알림 전송

    Returns:
        처리된 파일 수
    """
    failed_files = scan_failed_files()
    processed = 0

    for failed_file in failed_files:
        try:
            data = json.loads(failed_file.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError) as e:
            log_protocol(f"ERROR [{failed_file.name}]: JSON 파싱 실패: {e}")
            continue

        task_id = data.get("task_id", failed_file.stem)
        fail_reason = data.get("fail_reason", "QC FAIL")

        message = f"[QC FAIL] {task_id} — {fail_reason}"
        log_protocol(f"[FAILED-WATCHER] {failed_file.name}: Telegram 알림 전송 시도")

        if send_telegram_notification(message):
            acked_path = EVENTS_DIR / (failed_file.name + ".acked")
            failed_file.rename(acked_path)
            log_protocol(f"[FAILED-WATCHER] {failed_file.name}: 알림 전송 완료 → {acked_path.name}")
            processed += 1
        else:
            log_protocol(f"[FAILED-WATCHER] {failed_file.name}: 알림 전송 실패 — 다음 주기에 재시도")

    return processed


def run_once() -> None:
    """1회 실행"""
    log_protocol("[DONE-WATCHER] 1회 실행 시작")
    processed = process_done_files()
    log_protocol(f"[DONE-WATCHER] {processed}개 .done 파일 처리 완료")
    failed_processed = process_failed_files()
    log_protocol(f"[DONE-WATCHER] {failed_processed}개 .failed 파일 처리 완료")


def run_daemon() -> None:
    """데몬 모드"""
    log_protocol("[DONE-WATCHER] 데몬 모드 시작")
    print(f"[DONE-WATCHER] 데몬 모드 시작 (간격: {DAEMON_INTERVAL}초)")

    while True:
        try:
            processed = process_done_files()
            if processed > 0:
                print(f"[DONE-WATCHER] {processed}개 .done 파일 처리")
            failed_processed = process_failed_files()
            if failed_processed > 0:
                print(f"[DONE-WATCHER] {failed_processed}개 .failed 파일 처리")
        except Exception as e:
            log_protocol(f"ERROR: 예외 발생: {e}")

        time.sleep(DAEMON_INTERVAL)


def main() -> None:
    parser = argparse.ArgumentParser(description=".done 파일 + .failed 파일 감시 → bot idle 전환 + FAIL 알림")
    parser.add_argument("--daemon", action="store_true", help="데몬 모드 (30초마다)")
    args = parser.parse_args()

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


if __name__ == "__main__":
    main()
