#!/usr/bin/env python3
"""
실시간 작업 오케스트레이터
3개 팀(dev1/dev2/dev3)에 작업을 동시 배치하고, 완료 감지 시 다음 작업을 즉시 배치.

Usage:
    python3 /home/jay/workspace/orchestrator.py --tasks-file tasks.json
    python3 /home/jay/workspace/orchestrator.py --tasks-file tasks.json --dry-run
"""

import argparse
import json
import os
import subprocess
import sys
import threading
import time
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional

# 프롬프트 모듈 임포트
try:
    from prompts.team_prompts import build_prompt as _build_team_prompt
except ImportError:
    sys.path.insert(0, str(Path(__file__).parent))
    from prompts.team_prompts import build_prompt as _build_team_prompt

# ── 선택적 유틸리티 모듈 임포트 ──────────────────────────────────────────
try:
    from utils.config_loader import load_config as _load_config

    _CONFIG_LOADER_AVAILABLE = True
except ImportError:
    _load_config = None  # type: ignore[assignment]
    _CONFIG_LOADER_AVAILABLE = False

try:
    from utils.interrupt import INTERRUPT as _INTERRUPT
    from utils.interrupt import InterruptFlag  # noqa: F401 (re-exported for type hints)

    _INTERRUPT_AVAILABLE = True
except ImportError:
    _INTERRUPT = None  # type: ignore[assignment]
    _INTERRUPT_AVAILABLE = False

try:
    from utils.memory_manager import update_memory as _update_memory

    _MEMORY_MANAGER_AVAILABLE = True
except ImportError:
    _update_memory = None  # type: ignore[assignment]
    _MEMORY_MANAGER_AVAILABLE = False

try:
    from utils.checkpoint import snapshot as _checkpoint_snapshot

    _CHECKPOINT_AVAILABLE = True
except ImportError:
    _checkpoint_snapshot = None  # type: ignore[assignment]
    _CHECKPOINT_AVAILABLE = False


# ── 설정 ──────────────────────────────────────────────────────────
CHAT_ID = "6937032012"
WORKSPACE = Path("/home/jay/workspace")
TIMER_FILE = WORKSPACE / "memory" / "task-timers.json"
REPORTS_DIR = WORKSPACE / "memory" / "reports"
EVENTS_DIR = WORKSPACE / "memory" / "events"
LOG_FILE = WORKSPACE / "memory" / "orchestrator.log"
POLL_INTERVAL = 5  # 초 (fallback 폴링 간격)

TEAMS = {
    "dev1-team": {
        "key": "c38fb9955616e24d",
        "leader": "헤르메스(Hermes)",
        "role": "개발1팀장",
        "type": "direct",  # Opus 직접 코딩
    },
    "dev2-team": {
        "key": "f3e244a7f4f0d036",
        "leader": "오딘(Odin)",
        "role": "개발2팀장",
        "type": "direct",  # Opus 직접 코딩
    },
    "dev3-team": {
        "key": "a5dddf38a8c57168",
        "leader": "라(Ra)",
        "role": "개발3팀장",
        "type": "glm",  # GLM에 코딩 위임
    },
}

ANU_KEY = "c119085addb0f8b7"  # 봇A 아누 - 보고용

TEAM_ORDER = ["dev1-team", "dev2-team", "dev3-team"]


# ── 로깅 ──────────────────────────────────────────────────────────
def log(msg: str):
    """로그 파일과 stdout에 동시 출력"""
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] {msg}"
    print(line)
    try:
        LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except Exception as e:
        print(f"[LOG WRITE ERROR] {e}")


# ── cokacdir 실행 ─────────────────────────────────────────────────
def run_cokacdir(args: List[str], dry_run: bool = False) -> Optional[dict]:
    """cokacdir 명령 실행. dry_run이면 출력만."""
    cmd = ["cokacdir"] + args
    if dry_run:
        log(f"[DRY-RUN] {' '.join(cmd)}")
        return {"status": "ok", "dry_run": True}
    try:
        log(f"[EXEC] {' '.join(cmd)}")
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        stdout = result.stdout.strip()
        if stdout:
            try:
                return json.loads(stdout)
            except json.JSONDecodeError:
                log(f"[WARN] non-JSON output: {stdout}")
                return {"status": "ok", "raw": stdout}
        if result.returncode != 0:
            log(f"[ERROR] returncode={result.returncode}, stderr={result.stderr.strip()}")
            return {"status": "error", "message": result.stderr.strip()}
        return {"status": "ok"}
    except subprocess.TimeoutExpired:
        log("[ERROR] cokacdir timeout (30s)")
        return {"status": "error", "message": "timeout"}
    except Exception as e:
        log(f"[ERROR] cokacdir exception: {e}")
        return {"status": "error", "message": str(e)}


# ── 프롬프트 생성 ─────────────────────────────────────────────────
def build_prompt(task: dict, team_id: str) -> str:
    """팀별 dispatch 프롬프트 생성 (공통 모듈 위임)"""
    return _build_team_prompt(team_id, task["id"], task["desc"], task.get("level", "normal"))


# ── 작업 배치 ──────────────────────────────────────────────────────
def get_dispatch_time(delay_seconds: int = 15) -> str:
    """현재 시간 + delay_seconds 후의 절대 시간 반환"""
    from datetime import timedelta

    t = datetime.now() + timedelta(seconds=delay_seconds)
    return t.strftime("%Y-%m-%d %H:%M:%S")


def dispatch_task(task: dict, team_id: str, dry_run: bool = False) -> bool:
    """특정 팀에 작업 배치"""
    prompt = build_prompt(task, team_id)
    key = TEAMS[team_id]["key"]
    at_time = get_dispatch_time(15)

    log(f"[DISPATCH] {task['id']} → {team_id} ({TEAMS[team_id]['leader']}) at {at_time}")

    result = run_cokacdir(
        [
            "--cron",
            prompt,
            "--at",
            at_time,
            "--chat",
            CHAT_ID,
            "--key",
            key,
            "--once",
        ],
        dry_run=dry_run,
    )

    if result and result.get("status") == "ok":
        log(f"[DISPATCH OK] {task['id']} → {team_id}")
        return True
    else:
        log(f"[DISPATCH FAIL] {task['id']} → {team_id}: {result}")
        return False


def report_to_anu(message: str, dry_run: bool = False):
    """봇A(아누)를 통해 제이회장님께 상태 보고"""
    prompt = f"당신은 아누(Anu), 비서실장입니다. " f"제이회장님께 다음 내용을 간결하게 보고하세요:\n\n{message}"
    run_cokacdir(
        [
            "--cron",
            prompt,
            "--at",
            "1m",
            "--chat",
            CHAT_ID,
            "--key",
            ANU_KEY,
            "--once",
        ],
        dry_run=dry_run,
    )


# ── 이벤트 감시 ──────────────────────────────────────────────────
class EventWatcher:
    """events/ 디렉토리의 .done 파일을 감시하는 클래스.
    watchdog 사용 가능 시 inotify 기반, 아니면 폴링 fallback."""

    def __init__(self, callback):
        self.callback = callback  # .done 파일 발견 시 호출할 콜백
        self._stop = threading.Event()
        self._thread = None
        self._use_watchdog = False

        # watchdog 사용 가능 여부 확인
        try:
            from watchdog.events import FileSystemEventHandler  # type: ignore[import-not-found]
            from watchdog.observers import Observer  # type: ignore[import-not-found]

            self._use_watchdog = True
        except ImportError:
            self._use_watchdog = False

    def start(self):
        """감시 시작"""
        EVENTS_DIR.mkdir(parents=True, exist_ok=True)

        if self._use_watchdog:
            log("[WATCHER] watchdog 기반 이벤트 감시 시작")
            self._start_watchdog()
        else:
            log("[WATCHER] 폴링 기반 이벤트 감시 시작 (5초 간격)")
            self._start_polling()

    def stop(self):
        """감시 중지"""
        self._stop.set()
        if self._use_watchdog and hasattr(self, "_observer"):
            self._observer.stop()
            self._observer.join()
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=10)

    def _start_watchdog(self):
        """watchdog 기반 감시"""
        from watchdog.events import FileSystemEventHandler  # type: ignore[import-not-found]
        from watchdog.observers import Observer  # type: ignore[import-not-found]

        watcher = self

        class DoneFileHandler(FileSystemEventHandler):
            def on_created(self, event):
                if not event.is_directory and event.src_path.endswith(".done"):
                    path = Path(event.src_path)
                    log(f"[WATCHER] .done 파일 감지: {path.name}")
                    watcher.callback(path)

        self._observer = Observer()
        self._observer.schedule(DoneFileHandler(), str(EVENTS_DIR), recursive=False)
        self._observer.start()

    def _start_polling(self):
        """폴링 기반 감시 (fallback)"""
        self._thread = threading.Thread(target=self._poll_loop, daemon=True)
        self._thread.start()

    def _poll_loop(self):
        """폴링 루프: EVENTS_DIR에서 .done 파일 주기적 확인"""
        while not self._stop.is_set():
            try:
                if EVENTS_DIR.exists():
                    for f in EVENTS_DIR.iterdir():
                        if f.suffix == ".done":
                            log(f"[WATCHER] .done 파일 감지(폴링): {f.name}")
                            self.callback(f)
            except Exception as e:
                log(f"[WATCHER ERROR] {e}")
            self._stop.wait(POLL_INTERVAL)


# ── 타이머 파일 읽기 ──────────────────────────────────────────────
def load_timers() -> dict:
    """task-timers.json 로드"""
    if not TIMER_FILE.exists():
        return {"tasks": {}}
    with open(TIMER_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


def is_task_completed(task_id: str) -> bool:
    """특정 작업이 완료되었는지 확인"""
    timers = load_timers()
    task_data = timers.get("tasks", {}).get(task_id)
    return task_data is not None and task_data.get("status") == "completed"


def get_task_duration(task_id: str) -> str:
    """작업 소요 시간 반환"""
    timers = load_timers()
    task_data = timers.get("tasks", {}).get(task_id)
    if task_data and task_data.get("duration_human"):
        return task_data["duration_human"]
    return "알 수 없음"


# ── 보고서 읽기 ───────────────────────────────────────────────────
def read_report(task_id: str) -> str:
    """보고서 파일 읽기"""
    report_path = REPORTS_DIR / f"{task_id}.md"
    if report_path.exists():
        content = report_path.read_text(encoding="utf-8")
        # 요약: 처음 500자만
        if len(content) > 500:
            return content[:500] + "\n... (이하 생략)"
        return content
    return "(보고서 파일 없음)"


# ── 오케스트레이터 메인 로직 ──────────────────────────────────────
class Orchestrator:
    def __init__(self, tasks: List[dict], dry_run: bool = False):
        self.all_tasks = tasks
        self.dry_run = dry_run

        # 상태 추적
        self.pending: List[dict] = []  # 대기 중
        self.running: Dict[str, dict] = {}  # team_id → task
        self.completed: List[dict] = []  # 완료됨
        self._processing_tasks: set = set()  # 중복 처리 방지용

        self.team_available = {t: True for t in TEAM_ORDER}
        self.start_time = datetime.now()

        # ── config_loader: 설정 중앙화 ──────────────────────────────────
        self._poll_interval: int = POLL_INTERVAL  # 기본값 유지
        if _CONFIG_LOADER_AVAILABLE and _load_config is not None:
            try:
                cfg = _load_config()
                _cfg_poll = cfg.get("orchestrator.poll_interval")
                if _cfg_poll is not None:
                    self._poll_interval = int(_cfg_poll)
                    log(f"[CONFIG] poll_interval={self._poll_interval}s (config_loader)")
            except Exception as _e:
                log(f"[CONFIG] config_loader 로드 실패, 기본값 사용: {_e}")

        # ── memory_manager: 오케스트레이터 상태 스냅샷 디렉토리 준비 ───
        self._snapshot_dir = WORKSPACE / "memory" / "orchestrator-snapshots"
        if _MEMORY_MANAGER_AVAILABLE:
            try:
                self._snapshot_dir.mkdir(parents=True, exist_ok=True)
            except Exception:
                pass

    def assign_initial_tasks(self):
        """초기 작업 배치: preference 기반 + 빈 팀에 자동 배치"""
        # 모든 태스크를 pending에 넣기
        self.pending = list(self.all_tasks)

        # 1단계: team_preference가 지정된 작업 먼저 배치
        preference_tasks = [t for t in self.pending if t.get("team_preference")]
        for task in preference_tasks:
            team_id = task["team_preference"]
            if team_id in TEAMS and self.team_available.get(team_id, False):
                self._dispatch(task, team_id)

        # 2단계: preference 없는 작업을 빈 팀에 배치
        no_pref_tasks = [t for t in self.pending if not t.get("team_preference")]
        for task in list(no_pref_tasks):
            # 빈 팀 찾기
            free_team = self._find_free_team()
            if free_team:
                self._dispatch(task, free_team)
            else:
                break  # 더 이상 빈 팀 없음

    def _find_free_team(self) -> Optional[str]:
        """사용 가능한 팀 반환 (순서대로)"""
        for team_id in TEAM_ORDER:
            if self.team_available[team_id]:
                return team_id
        return None

    def _dispatch(self, task: dict, team_id: str):
        """작업 배치 실행"""
        success = dispatch_task(task, team_id, dry_run=self.dry_run)
        if success:
            self.pending.remove(task)
            self.running[team_id] = task
            self.team_available[team_id] = False

    def handle_done_event(self, done_file: Path):
        """이벤트 파일(.done) 처리"""
        # 즉시 .done → .processed로 rename하여 중복 감지 방지
        processed_file = done_file.with_suffix(".processed")
        try:
            done_file.rename(processed_file)
            log(f"[EVENT] {done_file.name} → {processed_file.name}")
        except FileNotFoundError:
            log(f"[EVENT] {done_file.name} 이미 처리됨 (파일 없음, 무시)")
            return
        except Exception as e:
            log(f"[EVENT ERROR] rename 실패: {e}")
            return

        try:
            with open(processed_file, "r", encoding="utf-8") as f:
                event_data = json.load(f)
        except Exception as e:
            log(f"[EVENT ERROR] {processed_file.name} 읽기 실패: {e}")
            return

        task_id = event_data.get("task_id", "")

        # 이미 처리 중인 task_id면 무시
        if task_id in self._processing_tasks:
            log(f"[EVENT] {task_id} 이미 처리 중 (중복 무시)")
            return
        self._processing_tasks.add(task_id)

        # 현재 실행 중인 작업에서 찾기
        for team_id, task in list(self.running.items()):
            if task["id"] == task_id:
                log(f"[EVENT] {task_id} 완료 이벤트 수신 (팀: {team_id})")
                self._handle_completion(team_id, task)
                break
        else:
            log(f"[EVENT] {task_id} 이벤트 수신했지만 실행 중인 작업에 없음 (무시)")

    def poll_completions(self):
        """완료된 작업 감지 (fallback용 - 기존 타이머 기반)"""
        completed_teams = []

        for team_id, task in list(self.running.items()):
            task_id = task["id"]
            if is_task_completed(task_id):
                completed_teams.append((team_id, task))

        for team_id, task in completed_teams:
            self._handle_completion(team_id, task)

    def _handle_completion(self, team_id: str, task: dict):
        """작업 완료 처리"""
        task_id = task["id"]
        duration = get_task_duration(task_id)
        log(f"[COMPLETED] {task_id} by {team_id} (소요: {duration})")

        # 보고서 읽기
        report = read_report(task_id)
        log(f"[REPORT] {task_id}: {report[:100]}...")

        # 상태 업데이트
        del self.running[team_id]
        self.team_available[team_id] = True
        self.completed.append(task)

        # 다음 작업 배치
        next_task = self._get_next_task(team_id)
        next_info = ""
        if next_task:
            self._dispatch(next_task, team_id)
            next_info = f"다음 배치: {next_task['id']} ({next_task['desc']}) → {team_id}"
            log(f"[NEXT] {next_info}")
        else:
            log(f"[IDLE] {team_id} 대기 (남은 작업 없음)")
            next_info = f"{team_id} 대기 중 (남은 작업 없음)"

        # 중간 보고는 로그에만 기록 (report_to_anu 호출 제거)
        log(
            f"[COMPLETION INFO] {task_id} by {team_id} ({TEAMS[team_id]['leader']}), "
            f"소요: {duration}, 다음: {next_info}, "
            f"진행 현황: 완료 {len(self.completed)}/{len(self.all_tasks)}, "
            f"진행 중 {len(self.running)}, 대기 {len(self.pending)}"
        )

    def _get_next_task(self, freed_team_id: str) -> Optional[dict]:
        """다음 배치할 작업 결정"""
        # 1순위: 해당 팀을 preference로 지정한 작업
        for task in self.pending:
            if task.get("team_preference") == freed_team_id:
                return task

        # 2순위: preference 없는 작업
        for task in self.pending:
            if not task.get("team_preference"):
                return task

        # 3순위: 아무 대기 작업 (preference 팀이 바쁠 때 다른 팀에 배치)
        if self.pending:
            return self.pending[0]

        return None

    def is_all_done(self) -> bool:
        """모든 작업 완료 여부"""
        return len(self.pending) == 0 and len(self.running) == 0

    def save_state_snapshot(self, label: str = "auto") -> bool:
        """현재 오케스트레이션 상태를 memory_manager로 스냅샷 저장.

        _MEMORY_MANAGER_AVAILABLE이 True일 때만 동작하며,
        False이면 조용히 False를 반환합니다.

        Args:
            label: 스냅샷 파일 레이블 (기본값: "auto").

        Returns:
            bool: 저장 성공 시 True, 실패 또는 미사용 시 False.
        """
        if not _MEMORY_MANAGER_AVAILABLE or _update_memory is None:
            return False
        try:
            snapshot_path = self._snapshot_dir / f"state-{label}.md"
            # ── checkpoint: 기존 스냅샷 파일 덮어쓰기 전 백업 ──────────
            if _CHECKPOINT_AVAILABLE and _checkpoint_snapshot is not None and snapshot_path.exists():
                try:
                    _checkpoint_snapshot(snapshot_path, label=f"before-{label}")
                except Exception as _ce:
                    log(f"[CHECKPOINT] 체크포인트 저장 실패 (무시): {_ce}")
            lines = [
                f"# Orchestrator State Snapshot [{label}]",
                f"- timestamp: {datetime.now().isoformat()}",
                f"- total_tasks: {len(self.all_tasks)}",
                f"- completed: {len(self.completed)}",
                f"- running: {list(self.running.keys())}",
                f"- pending: {[t['id'] for t in self.pending]}",
            ]
            content = "\n".join(lines) + "\n"
            ok = _update_memory(snapshot_path, content, max_chars=4000)
            if ok:
                log(f"[MEMORY] 상태 스냅샷 저장: {snapshot_path.name}")
            return ok
        except Exception as _e:
            log(f"[MEMORY] 스냅샷 저장 실패 (무시): {_e}")
            return False

    def final_report(self):
        """최종 종합 보고"""
        elapsed = (datetime.now() - self.start_time).total_seconds()
        elapsed_human = format_duration(elapsed)

        lines = [
            "## 오케스트레이터 최종 보고",
            f"- **총 작업 수**: {len(self.all_tasks)}",
            f"- **전체 소요 시간**: {elapsed_human}",
            "",
            "### 완료된 작업",
        ]
        for task in self.completed:
            duration = get_task_duration(task["id"])
            lines.append(f"- {task['id']}: {task['desc']} (소요: {duration})")

        report_msg = "\n".join(lines)
        log(f"[FINAL REPORT]\n{report_msg}")
        report_to_anu(report_msg, dry_run=self.dry_run)

    def print_status(self):
        """현재 상태 출력"""
        log(f"[STATUS] 완료: {len(self.completed)}, " f"진행 중: {len(self.running)}, " f"대기: {len(self.pending)}")
        for team_id, task in self.running.items():
            log(f"  - {team_id}: {task['id']} ({task['desc']})")

    def run(self):
        """메인 루프 - 이벤트 기반 감시"""
        log("=" * 60)
        log("오케스트레이터 시작 (이벤트 기반)")
        log(f"총 작업 수: {len(self.all_tasks)}")
        log(f"Dry-run: {self.dry_run}")
        log("=" * 60)

        # 초기 배치
        self.assign_initial_tasks()
        self.print_status()

        if self.is_all_done():
            log("배치할 작업이 없습니다.")
            return

        # 이벤트 감시 시작 전, 이미 존재하는 .done 파일 처리
        self._process_existing_done_files()
        # direct 타입 팀(dev1, dev2)의 완료를 task-timers.json 기반으로 감지
        self.poll_completions()

        # 이벤트 감시 시작
        watcher = EventWatcher(callback=self.handle_done_event)
        watcher.start()

        try:
            while not self.is_all_done():
                # ── interrupt 체크: INTERRUPT 플래그 감지 시 안전 종료 ──
                if _INTERRUPT_AVAILABLE and _INTERRUPT is not None:
                    try:
                        if _INTERRUPT.is_set():
                            log("[INTERRUPT] InterruptFlag 감지 - 안전 종료 시작")
                            break
                    except Exception:
                        pass

                # 메인 스레드는 주기적으로 상태 출력 + 누락 이벤트 보완
                time.sleep(self._poll_interval)
                try:
                    # 폴링 fallback: 혹시 이벤트를 놓쳤을 경우 .done 파일 재확인
                    self._process_existing_done_files()
                    # direct 타입 팀(dev1, dev2)의 완료를 task-timers.json 기반으로 감지
                    self.poll_completions()
                    self.print_status()
                except KeyboardInterrupt:
                    raise
                except Exception as e:
                    log(f"[POLL ERROR] {e}")

        except KeyboardInterrupt:
            log("[INTERRUPTED] KeyboardInterrupt - 안전 종료")
            log(f"  진행 중이던 작업: {list(self.running.keys())}")
            log(f"  미완료 대기 작업: {[t['id'] for t in self.pending]}")
        finally:
            watcher.stop()

        if self.is_all_done():
            # 전체 완료
            log("=" * 60)
            log("모든 작업 완료!")
            self.final_report()
            log("=" * 60)

    def _process_existing_done_files(self):
        """이미 존재하는 .done 파일 처리"""
        if not EVENTS_DIR.exists():
            return
        for f in sorted(EVENTS_DIR.iterdir()):
            if f.suffix == ".done":
                self.handle_done_event(f)


def format_duration(seconds: float) -> str:
    """소요 시간 포맷팅"""
    if seconds < 60:
        return f"{int(seconds)}초"
    elif seconds < 3600:
        m = int(seconds / 60)
        s = int(seconds % 60)
        return f"{m}분 {s}초"
    else:
        h = int(seconds / 3600)
        m = int((seconds % 3600) / 60)
        return f"{h}시간 {m}분"


# ── CLI ───────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(description="실시간 작업 오케스트레이터")
    parser.add_argument("--tasks-file", required=True, help="작업 목록 JSON 파일 경로")
    parser.add_argument("--dry-run", action="store_true", help="실제 cokacdir 호출 없이 테스트")
    args = parser.parse_args()

    # tasks.json 로드
    tasks_path = Path(args.tasks_file)
    if not tasks_path.exists():
        print(f"Error: {tasks_path} not found")
        sys.exit(1)

    with open(tasks_path, "r", encoding="utf-8") as f:
        data = json.load(f)

    tasks = data.get("tasks", [])
    if not tasks:
        print("Error: No tasks found in file")
        sys.exit(1)

    # 오케스트레이터 실행
    orch = Orchestrator(tasks, dry_run=args.dry_run)
    orch.run()


if __name__ == "__main__":
    main()
