#!/usr/bin/env python3
"""
작업 시간 추적 시스템

Usage:
    python3 memory/task-timer.py start <task_id> [team_id] [description]
    python3 memory/task-timer.py end <task_id>
    python3 memory/task-timer.py status <task_id>
    python3 memory/task-timer.py list [status]
    python3 memory/task-timer.py progress [--project <project_id>]
    python3 memory/task-timer.py log <message>
    python3 memory/task-timer.py cleanup [--running-hours 2] [--reserved-minutes 30] [--dry-run]
    python3 memory/task-timer.py cross-start <agent_name> --task <task_id> [--desc <description>] [--team <team_id>]
    python3 memory/task-timer.py cross-end <agent_name>
"""

import fcntl
import json
import os
import re
import sys
import tempfile
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional

try:
    from utils.logger import get_logger
except ImportError:
    sys.path.insert(0, str(Path(__file__).parent.parent))  # /home/jay/workspace
    from utils.logger import get_logger

logger = get_logger(__name__)

TASK_ID_PATTERN = re.compile(r"^task-\d+(\.\d+)?(_\d+\.\d+)?(_[a-z])?(\+\d+)?$")
# 동기화 주의: dispatch.py DYNAMIC_BOT_TEAMS, utils/composite_constants.py COMPOSITE_ALLOWED_TEAMS와 일치해야 함
ALLOWED_TEAM_IDS = {
    "dev1-team",
    "dev2-team",
    "dev3-team",
    "dev4-team",
    "dev5-team",
    "dev6-team",
    "dev7-team",
    "dev8-team",
    "marketing",
    "consulting",
    "anu-direct",
    "design",
    "publishing",
    "composite",
    "",
}
ALLOWED_CROSS_AGENTS = {"loki", "venus", "maat", "janus"}


def validate_task_id(task_id: str) -> bool:
    """task_id 형식 검증: 포맷 v2 지원 (task-N[.N][_P.P][_x][+R])"""
    return bool(TASK_ID_PATTERN.match(task_id))


def validate_team_id(team_id: str) -> bool:
    """team_id 형식 검증: 허용 목록에 있는지 확인"""
    return team_id in ALLOWED_TEAM_IDS


# 로그 타입 → 섹션 매핑
ENTRY_TYPE_SECTION = {
    "note": None,  # 기존 방식: 완료된 작업 섹션에 추가
    "decision": "의사결정",  # 의사결정 로그
    "system": "시스템 변경",  # 시스템/설정 변경
    "arch": "아키텍처 논의",  # 아키텍처 논의
    "dispatch": "위임 기록",  # 팀 위임 자동 기록
}


class TaskTimer:
    """작업 시간 추적 관리자"""

    def __init__(self, workspace_path: Optional[str] = None) -> None:
        if workspace_path is None:
            workspace_path = os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")
        self.workspace_path = Path(workspace_path)
        self.timer_file = self.workspace_path / "memory" / "task-timers.json"
        self.daily_log_dir = self.workspace_path / "memory" / "daily"
        self.pipeline_status_file = self.workspace_path / "memory" / "pipeline-status.json"
        self.cross_functional_file = self.workspace_path / "memory" / "cross-functional-status.json"
        self.timers = self._load_timers()

    def _load_timers(self) -> Dict:
        """타이머 데이터 로드"""
        if self.timer_file.exists():
            try:
                lock_path = self.timer_file.parent / ".task-timers.lock"
                lock_path.parent.mkdir(parents=True, exist_ok=True)
                with open(lock_path, "w") as lock_fd:
                    fcntl.flock(lock_fd, fcntl.LOCK_SH)
                    try:
                        with open(self.timer_file, "r", encoding="utf-8") as f:
                            return json.load(f)
                    finally:
                        fcntl.flock(lock_fd, fcntl.LOCK_UN)
            except Exception as e:
                logger.error(f"타이머 파일 로드 실패: {self.timer_file} - {e}")
                return {"tasks": {}}
        else:
            return {"tasks": {}}

    def _save_timers(self) -> None:
        """타이머 데이터 저장 (원자적 쓰기)"""
        import tempfile as _tempfile

        try:
            self.timer_file.parent.mkdir(parents=True, exist_ok=True)
            lock_path = self.timer_file.parent / ".task-timers.lock"
            with open(lock_path, "w") as lock_fd:
                fcntl.flock(lock_fd, fcntl.LOCK_EX)
                try:
                    with _tempfile.NamedTemporaryFile(
                        "w",
                        dir=self.timer_file.parent,
                        delete=False,
                        suffix=".tmp",
                        encoding="utf-8",
                    ) as tmp:
                        json.dump(self.timers, tmp, ensure_ascii=False, indent=2)
                        tmp.flush()
                        os.fsync(tmp.fileno())
                    os.replace(tmp.name, str(self.timer_file))
                finally:
                    fcntl.flock(lock_fd, fcntl.LOCK_UN)
        except Exception as e:
            logger.error(f"타이머 파일 저장 실패: {self.timer_file} - {e}")

    def start_task(
        self, task_id: str, team_id: str = "", description: str = "", project_id: str = "system", work_level: str = ""
    ) -> Dict:
        """작업 시작"""

        if not validate_task_id(task_id):
            logger.warning(f"잘못된 task_id 형식: {task_id} (expected: task-N[.N][_P.P][_x][+R])")
            return {
                "status": "error",
                "reason": f"Invalid task_id format: '{task_id}'. Expected: task-N[.N][_P.P][_x][+R]",
            }

        if team_id and not validate_team_id(team_id):
            logger.warning(f"잘못된 team_id: {team_id} (allowed: {', '.join(ALLOWED_TEAM_IDS - {''})})")
            return {
                "status": "error",
                "reason": f"Invalid team_id: '{team_id}'. Allowed: dev1-team~dev8-team, marketing, consulting, design, publishing, composite, anu-direct",
            }

        # 이중 등록 방어: 이미 running 상태인 동일 task_id가 있으면 중복 시작 거부
        existing = self.timers["tasks"].get(task_id)
        if existing and existing.get("status") == "running":
            logger.warning(f"이중 등록 시도 거부: {task_id}는 이미 running 상태")
            return {"status": "already_running", "task_id": task_id, "start_time": existing["start_time"]}

        # 완료된 task 덮어쓰기 방지 (task-464.1)
        if existing and existing.get("status") == "completed":
            logger.warning(f"완료된 task 덮어쓰기 시도 거부: {task_id}")
            return {"status": "error", "reason": f"task_id '{task_id}' is already completed. Use a new ID."}

        # stale task 재시작 방지 (task-486.1)
        if existing and existing.get("status") == "stale":
            logger.warning(f"stale task 재시작 시도 거부: {task_id}")
            return {"status": "error", "reason": f"task_id '{task_id}' is in stale state. Use a new ID."}

        start_time = datetime.now()

        self.timers["tasks"][task_id] = {
            "task_id": task_id,
            "team_id": team_id,
            "description": description,
            "project_id": project_id,
            "work_level": work_level,
            "start_time": start_time.isoformat(),
            "end_time": None,
            "duration_seconds": None,
            "status": "running",
        }

        logger.info(f"태스크 시작: {task_id} (team={team_id})")
        self._save_timers()
        self._update_pipeline_status("start", self.timers["tasks"][task_id])

        # 일일 로그에 시작 기록
        if team_id or description:
            level_tag = f"[{work_level.upper()}] " if work_level else ""
            log_entry = f"- [{start_time.strftime('%H:%M:%S')}] {level_tag}{team_id}: {description} - started"
            self._append_to_daily_log(log_entry)

        return {"status": "started", "task_id": task_id, "start_time": start_time.isoformat()}

    def _fuzzy_find_running(self, task_id: str) -> Optional[str]:
        """task_id prefix로 running 상태인 항목을 찾아 실제 task_id 반환.

        - 정확 매칭이 존재하면 None 반환 (호출자가 직접 처리)
        - task_id + "." 로 시작하는 running 항목 검색
        - running 항목이 1개면 해당 task_id 반환
        - running 항목이 여러 개면 start_time이 가장 최신인 것 반환
        - 없으면 None 반환
        """
        prefix = task_id + "."
        candidates = [
            tid
            for tid, data in self.timers["tasks"].items()
            if tid.startswith(prefix) and data.get("status") == "running"
        ]
        if not candidates:
            return None
        if len(candidates) == 1:
            return candidates[0]
        # 여러 개면 start_time이 가장 최신인 것 선택
        return max(
            candidates,
            key=lambda tid: self.timers["tasks"][tid].get("start_time", ""),
        )

    def end_task(self, task_id: str, qc_result: str = "") -> Dict:
        """작업 종료 (멱등성 보장: 이미 completed면 조용히 기존 결과 반환)

        정확 매칭이 없으면 task_id + "." prefix로 running 항목을 fuzzy match하여 종료.

        Args:
            task_id: 종료할 작업 ID
            qc_result: QC 결과 ("PASS", "FAIL", "WARN" 또는 빈 문자열). 빈 문자열이면 None으로 저장.
        """

        # 정확 매칭이 없으면 fuzzy match 시도
        if task_id not in self.timers["tasks"]:
            matched = self._fuzzy_find_running(task_id)
            if matched is None:
                logger.error(f"태스크를 찾을 수 없음: {task_id}")
                return {"status": "error", "reason": f"Task '{task_id}' not found"}
            task_id = matched

        task_data = self.timers["tasks"][task_id]

        # 멱등성: 이미 완료된 태스크는 기존 결과를 반환
        if task_data.get("status") == "completed":
            logger.info(f"태스크 이미 완료됨 (멱등): {task_id}")
            return {
                "status": "completed",
                "task_id": task_id,
                "start_time": task_data["start_time"],
                "end_time": task_data.get("end_time"),
                "duration_seconds": task_data.get("duration_seconds"),
                "duration_human": task_data.get("duration_human"),
                "already_completed": True,
            }

        end_time = datetime.now()

        # 시작 시간 파싱
        start_time = datetime.fromisoformat(task_data["start_time"])

        # 소요 시간 계산
        duration = (end_time - start_time).total_seconds()

        # 업데이트
        task_data["end_time"] = end_time.isoformat()
        task_data["duration_seconds"] = duration
        task_data["duration_human"] = self._format_duration(duration)
        task_data["status"] = "completed"
        task_data["qc_result"] = qc_result if qc_result else None

        logger.info(f"태스크 완료: {task_id} ({task_data['duration_human']})")
        self._save_timers()
        self._update_pipeline_status("end", task_data)
        # 프로젝트 진행률 자동 갱신
        self._update_project_progress(task_data.get("project_id", ""))

        # bot-activity.json 갱신
        self._update_bot_activity(task_data.get("team_id", ""))

        # 이벤트 파일 생성 (.done)
        self._write_event_file(task_id, task_data, end_time, duration)

        # 일일 로그에 완료 기록
        if task_data["team_id"] or task_data["description"]:
            log_entry = f"- [{end_time.strftime('%H:%M:%S')}] {task_data['team_id']}: {task_data['description']} - completed ({task_data['duration_human']})"
            self._append_to_daily_log(log_entry)

        return {
            "status": "completed",
            "task_id": task_id,
            "start_time": task_data["start_time"],
            "end_time": task_data["end_time"],
            "duration_seconds": duration,
            "duration_human": task_data["duration_human"],
        }

    def get_task_status(self, task_id: str) -> Optional[Dict]:
        """작업 상태 조회.

        정확 매칭이 없으면 task_id + "." prefix로 fuzzy match:
        - running 상태 우선
        - 없으면 start_time 기준 가장 최근 항목 반환
        """

        # 정확 매칭 우선
        if task_id in self.timers["tasks"]:
            return self.timers["tasks"][task_id]

        # fuzzy match: task_id + "." prefix로 시작하는 항목 검색
        prefix = task_id + "."
        candidates = [(tid, data) for tid, data in self.timers["tasks"].items() if tid.startswith(prefix)]
        if not candidates:
            return None

        # running 상태 우선
        running = [(tid, data) for tid, data in candidates if data.get("status") == "running"]
        if running:
            # start_time 기준 가장 최신
            return max(running, key=lambda x: x[1].get("start_time", ""))[1]

        # running이 없으면 start_time 기준 가장 최근 항목
        return max(candidates, key=lambda x: x[1].get("start_time", ""))[1]

    def list_tasks(self, status: Optional[str] = None) -> Dict:
        """작업 목록 조회"""

        tasks = []

        for task_id, task_data in self.timers["tasks"].items():
            if status is None or task_data["status"] == status:
                tasks.append(task_data)

        return {"total": len(tasks), "tasks": tasks}

    def calculate_progress(self, project_filter: Optional[str] = None) -> Dict:
        """프로젝트별 진행률 계산"""
        project_stats: Dict[str, Dict[str, int]] = {}
        for task_data in self.timers["tasks"].values():
            pid = task_data.get("project_id", "")
            if not pid or pid == "system":
                continue
            if project_filter and pid != project_filter:
                continue
            if pid not in project_stats:
                project_stats[pid] = {"total": 0, "completed": 0, "pct": 0}
            project_stats[pid]["total"] += 1
            if task_data.get("status") == "completed":
                project_stats[pid]["completed"] += 1
        for pid, stats in project_stats.items():
            stats["pct"] = round(stats["completed"] / stats["total"] * 100) if stats["total"] > 0 else 0
        return {"projects": project_stats}

    def _update_project_progress(self, project_id: str) -> None:
        """active-projects.json의 progress 필드 자동 갱신"""
        if not project_id or project_id == "system":
            return
        active_projects_file = self.workspace_path / "memory" / "active-projects.json"
        if not active_projects_file.exists():
            logger.warning(f"active-projects.json 없음: {active_projects_file}")
            return
        progress_data = self.calculate_progress(project_filter=project_id)
        proj_stats = progress_data["projects"].get(project_id)
        if not proj_stats:
            return
        try:
            with open(active_projects_file, "r", encoding="utf-8") as f:
                ap_data = json.load(f)
            updated = False
            for project in ap_data.get("active", []):
                if project.get("id") == project_id:
                    project["progress"] = {
                        "pct": proj_stats["pct"],
                        "completed": proj_stats["completed"],
                        "total": proj_stats["total"],
                        "updated_at": datetime.now().isoformat(),
                    }
                    updated = True
                    break
            if updated:
                ap_data["last_updated"] = datetime.now().isoformat()
                with tempfile.NamedTemporaryFile(
                    mode="w",
                    dir=active_projects_file.parent,
                    delete=False,
                    suffix=".tmp",
                    encoding="utf-8",
                ) as tmp:
                    json.dump(ap_data, tmp, ensure_ascii=False, indent=2)
                os.replace(tmp.name, str(active_projects_file))
                logger.info(f"active-projects.json 갱신: {project_id} → {proj_stats['pct']}%")
            else:
                logger.debug(f"active-projects.json에 프로젝트 없음: {project_id}")
        except Exception as e:
            logger.error(f"active-projects.json 갱신 실패: {e}")

    def _update_bot_activity(self, team_id: str) -> None:
        """해당 팀의 bot-activity.json status를 idle로 갱신.

        단, 해당 팀에 아직 running 상태인 task가 있으면 갱신하지 않음.
        """
        if not team_id:
            return

        # team_id에서 bot_key 추출 (예: "dev2-team" → "dev2")
        bot_key = team_id.replace("-team", "")

        # 해당 팀에 다른 running task가 있는지 확인
        for tid, tdata in self.timers["tasks"].items():
            if tdata.get("status") == "running" and tdata.get("team_id") == team_id:
                return  # 아직 running task 있으므로 갱신 안 함

        # bot-activity.json 읽기/갱신
        bot_activity_path = self.workspace_path / "memory" / "events" / "bot-activity.json"
        try:
            if bot_activity_path.exists():
                with open(bot_activity_path, "r", encoding="utf-8") as f:
                    data = json.load(f)
            else:
                data = {"bots": {}}

            if "bots" not in data:
                data["bots"] = {}

            from datetime import timezone as _tz

            now_utc = datetime.now(_tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
            data["bots"][bot_key] = {"status": "idle", "since": now_utc}

            # 원자적 쓰기
            import tempfile as _tempfile

            bot_activity_path.parent.mkdir(parents=True, exist_ok=True)
            with _tempfile.NamedTemporaryFile(
                "w",
                dir=bot_activity_path.parent,
                delete=False,
                suffix=".tmp",
                encoding="utf-8",
            ) as tmp:
                json.dump(data, tmp, ensure_ascii=False, indent=2)
            os.replace(tmp.name, str(bot_activity_path))
        except Exception as e:
            logger.error(f"bot-activity.json 갱신 실패 ({bot_key}): {e}")

    def _update_pipeline_status(self, action: str, task_data: dict) -> None:
        """pipeline-status.json 업데이트.

        action="start": active_tasks에 작업 추가 (stage=2, stage_name="AI 작업 수행")
        action="end"  : active_tasks에서 task_id 제거
        """
        try:
            self.pipeline_status_file.parent.mkdir(parents=True, exist_ok=True)

            # 기존 파일 읽기 또는 초기 구조 생성
            if self.pipeline_status_file.exists():
                with open(self.pipeline_status_file, "r", encoding="utf-8") as f:
                    fcntl.flock(f, fcntl.LOCK_SH)
                    try:
                        pipeline_data = json.load(f)
                    except json.JSONDecodeError:
                        pipeline_data = {}
                    finally:
                        fcntl.flock(f, fcntl.LOCK_UN)
            else:
                pipeline_data = {}

            # 기본 구조 보장
            if "active_tasks" not in pipeline_data:
                pipeline_data["active_tasks"] = []
            if "last_health_check" not in pipeline_data:
                pipeline_data["last_health_check"] = None

            task_id = task_data.get("task_id", "")
            now_iso = datetime.now().isoformat()

            if action == "start":
                # 이미 존재하는 항목은 제거 후 재등록 (중복 방지)
                pipeline_data["active_tasks"] = [
                    t for t in pipeline_data["active_tasks"] if t.get("task_id") != task_id
                ]
                pipeline_data["active_tasks"].append(
                    {
                        "task_id": task_id,
                        "team": task_data.get("team_id", ""),
                        "stage": 2,
                        "stage_name": "AI 작업 수행",
                        "started_at": task_data.get("start_time", now_iso),
                    }
                )

            elif action == "end":
                pipeline_data["active_tasks"] = [
                    t for t in pipeline_data["active_tasks"] if t.get("task_id") != task_id
                ]

            pipeline_data["last_updated"] = now_iso

            lock_path = self.pipeline_status_file.parent / ".pipeline-status.lock"
            with open(lock_path, "w") as lock_fd:
                fcntl.flock(lock_fd, fcntl.LOCK_EX)
                try:
                    with tempfile.NamedTemporaryFile(
                        "w",
                        dir=self.pipeline_status_file.parent,
                        delete=False,
                        suffix=".tmp",
                        encoding="utf-8",
                    ) as tmp:
                        json.dump(pipeline_data, tmp, ensure_ascii=False, indent=2)
                        tmp.flush()
                        os.fsync(tmp.fileno())
                    os.replace(tmp.name, str(self.pipeline_status_file))
                finally:
                    fcntl.flock(lock_fd, fcntl.LOCK_UN)

        except Exception:
            # pipeline-status 업데이트 실패해도 task-timer 기능은 계속 동작
            pass

    def _write_event_file(self, task_id: str, task_data: dict, end_time: datetime, duration: float) -> None:
        """완료 이벤트 파일 생성 (.done)"""
        events_dir = self.workspace_path / "memory" / "events"
        events_dir.mkdir(parents=True, exist_ok=True)

        event = {
            "task_id": task_id,
            "team_id": task_data.get("team_id", ""),
            "end_time": end_time.isoformat(),
            "duration_seconds": duration,
        }

        # qc_result 보존 로직: task_data > 기존 .done 파일 > .qc-result 파일 순으로 fallback
        qc_result = None

        # 1순위: task_data에 qc_result가 있으면 사용
        if task_data.get("qc_result") is not None:
            qc_result = task_data["qc_result"]

        # 2순위: 기존 .done 파일에 qc_result가 있으면 보존
        event_file = events_dir / f"{task_id}.done"
        if qc_result is None and event_file.exists():
            try:
                with open(event_file, "r", encoding="utf-8") as f:
                    existing = json.load(f)
                if existing.get("qc_result") is not None:
                    qc_result = existing["qc_result"]
            except Exception:
                pass

        # 3순위: .qc-result 파일에서 fallback
        if qc_result is None:
            qc_result_file = events_dir / f"{task_id}.qc-result"
            if qc_result_file.exists():
                try:
                    with open(qc_result_file, "r", encoding="utf-8") as f:
                        qc_data = json.load(f)
                    if qc_data.get("qc_result") is not None:
                        qc_result = qc_data["qc_result"]
                except Exception:
                    pass

        if qc_result is not None:
            event["qc_result"] = qc_result

        # 원자적 쓰기: 임시 파일에 쓰기 후 rename으로 교체 (race condition 방지)
        # Source: gstack eval-store.ts atomic write pattern (MIT License)
        with tempfile.NamedTemporaryFile(
            mode="w",
            dir=events_dir,
            suffix=".tmp",
            delete=False,
            encoding="utf-8",
        ) as tmp:
            json.dump(event, tmp, ensure_ascii=False, indent=2)
        os.replace(tmp.name, str(event_file))

    def _format_duration(self, seconds: float) -> str:
        """소요 시간 포맷팅"""

        if seconds < 60:
            return f"{int(seconds)}초"
        elif seconds < 3600:
            minutes = int(seconds / 60)
            secs = int(seconds % 60)
            return f"{minutes}분 {secs}초"
        else:
            hours = int(seconds / 3600)
            minutes = int((seconds % 3600) / 60)
            return f"{hours}시간 {minutes}분"

    def _get_daily_log_path(self, date: Optional[datetime] = None) -> Path:
        """해당 날짜의 일일 로그 파일 경로 반환"""

        if date is None:
            date = datetime.now()

        date_str = date.strftime("%Y-%m-%d")
        return self.daily_log_dir / f"{date_str}.md"

    def _append_to_daily_log(self, log_entry: str) -> None:
        """일일 로그에 항목 추가 (완료된 작업 섹션)"""

        log_path = self._get_daily_log_path()
        self.daily_log_dir.mkdir(parents=True, exist_ok=True)

        current_date = datetime.now()
        date_str = current_date.strftime("%Y-%m-%d")
        header = f"# {date_str} 업무일지\n\n## 완료된 작업\n"

        if log_path.exists():
            with open(log_path, "r", encoding="utf-8") as f:
                content = f.read()
        else:
            content = header

        # 헤더가 없으면 추가
        if not content.startswith(f"# {date_str}"):
            content = header + content

        # 로그 항목 추가
        with open(log_path, "a", encoding="utf-8") as f:
            f.write(log_entry + "\n")

    def _append_to_section(self, log_entry: str, section: str) -> None:
        """일일 로그의 특정 섹션에 항목 추가. 섹션이 없으면 파일 끝에 생성."""

        log_path = self._get_daily_log_path()
        self.daily_log_dir.mkdir(parents=True, exist_ok=True)

        current_date = datetime.now()
        date_str = current_date.strftime("%Y-%m-%d")
        base_header = f"# {date_str} 업무일지\n\n## 완료된 작업\n"

        if log_path.exists():
            content = log_path.read_text(encoding="utf-8")
        else:
            content = base_header

        if not content.startswith(f"# {date_str}"):
            content = base_header + content

        section_header = f"## {section}"

        if section_header in content:
            # 섹션 찾아서 다음 섹션 직전에 삽입
            lines = content.split("\n")
            section_idx = None
            next_section_idx = None
            for i, line in enumerate(lines):
                if line.strip() == section_header:
                    section_idx = i
                elif section_idx is not None and line.startswith("## ") and i > section_idx:
                    next_section_idx = i
                    break

            if section_idx is not None:
                if next_section_idx is not None:
                    lines.insert(next_section_idx, log_entry)
                else:
                    lines.append(log_entry)
                log_path.write_text("\n".join(lines), encoding="utf-8")
                return

        # 섹션이 없으면 파일 끝에 새 섹션 추가
        if not content.endswith("\n"):
            content += "\n"
        content += f"\n{section_header}\n{log_entry}\n"
        log_path.write_text(content, encoding="utf-8")

    def _load_cross_status(self) -> Dict:
        """횡단조직 상태 데이터 로드"""
        default: Dict = {
            "cross_functional": {
                "loki": {"status": "idle"},
                "venus": {"status": "idle"},
                "maat": {"status": "idle"},
                "janus": {"status": "idle"},
            }
        }
        if self.cross_functional_file.exists():
            try:
                lock_path = self.cross_functional_file.parent / ".cross-functional-status.lock"
                lock_path.parent.mkdir(parents=True, exist_ok=True)
                with open(lock_path, "w") as lock_fd:
                    fcntl.flock(lock_fd, fcntl.LOCK_SH)
                    try:
                        with open(self.cross_functional_file, "r", encoding="utf-8") as f:
                            return json.load(f)
                    finally:
                        fcntl.flock(lock_fd, fcntl.LOCK_UN)
            except Exception as e:
                logger.error(f"횡단조직 파일 로드 실패: {self.cross_functional_file} - {e}")
                self._save_cross_status(default)
                return default
        self._save_cross_status(default)
        return default

    def _save_cross_status(self, data: Dict) -> None:
        """횡단조직 상태 데이터 저장 (원자적 쓰기)"""
        import tempfile as _tempfile

        try:
            self.cross_functional_file.parent.mkdir(parents=True, exist_ok=True)
            lock_path = self.cross_functional_file.parent / ".cross-functional-status.lock"
            with open(lock_path, "w") as lock_fd:
                fcntl.flock(lock_fd, fcntl.LOCK_EX)
                try:
                    with _tempfile.NamedTemporaryFile(
                        "w",
                        dir=self.cross_functional_file.parent,
                        delete=False,
                        suffix=".tmp",
                        encoding="utf-8",
                    ) as tmp:
                        json.dump(data, tmp, ensure_ascii=False, indent=2)
                    os.replace(tmp.name, str(self.cross_functional_file))
                finally:
                    fcntl.flock(lock_fd, fcntl.LOCK_UN)
        except Exception as e:
            logger.error(f"횡단조직 파일 저장 실패: {self.cross_functional_file} - {e}")

    def cross_start(self, agent_name: str, task_id: str, description: str = "", parent_team: str = "") -> Dict:
        """횡단조직 에이전트 소환 시작"""
        if agent_name not in ALLOWED_CROSS_AGENTS:
            allowed = ", ".join(sorted(ALLOWED_CROSS_AGENTS))
            logger.warning(f"잘못된 cross agent: {agent_name} (allowed: {allowed})")
            return {
                "status": "error",
                "reason": f"Unknown cross-functional agent: '{agent_name}'. Allowed: {allowed}",
            }

        if not validate_task_id(task_id):
            logger.warning(f"잘못된 task_id 형식 (cross-start): {task_id}")
            return {
                "status": "error",
                "reason": f"Invalid task_id format: '{task_id}'. Expected: task-N[.N][_P.P][_x][+R]",
            }

        cross_data = self._load_cross_status()
        started_at = datetime.now().isoformat()

        cross_data["cross_functional"][agent_name] = {
            "status": "active",
            "task_id": task_id,
            "description": description,
            "started_at": started_at,
            "parent_team": parent_team,
        }

        self._save_cross_status(cross_data)
        logger.info(f"횡단조직 소환 시작: {agent_name} → {task_id}")
        return {"status": "started", "agent": agent_name, "task_id": task_id}

    def cross_end(self, agent_name: str) -> Dict:
        """횡단조직 에이전트 소환 종료"""
        if agent_name not in ALLOWED_CROSS_AGENTS:
            allowed = ", ".join(sorted(ALLOWED_CROSS_AGENTS))
            logger.warning(f"잘못된 cross agent (end): {agent_name}")
            return {
                "status": "error",
                "reason": f"Unknown cross-functional agent: '{agent_name}'. Allowed: {allowed}",
            }

        cross_data = self._load_cross_status()
        agent_state = cross_data["cross_functional"].get(agent_name, {})

        # 멱등성: 이미 idle이면 조용히 성공 반환
        if agent_state.get("status", "idle") == "idle":
            logger.info(f"횡단조직 이미 idle (멱등): {agent_name}")
            return {"status": "ended", "agent": agent_name}

        cross_data["cross_functional"][agent_name] = {"status": "idle"}
        self._save_cross_status(cross_data)
        logger.info(f"횡단조직 소환 종료: {agent_name}")
        return {"status": "ended", "agent": agent_name}

    def cleanup_stale(self, running_hours: float = 2.0, reserved_minutes: float = 30.0) -> list:
        """stale 상태로 전환할 작업을 정리

        - running 상태이고 start_time이 running_hours 초과: stale_reason="timeout_running"
        - reserved 상태이고 reserved_at이 reserved_minutes 초과: stale_reason="timeout_reserved"

        반환값: 정리된 task 목록 (task_id, prev_status, stale_reason)
        """
        now = datetime.now()
        running_threshold = running_hours * 3600  # 초 단위
        reserved_threshold = reserved_minutes * 60  # 초 단위
        cleaned = []

        for task_id, task_data in self.timers["tasks"].items():
            status = task_data.get("status")
            prev_status = status
            stale_reason = None

            if status == "running":
                start_time_str = task_data.get("start_time")
                if start_time_str:
                    try:
                        start_time = datetime.fromisoformat(start_time_str)
                        elapsed = (now - start_time).total_seconds()
                        if elapsed > running_threshold:
                            stale_reason = "timeout_running"
                    except ValueError as e:
                        logger.warning(f"start_time 파싱 실패 ({task_id}): {e}")

            elif status == "reserved":
                reserved_at_str = task_data.get("reserved_at")
                if reserved_at_str:
                    try:
                        reserved_at = datetime.fromisoformat(reserved_at_str)
                        elapsed = (now - reserved_at).total_seconds()
                        if elapsed > reserved_threshold:
                            stale_reason = "timeout_reserved"
                    except ValueError as e:
                        logger.warning(f"reserved_at 파싱 실패 ({task_id}): {e}")

            if stale_reason:
                task_data["status"] = "stale"
                task_data["stale_reason"] = stale_reason
                task_data["stale_at"] = now.isoformat()
                cleaned.append(
                    {
                        "task_id": task_id,
                        "prev_status": prev_status,
                        "stale_reason": stale_reason,
                    }
                )
                logger.info(f"stale 전환: {task_id} ({prev_status} → stale, reason={stale_reason})")

        if cleaned:
            self._save_timers()

        return cleaned

    def add_log_entry(self, message: str, entry_type: str = "note") -> Dict:
        """일일 로그에 직접 메모 추가.

        entry_type:
          note     - 완료된 작업 섹션 (기본값)
          decision - 의사결정 섹션
          system   - 시스템 변경 섹션
          arch     - 아키텍처 논의 섹션
          dispatch - 위임 기록 섹션
        """

        now = datetime.now()
        type_label = entry_type if entry_type else "note"
        log_entry = f"- [{now.strftime('%H:%M:%S')}] {type_label}: {message}"
        section = ENTRY_TYPE_SECTION.get(entry_type)

        logger.info(f"로그 추가: [{entry_type}] {message[:50]}")

        if section:
            self._append_to_section(log_entry, section)
        else:
            self._append_to_daily_log(log_entry)

        return {"status": "logged", "type": entry_type, "message": message, "timestamp": now.isoformat()}


def main() -> None:
    """CLI 인터페이스"""

    if len(sys.argv) < 2:
        print("Usage:")
        print("  python3 memory/task-timer.py start <task_id> [team_id] [description]")
        print("  python3 memory/task-timer.py end <task_id>")
        print("  python3 memory/task-timer.py status <task_id>")
        print("  python3 memory/task-timer.py list [status]")
        print("  python3 memory/task-timer.py log <message>")
        print("  python3 memory/task-timer.py cleanup [--running-hours 2] [--reserved-minutes 30] [--dry-run]")
        sys.exit(1)

    command = sys.argv[1]
    timer = TaskTimer()
    result: Any

    if command == "start":
        if len(sys.argv) < 3:
            print(
                "Error: task_id가 누락되었습니다. "
                "Usage: python3 memory/task-timer.py start <task_id> [--team <team_id>] [--desc <description>]. "
                "예: python3 memory/task-timer.py start task-100.1 --team dev1-team"
            )
            sys.exit(1)

        task_id = sys.argv[2]

        if not validate_task_id(task_id):
            print(
                f"Error: 잘못된 task_id 형식 '{task_id}'. "
                f"Expected 패턴: 'task-N[.N][_P.P][_x][+R]' (예: task-100, task-100.1, task-100_3.3). "
                f"올바른 형식으로 재실행하세요: python3 memory/task-timer.py start task-100.1"
            )
            sys.exit(1)

        team_id = ""
        description = ""
        project_id = "system"

        # --team, --desc, --project, --work-level 플래그 파싱
        work_level = ""
        args = sys.argv[3:]
        i = 0
        while i < len(args):
            if args[i] == "--team" and i + 1 < len(args):
                team_id = args[i + 1]
                i += 2
            elif args[i] == "--desc" and i + 1 < len(args):
                description = args[i + 1]
                i += 2
            elif args[i] == "--project" and i + 1 < len(args):
                project_id = args[i + 1]
                i += 2
            elif args[i] == "--work-level" and i + 1 < len(args):
                work_level = args[i + 1]
                i += 2
            else:
                # 플래그 없이 위치 인자로 전달된 경우
                if not team_id:
                    team_id = args[i]
                elif not description:
                    description = args[i]
                i += 1

        result = timer.start_task(task_id, team_id, description, project_id, work_level)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif command == "end":
        if len(sys.argv) < 3:
            print(
                "Error: task_id가 누락되었습니다. "
                "Usage: python3 memory/task-timer.py end <task_id> [--qc-result PASS|FAIL|WARN]. "
                "예: python3 memory/task-timer.py end task-100.1"
            )
            sys.exit(1)

        task_id = sys.argv[2]

        if not validate_task_id(task_id):
            print(
                f"Error: 잘못된 task_id 형식 '{task_id}'. "
                f"Expected 패턴: 'task-N[.N][_P.P][_x][+R]' (예: task-100, task-100.1, task-100_3.3). "
                f"올바른 형식으로 재실행하세요: python3 memory/task-timer.py end task-100.1"
            )
            sys.exit(1)

        # --qc-result 플래그 파싱
        qc_result = ""
        args = sys.argv[3:]
        i = 0
        while i < len(args):
            if args[i] == "--qc-result" and i + 1 < len(args):
                qc_result = args[i + 1]
                i += 2
            else:
                i += 1

        result = timer.end_task(task_id, qc_result=qc_result)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif command == "status":
        if len(sys.argv) < 3:
            print(
                "Error: task_id가 누락되었습니다. "
                "Usage: python3 memory/task-timer.py status <task_id>. "
                "예: python3 memory/task-timer.py status task-100.1"
            )
            sys.exit(1)

        task_id = sys.argv[2]

        if not validate_task_id(task_id):
            print(
                f"Error: 잘못된 task_id 형식 '{task_id}'. "
                f"Expected 패턴: 'task-N[.N][_P.P][_x][+R]' (예: task-100, task-100.1, task-100_3.3). "
                f"올바른 형식으로 재실행하세요: python3 memory/task-timer.py status task-100.1"
            )
            sys.exit(1)

        result = timer.get_task_status(task_id)

        if result:
            print(json.dumps(result, ensure_ascii=False, indent=2))
        else:
            print(
                f"Error: Task '{task_id}'를 찾을 수 없습니다. "
                f"'python3 memory/task-timer.py list'로 등록된 task 목록을 확인하거나, "
                f"'python3 memory/task-timer.py start {task_id}'로 task를 먼저 시작하세요."
            )
            sys.exit(1)

    elif command == "progress":
        project_filter = None
        args = sys.argv[2:]
        i = 0
        while i < len(args):
            if args[i] == "--project" and i + 1 < len(args):
                project_filter = args[i + 1]
                i += 2
            else:
                print(
                    f"Error: 알 수 없는 progress 옵션 '{args[i]}'. "
                    f"유효한 옵션: --project <project_id>. "
                    f"예: python3 memory/task-timer.py progress --project insuwiki"
                )
                sys.exit(1)

        result = timer.calculate_progress(project_filter=project_filter)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif command == "list":
        status = sys.argv[2] if len(sys.argv) > 2 else None
        result = timer.list_tasks(status)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif command == "log":
        if len(sys.argv) < 3:
            print(
                "Error: 로그 메시지가 누락되었습니다. "
                "Usage: python3 memory/task-timer.py log <message> [--type note|decision|system|arch|dispatch]. "
                "예: python3 memory/task-timer.py log '배포 완료' --type decision"
            )
            sys.exit(1)

        # --type 플래그 파싱 (나머지는 메시지)
        entry_type = "note"
        msg_parts = []
        args = sys.argv[2:]
        i = 0
        while i < len(args):
            if args[i] == "--type" and i + 1 < len(args):
                entry_type = args[i + 1]
                i += 2
            else:
                msg_parts.append(args[i])
                i += 1

        message = " ".join(msg_parts)
        if entry_type not in ENTRY_TYPE_SECTION:
            valid = ", ".join(ENTRY_TYPE_SECTION.keys())
            print(
                f"Error: 알 수 없는 로그 타입 '{entry_type}'. "
                f"유효한 타입: {valid}. "
                f"올바른 타입으로 재실행하세요: python3 memory/task-timer.py log '<message>' --type {list(ENTRY_TYPE_SECTION.keys())[0]}"
            )
            sys.exit(1)

        result = timer.add_log_entry(message, entry_type)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif command == "cleanup":
        # --running-hours, --reserved-minutes, --dry-run 플래그 파싱
        running_hours = 2.0
        reserved_minutes = 30.0
        dry_run = False
        args = sys.argv[2:]
        i = 0
        while i < len(args):
            if args[i] == "--running-hours" and i + 1 < len(args):
                try:
                    running_hours = float(args[i + 1])
                except ValueError:
                    print(
                        f"Error: --running-hours 값 '{args[i + 1]}'이 유효한 숫자가 아닙니다. "
                        f"숫자(float)를 입력하세요. "
                        f"예: python3 memory/task-timer.py cleanup --running-hours 2.0"
                    )
                    sys.exit(1)
                i += 2
            elif args[i] == "--reserved-minutes" and i + 1 < len(args):
                try:
                    reserved_minutes = float(args[i + 1])
                except ValueError:
                    print(
                        f"Error: --reserved-minutes 값 '{args[i + 1]}'이 유효한 숫자가 아닙니다. "
                        f"숫자(float)를 입력하세요. "
                        f"예: python3 memory/task-timer.py cleanup --reserved-minutes 30.0"
                    )
                    sys.exit(1)
                i += 2
            elif args[i] == "--dry-run":
                dry_run = True
                i += 1
            else:
                print(
                    f"Error: 알 수 없는 cleanup 옵션 '{args[i]}'. "
                    f"유효한 옵션: --running-hours <float>, --reserved-minutes <float>, --dry-run. "
                    f"예: python3 memory/task-timer.py cleanup --running-hours 2 --reserved-minutes 30 --dry-run"
                )
                sys.exit(1)

        if dry_run:
            # dry-run: 실제 변경 없이 대상만 탐색
            from datetime import datetime as _dt

            now = _dt.now()
            running_threshold = running_hours * 3600
            reserved_threshold = reserved_minutes * 60
            candidates = []
            for task_id, task_data in timer.timers["tasks"].items():
                status = task_data.get("status")
                if status == "running":
                    start_time_str = task_data.get("start_time")
                    if start_time_str:
                        try:
                            start_time = _dt.fromisoformat(start_time_str)
                            elapsed = (now - start_time).total_seconds()
                            if elapsed > running_threshold:
                                candidates.append(
                                    {
                                        "task_id": task_id,
                                        "prev_status": "running",
                                        "stale_reason": "timeout_running",
                                    }
                                )
                        except ValueError:
                            pass
                elif status == "reserved":
                    reserved_at_str = task_data.get("reserved_at")
                    if reserved_at_str:
                        try:
                            reserved_at = _dt.fromisoformat(reserved_at_str)
                            elapsed = (now - reserved_at).total_seconds()
                            if elapsed > reserved_threshold:
                                candidates.append(
                                    {
                                        "task_id": task_id,
                                        "prev_status": "reserved",
                                        "stale_reason": "timeout_reserved",
                                    }
                                )
                        except ValueError:
                            pass
            result = {"status": "dry_run", "would_clean_count": len(candidates), "tasks": candidates}
        else:
            cleaned = timer.cleanup_stale(running_hours=running_hours, reserved_minutes=reserved_minutes)
            result = {"status": "cleaned", "cleaned_count": len(cleaned), "tasks": cleaned}

        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif command == "cross-start":
        if len(sys.argv) < 3:
            allowed_agents = ", ".join(sorted(ALLOWED_CROSS_AGENTS))
            print(
                f"Error: agent_name이 누락되었습니다. "
                f"허용된 에이전트: {allowed_agents}. "
                f"Usage: python3 memory/task-timer.py cross-start <agent_name> --task <task_id> [--desc <description>] [--team <team_id>]. "
                f"예: python3 memory/task-timer.py cross-start loki --task task-100.1"
            )
            sys.exit(1)

        agent_name = sys.argv[2]
        task_id = ""
        description = ""
        parent_team = ""

        args = sys.argv[3:]
        i = 0
        while i < len(args):
            if args[i] == "--task" and i + 1 < len(args):
                task_id = args[i + 1]
                i += 2
            elif args[i] == "--desc" and i + 1 < len(args):
                description = args[i + 1]
                i += 2
            elif args[i] == "--team" and i + 1 < len(args):
                parent_team = args[i + 1]
                i += 2
            else:
                print(
                    f"Error: 알 수 없는 cross-start 옵션 '{args[i]}'. "
                    f"유효한 옵션: --task <task_id>, --desc <description>, --team <team_id>. "
                    f"예: python3 memory/task-timer.py cross-start loki --task task-100.1 --team dev1-team"
                )
                sys.exit(1)

        if not task_id:
            print(
                "Error: --task <task_id>가 누락되었습니다. "
                "cross-start 명령은 반드시 --task 옵션이 필요합니다. "
                "예: python3 memory/task-timer.py cross-start loki --task task-100.1"
            )
            sys.exit(1)

        result = timer.cross_start(agent_name, task_id, description, parent_team)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif command == "cross-end":
        if len(sys.argv) < 3:
            allowed_agents = ", ".join(sorted(ALLOWED_CROSS_AGENTS))
            print(
                f"Error: agent_name이 누락되었습니다. "
                f"허용된 에이전트: {allowed_agents}. "
                f"Usage: python3 memory/task-timer.py cross-end <agent_name>. "
                f"예: python3 memory/task-timer.py cross-end loki"
            )
            sys.exit(1)

        agent_name = sys.argv[2]
        result = timer.cross_end(agent_name)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    else:
        valid_commands = "start, end, status, list, progress, log, cleanup, cross-start, cross-end"
        print(
            f"Error: 알 수 없는 명령 '{command}'. "
            f"유효한 명령: {valid_commands}. "
            f"Usage: python3 memory/task-timer.py <command> [args]. "
            f"예: python3 memory/task-timer.py start task-100.1"
        )
        sys.exit(1)


if __name__ == "__main__":
    main()
