"""auto_orch.py — Phase 3 오케스트레이터 코어 메인 모듈.

CLI 진입점 + 핵심 함수들:
  - acquire_global_lock / release_global_lock : 중복 실행 방지 (fcntl.flock)
  - load_pipeline  : YAML 파이프라인 파일 로드
  - scan_events    : incoming/ 디렉터리에서 미처리 이벤트 수집
  - get_pipeline_state / save_pipeline_state : state/*.json 관리
  - dispatch_step  : dispatch.py 서브프로세스 호출 (shell=False)
  - update_health  : health.json 갱신
  - cmd_scan / cmd_run / cmd_status / cmd_list / cmd_validate : CLI 커맨드

작성자: 토르 (dev2-team backend)
날짜: 2026-03-24
"""

import argparse
import fcntl
import json
import logging
import os
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

import requests
import yaml

# ---------------------------------------------------------------------------
# sys.path 세팅 — Phase 2 모듈 import 경로
# ---------------------------------------------------------------------------
WORKSPACE_ROOT = "/home/jay/workspace"
if WORKSPACE_ROOT not in sys.path:
    sys.path.insert(0, WORKSPACE_ROOT)

from orchestrator.event_bus import consume_event, scan_done_events  # noqa: E402
from orchestrator.pipeline_validator import validate_pipeline  # noqa: E402
from orchestrator.team_lock import TeamLock  # noqa: E402
from orchestrator.token_ledger import TokenLedger  # noqa: E402

# ---------------------------------------------------------------------------
# 모듈 레벨 상수
# ---------------------------------------------------------------------------
GLOBAL_LOCK_PATH = "/tmp/auto-orch.lock"
STATE_DIR = os.path.join(WORKSPACE_ROOT, "orchestrator/state")
PIPELINES_DIR = os.path.join(WORKSPACE_ROOT, "pipelines")
HEALTH_PATH = os.path.join(WORKSPACE_ROOT, "orchestrator/health.json")
INCOMING_DIR = os.path.join(WORKSPACE_ROOT, "orchestrator/incoming")
PROCESSED_DIR = os.path.join(WORKSPACE_ROOT, "orchestrator/processed")
EVENTS_DIR = os.path.join(WORKSPACE_ROOT, "memory/events")
ALERTS_SENT_PATH = os.path.join(WORKSPACE_ROOT, "orchestrator/state/alerts_sent.json")
TASK_TIMERS_PATH = os.path.join(WORKSPACE_ROOT, "memory/task-timers.json")
STALE_TASK_RUNNING_SECONDS = 7200  # 2시간
LEDGER_PATH = os.path.join(WORKSPACE_ROOT, "orchestrator/state/token_ledger.json")

# ---------------------------------------------------------------------------
# 로거 설정
# ---------------------------------------------------------------------------
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# __all__
# ---------------------------------------------------------------------------
__all__ = [
    "acquire_global_lock",
    "release_global_lock",
    "load_pipeline",
    "is_pipeline_enabled",
    "scan_events",
    "get_pipeline_state",
    "save_pipeline_state",
    "dispatch_step",
    "update_health",
    "send_telegram_alert",
    "check_stale_tasks",
    "cmd_scan",
    "cmd_run",
    "cmd_status",
    "cmd_list",
    "cmd_validate",
]


# ---------------------------------------------------------------------------
# 전역 flock 함수
# ---------------------------------------------------------------------------


def acquire_global_lock() -> Optional[int]:
    """GLOBAL_LOCK_PATH에 LOCK_EX|LOCK_NB flock을 획득한다.

    Returns:
        성공 시 파일 디스크립터(int), 이미 잠겨 있으면 None.
    """
    try:
        fd = os.open(GLOBAL_LOCK_PATH, os.O_CREAT | os.O_RDWR)
        fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
        logger.debug("전역 락 획득 성공: fd=%d path=%s", fd, GLOBAL_LOCK_PATH)
        return fd
    except BlockingIOError:
        logger.debug("전역 락 획득 실패 (이미 잠겨 있음): %s", GLOBAL_LOCK_PATH)
        try:
            os.close(fd)  # type: ignore[possibly-undefined]
        except Exception:
            pass
        return None
    except OSError as exc:
        logger.error("전역 락 획득 중 OS 오류: %s", exc)
        return None


def release_global_lock(fd: int) -> None:
    """fd에 대한 flock을 해제하고 파일 디스크립터를 닫는다.

    Args:
        fd: acquire_global_lock()이 반환한 파일 디스크립터.
    """
    try:
        fcntl.flock(fd, fcntl.LOCK_UN)
        logger.debug("전역 락 해제 성공: fd=%d", fd)
    finally:
        os.close(fd)


# ---------------------------------------------------------------------------
# 파이프라인 YAML 로드
# ---------------------------------------------------------------------------


def load_pipeline(yaml_path: str) -> dict:
    """YAML 파이프라인 파일을 로드하여 dict를 반환한다.

    Args:
        yaml_path: 파이프라인 YAML 파일의 절대/상대 경로.

    Returns:
        파이프라인 dict.

    Raises:
        yaml.YAMLError: YAML 구문 오류.
        OSError: 파일 읽기 오류.
    """
    logger.debug("파이프라인 로드: %s", yaml_path)
    with open(yaml_path, "r", encoding="utf-8") as fh:
        data = yaml.safe_load(fh)
    if not isinstance(data, dict):
        raise ValueError(f"파이프라인 YAML이 dict가 아닙니다: {type(data)}")
    return data


# ---------------------------------------------------------------------------
# 파이프라인 활성화 여부 확인
# ---------------------------------------------------------------------------


def is_pipeline_enabled(pipeline: dict) -> bool:
    """파이프라인 dict에서 enabled 여부를 반환한다.

    Args:
        pipeline: 파이프라인 dict (YAML 로드 결과).

    Returns:
        enabled 필드가 있으면 그 bool 값, 없으면 True (후방호환).
    """
    return bool(pipeline.get("enabled", True))


# ---------------------------------------------------------------------------
# 이벤트 스캔
# ---------------------------------------------------------------------------


def scan_events(incoming_dir: str, processed_dir: str) -> list[str]:
    """incoming_dir에서 미처리 .done 이벤트 파일 목록을 반환한다.

    이미 processed_dir에 존재하는 파일은 제외한다.

    Args:
        incoming_dir: 미처리 이벤트 파일 디렉터리.
        processed_dir: 처리 완료된 이벤트 파일 디렉터리.

    Returns:
        미처리 .done 파일명 리스트 (순수 파일명, 경로 제외).
    """
    incoming_path = Path(incoming_dir)
    processed_path = Path(processed_dir)

    if not incoming_path.exists():
        logger.warning("incoming 디렉터리가 없습니다: %s", incoming_dir)
        return []

    processed_files: set[str] = set()
    if processed_path.exists():
        processed_files = {f.name for f in processed_path.iterdir() if f.is_file()}

    results: list[str] = []
    for f in incoming_path.iterdir():
        if f.is_file() and f.suffix == ".done":
            if f.name not in processed_files:
                results.append(f.name)
                logger.debug("미처리 이벤트 발견: %s", f.name)

    logger.debug("scan_events 결과: %d개 파일", len(results))
    return results


# ---------------------------------------------------------------------------
# 파이프라인 상태 관리
# ---------------------------------------------------------------------------


def get_pipeline_state(pipeline_id: str) -> Optional[dict]:
    """STATE_DIR/<pipeline_id>.json을 로드한다.

    Args:
        pipeline_id: 파이프라인 식별자.

    Returns:
        상태 dict, 파일 없음 또는 파싱 실패 시 None.
    """
    state_file = os.path.join(STATE_DIR, f"{pipeline_id}.json")
    if not os.path.exists(state_file):
        logger.debug("파이프라인 상태 파일 없음: %s", state_file)
        return None
    try:
        with open(state_file, "r", encoding="utf-8") as fh:
            content = fh.read()
        if not content.strip():
            logger.warning("파이프라인 상태 파일이 비어 있음: %s", state_file)
            return None
        data = json.loads(content)
        if not isinstance(data, dict):
            logger.warning("파이프라인 상태가 dict가 아님: %s", state_file)
            return None
        return data
    except (json.JSONDecodeError, ValueError) as exc:
        logger.warning("파이프라인 상태 JSON 파싱 실패 (%s): %s", state_file, exc)
        return None
    except OSError as exc:
        logger.error("파이프라인 상태 파일 읽기 오류 (%s): %s", state_file, exc)
        return None


def save_pipeline_state(pipeline_id: str, state: dict) -> None:
    """STATE_DIR/<pipeline_id>.json에 상태를 저장한다.

    Args:
        pipeline_id: 파이프라인 식별자.
        state: 저장할 상태 dict.
    """
    os.makedirs(STATE_DIR, exist_ok=True)
    state_file = os.path.join(STATE_DIR, f"{pipeline_id}.json")
    with open(state_file, "w", encoding="utf-8") as fh:
        json.dump(state, fh, ensure_ascii=False, indent=2)
    logger.debug("파이프라인 상태 저장: %s", state_file)


# ---------------------------------------------------------------------------
# 스텝 디스패치
# ---------------------------------------------------------------------------


def dispatch_step(step: dict, pipeline_id: str, state: dict) -> bool:
    """dispatch.py를 서브프로세스로 호출하여 스텝을 실행한다.

    Args:
        step: 실행할 스텝 dict (id, target_team, task_file_template 등).
        pipeline_id: 파이프라인 식별자.
        state: 현재 파이프라인 상태 dict.

    Returns:
        성공(returncode==0) 시 True, 실패 시 False.
    """
    task_file = step.get("task_file_template", f"pipelines/templates/{step.get('id', 'unknown')}.md")
    target_team = step.get("target_team", "unknown-team")

    cmd: list[str] = [
        "/usr/bin/python3",
        os.path.join(WORKSPACE_ROOT, "dispatch.py"),
        "--team",
        target_team,
        "--task-file",
        task_file,
    ]
    logger.debug(
        "dispatch_step 호출: pipeline_id=%s step_id=%s team=%s cmd=%s",
        pipeline_id,
        step.get("id"),
        target_team,
        cmd,
    )

    try:
        result = subprocess.run(
            cmd,
            shell=False,
            capture_output=True,
            text=True,
        )
        if result.returncode == 0:
            logger.info("dispatch 성공: pipeline=%s step=%s", pipeline_id, step.get("id"))
            return True
        else:
            logger.warning(
                "dispatch 실패: pipeline=%s step=%s returncode=%d stderr=%s",
                pipeline_id,
                step.get("id"),
                result.returncode,
                result.stderr[:200] if result.stderr else "",
            )
            return False
    except OSError as exc:
        logger.error("dispatch 실행 오류: %s", exc)
        return False


# ---------------------------------------------------------------------------
# health.json 갱신
# ---------------------------------------------------------------------------


def update_health(active_pipelines: int, errors: int, token_usage: dict | None = None) -> None:
    """HEALTH_PATH의 health.json을 갱신한다.

    Args:
        active_pipelines: 현재 활성 파이프라인 수.
        errors: 최근 1시간 내 에러 수.
        token_usage: 토큰 사용량 dict (옵션). 있으면 version "1.1"로 기록.
    """
    health_data: dict = {
        "last_tick": datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
        "active_pipelines": active_pipelines,
        "errors_last_hour": errors,
        "version": "1.1" if token_usage is not None else "1.0",
    }
    if token_usage is not None:
        health_data["token_usage"] = token_usage
    health_file = Path(HEALTH_PATH)
    health_file.parent.mkdir(parents=True, exist_ok=True)
    health_file.write_text(json.dumps(health_data, ensure_ascii=False, indent=2), encoding="utf-8")
    logger.debug(
        "health.json 갱신: active_pipelines=%d errors=%d token_usage=%s",
        active_pipelines,
        errors,
        token_usage,
    )


# ---------------------------------------------------------------------------
# Telegram 경고 발송
# ---------------------------------------------------------------------------


def send_telegram_alert(message: str, alert_type: str) -> bool:
    """Telegram Direct API로 경고 발송 (토큰 0, 중복 방지).

    Args:
        message: 발송할 메시지 텍스트
        alert_type: 경고 유형 (예: "token_warning", "stale_task")

    Returns:
        True: 발송 성공
        False: 이미 발송됨 또는 실패
    """
    # 1. alerts_sent.json 로드 (없으면 빈 dict)
    alerts_path = Path(ALERTS_SENT_PATH)
    try:
        if alerts_path.exists():
            alerts_data: dict = json.loads(alerts_path.read_text(encoding="utf-8"))
        else:
            alerts_data = {}
    except (json.JSONDecodeError, OSError):
        alerts_data = {}

    # 2. 오늘 날짜(date key) + alert_type 조합 확인
    today_key = datetime.now(timezone.utc).date().isoformat()
    today_alerts: list = alerts_data.get(today_key, [])

    # 3. 이미 있으면 False 반환 (중복 방지)
    if alert_type in today_alerts:
        logger.debug("경고 이미 발송됨 (중복 방지): %s/%s", today_key, alert_type)
        return False

    # 4. ANU_BOT_TOKEN 환경변수 확인
    token = os.environ.get("ANU_BOT_TOKEN")
    if not token:
        logger.warning("ANU_BOT_TOKEN 환경변수가 설정되지 않음. Telegram 경고 발송 불가.")
        return False

    # 5. AUTO_ORCH_CHAT_ID 환경변수 확인
    chat_id = os.environ.get("AUTO_ORCH_CHAT_ID")
    if not chat_id:
        logger.warning("AUTO_ORCH_CHAT_ID 환경변수가 설정되지 않음. Telegram 경고 발송 불가.")
        return False

    # 6. Telegram API 호출
    try:
        resp = requests.post(
            f"https://api.telegram.org/bot{token}/sendMessage",
            json={"chat_id": chat_id, "text": message},
            timeout=10,
        )
        if not resp.ok:
            logger.warning("Telegram 경고 발송 실패: status=%d body=%s", resp.status_code, resp.text[:200])
            return False
    except requests.RequestException as exc:
        logger.error("Telegram 경고 발송 중 오류: %s", exc)
        return False

    # 7. 성공 시 alerts_sent.json에 기록 + True 반환
    today_alerts.append(alert_type)
    alerts_data[today_key] = today_alerts
    try:
        alerts_path.parent.mkdir(parents=True, exist_ok=True)
        alerts_path.write_text(json.dumps(alerts_data, ensure_ascii=False, indent=2), encoding="utf-8")
    except OSError as exc:
        logger.warning("alerts_sent.json 저장 실패: %s", exc)

    logger.info("Telegram 경고 발송 성공: %s/%s", today_key, alert_type)
    return True


# ---------------------------------------------------------------------------
# 스텝 타임아웃 감지
# ---------------------------------------------------------------------------


def check_stale_tasks() -> list[dict]:
    """task-timers.json에서 STALE_TASK_RUNNING_SECONDS 초과 running 태스크 목록 반환.

    Returns:
        [{"task_id": str, "team_id": str, "duration_seconds": float}, ...]
    """
    # 1. TASK_TIMERS_PATH 로드 (없으면 빈 리스트)
    timers_path = Path(TASK_TIMERS_PATH)
    try:
        if not timers_path.exists():
            return []
        raw = timers_path.read_text(encoding="utf-8")
        timers_data: dict = json.loads(raw)
    except (json.JSONDecodeError, OSError):
        return []

    # task-timers.json은 {"tasks": {...}} 형식 또는 flat dict 형식 지원
    if isinstance(timers_data, dict) and "tasks" in timers_data:
        tasks: dict = timers_data["tasks"]
    elif isinstance(timers_data, dict):
        tasks = timers_data
    else:
        tasks = {}
    stale: list[dict] = []
    now = datetime.now(timezone.utc)

    # 2. tasks 딕셔너리 순회
    for task_id, task_info in tasks.items():
        if not isinstance(task_info, dict):
            continue

        # 3. status == "running"이고 start_time이 있는 항목 필터
        if task_info.get("status") != "running":
            continue
        start_time_str = task_info.get("start_time")
        if not start_time_str:
            continue

        # 4. 현재시각 - start_time > STALE_TASK_RUNNING_SECONDS이면 목록에 추가
        try:
            # 5. start_time은 ISO format 파싱
            start_time = datetime.fromisoformat(start_time_str)
            # timezone-naive인 경우 UTC로 간주
            if start_time.tzinfo is None:
                start_time = start_time.replace(tzinfo=timezone.utc)
            duration_seconds = (now - start_time).total_seconds()
        except (ValueError, TypeError):
            logger.debug("start_time 파싱 실패: task_id=%s start_time=%s", task_id, start_time_str)
            continue

        if duration_seconds > STALE_TASK_RUNNING_SECONDS:
            team_id = task_info.get("team_id", "unknown")
            stale.append(
                {
                    "task_id": task_id,
                    "team_id": team_id,
                    "duration_seconds": duration_seconds,
                }
            )
            logger.debug("스텝 타임아웃 감지: task_id=%s team_id=%s duration=%.1fs", task_id, team_id, duration_seconds)

    return stale


# ---------------------------------------------------------------------------
# CLI 커맨드
# ---------------------------------------------------------------------------


def cmd_scan() -> None:
    """--scan: 전역 락 획득 → 이벤트 소비 → 파이프라인 스텝 디스패치 → health 갱신."""
    fd = acquire_global_lock()
    if fd is None:
        logger.warning("다른 auto_orch 프로세스가 실행 중입니다. 스캔을 건너뜁니다.")
        return

    errors_count = 0
    active_count = 0

    try:
        # 0. .done 파일을 memory/events/ → incoming/으로 복사
        scan_done_events(EVENTS_DIR, INCOMING_DIR, PROCESSED_DIR)

        # 1. 신규 이벤트 수집 및 소비
        new_events = scan_events(INCOMING_DIR, PROCESSED_DIR)
        for event_file in new_events:
            consumed = consume_event(INCOMING_DIR, PROCESSED_DIR, event_file)
            if consumed:
                logger.info("이벤트 소비 완료: %s", event_file)
            else:
                logger.debug("이벤트 소비 실패 또는 이미 처리됨: %s", event_file)

        # 2. state/*.json 검사하여 진행 중 파이프라인 관리
        state_path = Path(STATE_DIR)
        if not state_path.exists():
            state_path.mkdir(parents=True, exist_ok=True)

        for state_file in state_path.glob("*.json"):
            pipeline_id = state_file.stem
            state = get_pipeline_state(pipeline_id)
            if state is None:
                continue

            status = state.get("status", "")
            if status not in ("running", "pending"):
                continue

            active_count += 1

            # 대기 중인 스텝 처리
            pipeline_yaml = os.path.join(PIPELINES_DIR, f"{pipeline_id}.yaml")
            if not os.path.exists(pipeline_yaml):
                logger.debug("파이프라인 YAML 없음: %s", pipeline_yaml)
                continue

            try:
                pipeline = load_pipeline(pipeline_yaml)
            except Exception as exc:
                logger.error("파이프라인 YAML 로드 실패 (%s): %s", pipeline_yaml, exc)
                errors_count += 1
                continue

            # enabled: false이면 스킵
            if not is_pipeline_enabled(pipeline):
                logger.debug("파이프라인 비활성화 상태 — 스킵: %s", pipeline_id)
                continue

            steps: list[dict] = pipeline.get("steps", [])
            current_step_id = state.get("current_step")

            for step in steps:
                if step.get("id") != current_step_id:
                    continue
                target_team = step.get("target_team", "")
                if TeamLock.is_team_available(target_team):
                    success = dispatch_step(step, pipeline_id, state)
                    if not success:
                        errors_count += 1
                else:
                    logger.debug("팀 락 보유 중 — 디스패치 대기: team=%s", target_team)

        # 3. 토큰 사용량 조회
        ledger = TokenLedger(LEDGER_PATH)
        usage_summary = ledger.get_daily_usage_summary()

        # 4. health.json 갱신 (토큰 사용량 포함)
        update_health(active_pipelines=active_count, errors=errors_count, token_usage=usage_summary)

        # 5. 토큰 경고 임계값 확인
        if ledger.check_warning_threshold():
            today = usage_summary["today"]
            limit = usage_summary["limit"]
            remaining = usage_summary["remaining"]
            msg = (
                f"⚠️ 일일 토큰 사용량 80% 도달\n"
                f"사용: {today:,} / {limit:,}\n"
                f"잔여: {remaining:,} 토큰\n"
                f"활성 파이프라인: {active_count}개"
            )
            send_telegram_alert(msg, "token_warning")

        # 6. 스텝 타임아웃 감지
        stale_tasks = check_stale_tasks()
        for stale in stale_tasks:
            task_id = stale["task_id"]
            team_id = stale["team_id"]
            duration_seconds = stale["duration_seconds"]
            msg = (
                f"⚠️ 스텝 타임아웃 감지\n"
                f"태스크: {task_id}\n"
                f"팀: {team_id}\n"
                f"경과: {duration_seconds / 3600:.1f}시간"
            )
            send_telegram_alert(msg, f"stale_task_{task_id}")

    except Exception as exc:
        logger.error("cmd_scan 실행 중 예외 발생: %s", exc)
        errors_count += 1
        update_health(active_pipelines=active_count, errors=errors_count)
    finally:
        release_global_lock(fd)


def cmd_run(pipeline_id: str, dry_run: bool = False) -> None:
    """--run: 파이프라인 YAML 로드 → 검증 → 토큰 확인 → 첫 스텝 디스패치.

    Args:
        pipeline_id: 실행할 파이프라인 식별자.
        dry_run: True이면 실제 디스패치 없이 시뮬레이션만 수행.
    """
    pipeline_yaml = os.path.join(PIPELINES_DIR, f"{pipeline_id}.yaml")
    logger.info("파이프라인 실행 시작: %s", pipeline_id)

    # 1. YAML 로드
    try:
        pipeline = load_pipeline(pipeline_yaml)
    except FileNotFoundError:
        print(f"ERROR: 파이프라인 파일을 찾을 수 없습니다: {pipeline_yaml}")
        return
    except Exception as exc:
        print(f"ERROR: 파이프라인 로드 실패: {exc}")
        return

    # 2. 검증
    validation_errors = validate_pipeline(pipeline)
    if validation_errors:
        print(f"ERROR: 파이프라인 검증 실패:")
        for err in validation_errors:
            print(f"  - {err}")
        return

    # 3. 토큰 예산 확인
    token_budget = pipeline.get("token_budget", 0)
    ledger_path = os.path.join(WORKSPACE_ROOT, "orchestrator/state/token_ledger.json")
    ledger = TokenLedger(ledger_path)
    if not ledger.can_spend(pipeline_id, token_budget):
        print(f"ERROR: 토큰 예산 초과 또는 동시 실행 한도 초과. pipeline_id={pipeline_id}")
        return

    steps: list[dict] = pipeline.get("steps", [])

    # dry-run인 경우
    if dry_run:
        print(f"=== DRY-RUN: 파이프라인 '{pipeline_id}' ===")
        print(f"검증: 통과")
        print(f"토큰 예산: {token_budget:,}")
        print(f"스텝 수: {len(steps)}개")
        print(f"")
        for i, step in enumerate(steps, 1):
            sid = step.get("id", "<unknown>")
            name = step.get("name", sid)
            team = step.get("target_team", "unknown")
            deps = step.get("depends_on", [])
            task_file = step.get("task_file_template", "N/A")
            print(f"  [{i}] {sid}")
            print(f"      name: {name}")
            print(f"      team: {team}")
            print(f"      depends_on: {deps}")
            print(f"      task_file: {task_file}")
        print(f"")
        print(f"DRY-RUN 완료: 실제 디스패치는 수행되지 않았습니다.")
        return

    # 4. 초기 상태 생성
    first_step_id = steps[0].get("id") if steps else None

    initial_state: dict = {
        "pipeline_id": pipeline_id,
        "status": "running",
        "current_step": first_step_id,
        "started_at": datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
        "steps_done": [],
    }
    save_pipeline_state(pipeline_id, initial_state)
    logger.info("초기 상태 저장 완료: pipeline_id=%s first_step=%s", pipeline_id, first_step_id)

    # 5. 첫 스텝 디스패치
    if steps:
        first_step = steps[0]
        success = dispatch_step(first_step, pipeline_id, initial_state)
        if success:
            print(f"파이프라인 '{pipeline_id}' 시작 완료. 첫 스텝: {first_step_id}")
        else:
            print(f"WARNING: 첫 스텝 디스패치 실패: {first_step_id}")
    else:
        print(f"WARNING: 파이프라인 '{pipeline_id}'에 스텝이 없습니다.")


def cmd_status() -> None:
    """--status: state/*.json 전체를 읽고 파이프라인 상태 요약을 출력한다."""
    state_path = Path(STATE_DIR)
    if not state_path.exists():
        print("실행 중인 파이프라인이 없습니다.")
        return

    state_files = list(state_path.glob("*.json"))
    if not state_files:
        print("실행 중인 파이프라인이 없습니다.")
        return

    print(f"=== 파이프라인 상태 ({len(state_files)}개) ===")
    for state_file in sorted(state_files):
        pipeline_id = state_file.stem
        state = get_pipeline_state(pipeline_id)
        if state is None:
            print(f"  [{pipeline_id}] (상태 파일 손상)")
            continue
        status = state.get("status", "unknown")
        current_step = state.get("current_step", "-")
        started_at = state.get("started_at", "-")
        print(f"  [{pipeline_id}] status={status} current_step={current_step} started_at={started_at}")

    # 토큰 사용량 섹션
    ledger = TokenLedger(LEDGER_PATH)
    summary = ledger.get_daily_usage_summary()
    print("")
    print("=== 토큰 사용량 ===")
    print(f"오늘: {summary['today']:,} / {summary['limit']:,} ({summary['percentage']}%)")
    warning_limit = int(ledger.DAILY_HARD_LIMIT * 0.8)
    print(f"경고 임계값: {warning_limit:,} (80%)")


def cmd_list() -> None:
    """--list: PIPELINES_DIR/*.yaml을 스캔하여 파이프라인 목록을 출력한다."""
    pipelines_path = Path(PIPELINES_DIR)
    if not pipelines_path.exists():
        print("pipelines 디렉터리가 없습니다.")
        return

    yaml_files = list(pipelines_path.glob("*.yaml"))
    if not yaml_files:
        print("등록된 파이프라인이 없습니다.")
        return

    print(f"=== 파이프라인 목록 ({len(yaml_files)}개) ===")
    for yaml_file in sorted(yaml_files):
        pipeline_id = yaml_file.stem
        try:
            pipeline = load_pipeline(str(yaml_file))
            name = pipeline.get("name", pipeline_id)
            schema_version = pipeline.get("schema_version", "?")
            print(f"  {pipeline_id} | name={name} | schema_version={schema_version}")
        except Exception as exc:
            print(f"  {pipeline_id} | (로드 실패: {exc})")


def cmd_validate(yaml_path: str) -> None:
    """--validate: YAML 파일을 로드하고 파이프라인 스키마를 검증한다.

    Args:
        yaml_path: 검증할 YAML 파일 경로.
    """
    # 1. 로드
    try:
        pipeline = load_pipeline(yaml_path)
    except Exception as exc:
        print(f"ERROR: YAML 로드 실패: {exc}")
        return

    # 2. 검증
    errors = validate_pipeline(pipeline)
    if errors:
        print(f"Invalid: {len(errors)}개 오류 발견")
        for err in errors:
            print(f"  ERROR: {err}")
    else:
        print("Valid: 파이프라인 검증 통과")


# ---------------------------------------------------------------------------
# CLI 진입점
# ---------------------------------------------------------------------------


def main() -> None:
    """argparse 기반 CLI 진입점."""
    parser = argparse.ArgumentParser(
        prog="auto_orch.py",
        description="Auto Orchestrator — Phase 3 오케스트레이터 코어",
    )

    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument(
        "--scan",
        action="store_true",
        help="이벤트 스캔 및 대기 스텝 디스패치 (30초 주기 타이머에서 호출)",
    )
    group.add_argument(
        "--run",
        metavar="PIPELINE_ID",
        help="지정한 파이프라인을 수동 실행",
    )
    group.add_argument(
        "--status",
        action="store_true",
        help="현재 실행 중인 파이프라인 상태 출력",
    )
    group.add_argument(
        "--list",
        action="store_true",
        help="등록된 파이프라인 목록 출력",
    )
    group.add_argument(
        "--validate",
        metavar="YAML_PATH",
        help="파이프라인 YAML 파일 검증",
    )

    parser.add_argument(
        "--dry-run",
        action="store_true",
        default=False,
        help="파이프라인을 검증하고 시뮬레이션만 수행 (실제 디스패치 없음)",
    )

    args = parser.parse_args()

    if args.scan:
        cmd_scan()
    elif args.run:
        cmd_run(args.run, dry_run=args.dry_run)
    elif args.status:
        cmd_status()
    elif args.list:
        cmd_list()
    elif args.validate:
        cmd_validate(args.validate)


if __name__ == "__main__":
    main()
