#!/usr/bin/env python3
"""weekly-retro.py — 주간 회고 시스템

task-timer.py 데이터와 git log를 분석해 팀별 생산성 메트릭을 계산하고
JSON 스냅샷을 저장한다.

Usage:
    python3 weekly-retro.py [--week YYYY-WW] [--workspace /path]
"""

import argparse
import json
import os
import subprocess
import sys
from collections import defaultdict
from datetime import date, timedelta
from pathlib import Path
from typing import Any, Optional

# ---------------------------------------------------------------------------
# Week utilities
# ---------------------------------------------------------------------------


def get_current_week() -> str:
    """현재 ISO week를 'YYYY-WW' 형식으로 반환한다."""
    today = date.today()
    iso = today.isocalendar()
    return f"{iso[0]}-{iso[1]:02d}"


def get_week_period(week: str) -> tuple[date, date]:
    """'YYYY-WW' 형식의 week 문자열로부터 해당 주의 (월요일, 일요일)을 반환한다."""
    year_str, wnum_str = week.split("-")
    year = int(year_str)
    wnum = int(wnum_str)
    # ISO week의 월요일 계산
    monday = date.fromisocalendar(year, wnum, 1)
    sunday = monday + timedelta(days=6)
    return monday, sunday


def get_prev_week(week: str) -> str:
    """'YYYY-WW'에서 이전 주의 'YYYY-WW'를 반환한다."""
    monday, _ = get_week_period(week)
    prev_monday = monday - timedelta(days=7)
    iso = prev_monday.isocalendar()
    return f"{iso[0]}-{iso[1]:02d}"


# ---------------------------------------------------------------------------
# Task data loading and filtering
# ---------------------------------------------------------------------------


def load_tasks(tasks_file: Path) -> dict[str, Any]:
    """task-timers.json을 읽어 tasks dict를 반환한다.

    파일이 없거나 파싱 실패 시 빈 dict를 반환한다.
    """
    if not tasks_file.exists():
        return {}
    try:
        data = json.loads(tasks_file.read_text())
        tasks: dict[str, Any] = data.get("tasks", {})
        if not isinstance(tasks, dict):
            return {}
        return tasks
    except (json.JSONDecodeError, OSError):
        return {}


def filter_tasks_by_week(tasks: dict[str, Any], week: str) -> dict[str, Any]:
    """주어진 week에 해당하며 status가 'completed'인 태스크만 필터링한다.

    start_time의 ISO date가 해당 주(월~일) 범위 내에 있는 태스크만 포함한다.
    """
    if not tasks:
        return {}

    monday, sunday = get_week_period(week)
    result: dict[str, Any] = {}

    for task_id, task in tasks.items():
        if task.get("status") != "completed":
            continue
        start_time_str = task.get("start_time", "")
        if not start_time_str:
            continue
        try:
            # ISO 형식에서 날짜 부분만 파싱
            task_date = date.fromisoformat(start_time_str[:10])
        except ValueError:
            continue
        if monday <= task_date <= sunday:
            result[task_id] = task

    return result


# ---------------------------------------------------------------------------
# Session classification
# ---------------------------------------------------------------------------


def classify_session(duration_seconds: float) -> str:
    """소요 시간에 따라 세션을 분류한다.

    - deep:   > 1800초 (30분 초과)
    - medium: 600초 이상 1800초 이하 (10분 이상 30분 이하)
    - micro:  < 600초 (10분 미만)
    """
    if duration_seconds > 1800:
        return "deep"
    elif duration_seconds >= 600:
        return "medium"
    else:
        return "micro"


# ---------------------------------------------------------------------------
# Team metrics
# ---------------------------------------------------------------------------


def compute_team_metrics(
    tasks: dict[str, Any],
) -> dict[str, dict[str, Any]]:
    """completed 태스크들로부터 팀별 메트릭을 계산한다.

    반환:
        {
            "team-id": {
                "task_count": int,
                "avg_duration_seconds": float,
                "fix_pct": float,          # 기본 0.0, 커밋 분석 후 갱신
                "session_pattern": {"deep": int, "medium": int, "micro": int}
            }
        }
    """
    if not tasks:
        return {}

    # 팀별 데이터 수집
    team_durations: dict[str, list[float]] = defaultdict(list)
    team_sessions: dict[str, dict[str, int]] = defaultdict(lambda: {"deep": 0, "medium": 0, "micro": 0})

    for task in tasks.values():
        team_id: str = task.get("team_id", "unknown")
        if not team_id:
            team_id = "unknown"
        duration: float = float(task.get("duration_seconds", 0.0))
        team_durations[team_id].append(duration)
        category = classify_session(duration)
        team_sessions[team_id][category] += 1

    metrics: dict[str, dict[str, Any]] = {}
    for team_id, durations in team_durations.items():
        count = len(durations)
        avg_dur = sum(durations) / count if count > 0 else 0.0
        metrics[team_id] = {
            "task_count": count,
            "avg_duration_seconds": avg_dur,
            "fix_pct": 0.0,
            "session_pattern": dict(team_sessions[team_id]),
        }

    return metrics


def compute_fix_pct(commits: dict[str, Any]) -> float:
    """전체 커밋 중 fix 타입 비율(%)을 계산한다."""
    total: int = commits.get("total", 0)
    if total == 0:
        return 0.0
    fix_count: int = commits.get("by_type", {}).get("fix", 0)
    return fix_count / total * 100.0


# ---------------------------------------------------------------------------
# Git log
# ---------------------------------------------------------------------------


def fetch_git_log(workspace: Path, since: str, until: str) -> str:
    """git log를 실행하고 stdout을 반환한다. 실패 시 빈 문자열을 반환한다."""
    try:
        result = subprocess.run(
            [
                "git",
                "-C",
                str(workspace),
                "log",
                f"--after={since}T00:00:00",
                f"--before={until}T23:59:59",
                "--format=%h %ad %s",
                "--date=short",
            ],
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            return ""
        return result.stdout
    except (FileNotFoundError, OSError):
        return ""


def parse_git_log(log_output: str) -> dict[str, Any]:
    """git log stdout을 파싱하여 커밋 타입별 집계를 반환한다.

    커밋 메시지 패턴: "type:" 또는 "type(scope):" 접두사 (대소문자 무시)
    접두사 없으면 "other"로 분류.
    """
    commit_types = ["feat", "fix", "refactor", "docs", "chore"]
    by_type: dict[str, int] = {t: 0 for t in commit_types}
    by_type["other"] = 0

    if not log_output.strip():
        return {"total": 0, "by_type": by_type}

    lines = [line.strip() for line in log_output.strip().splitlines() if line.strip()]
    total = len(lines)

    for line in lines:
        # 형식: "<hash> <date> <subject>"
        # 세 번째 공백 이후가 커밋 메시지
        parts = line.split(" ", 2)
        subject = parts[2] if len(parts) >= 3 else ""
        classified = False
        for ctype in commit_types:
            # "type:" 또는 "type(scope):" 패턴 (대소문자 무시)
            lower = subject.lower()
            if lower.startswith(f"{ctype}:") or lower.startswith(f"{ctype}("):
                by_type[ctype] += 1
                classified = True
                break
        if not classified:
            by_type["other"] += 1

    return {"total": total, "by_type": by_type}


# ---------------------------------------------------------------------------
# Anomaly detection
# ---------------------------------------------------------------------------


def detect_anomalies(
    teams: dict[str, dict[str, Any]],
    prev_teams: Optional[dict[str, dict[str, Any]]],
) -> list[str]:
    """이상치를 감지하고 메시지 목록을 반환한다.

    감지 조건:
    1. fix_pct > 30% 인 팀
    2. 이전 주 대비 작업 수 50% 초과 감소 (prev_teams가 있을 때)
    """
    anomalies: list[str] = []

    for team_id, data in teams.items():
        fix_pct: float = data.get("fix_pct", 0.0)
        if fix_pct > 30.0:
            anomalies.append(f"fix_pct 30% 초과: {team_id} ({fix_pct:.1f}%)")

    if prev_teams is not None:
        for team_id, data in teams.items():
            prev_data = prev_teams.get(team_id)
            if prev_data is None:
                continue
            curr_count: int = data.get("task_count", 0)
            prev_count: int = prev_data.get("task_count", 0)
            if prev_count > 0:
                drop_pct = (prev_count - curr_count) / prev_count
                if drop_pct > 0.50:
                    anomalies.append(
                        f"생산성 급감: {team_id} (이전 {prev_count}건 → 현재 {curr_count}건, "
                        f"{drop_pct * 100:.1f}% 감소)"
                    )

    return anomalies


# ---------------------------------------------------------------------------
# Trend calculation
# ---------------------------------------------------------------------------


def compute_trend(
    teams: dict[str, dict[str, Any]],
    prev_teams: Optional[dict[str, dict[str, Any]]],
) -> dict[str, Any]:
    """이전 주 대비 전체 메트릭 변화율을 계산한다.

    prev_teams가 None이면 빈 dict를 반환한다.
    """
    if prev_teams is None:
        return {}

    # 전체 합산으로 비교
    curr_total_count = sum(d.get("task_count", 0) for d in teams.values())
    prev_total_count = sum(d.get("task_count", 0) for d in prev_teams.values())

    curr_durations = [d["avg_duration_seconds"] for d in teams.values() if d.get("task_count", 0) > 0]
    prev_durations = [d["avg_duration_seconds"] for d in prev_teams.values() if d.get("task_count", 0) > 0]

    curr_avg_dur = sum(curr_durations) / len(curr_durations) if curr_durations else 0.0
    prev_avg_dur = sum(prev_durations) / len(prev_durations) if prev_durations else 0.0

    trend: dict[str, Any] = {}

    if prev_total_count != 0:
        trend["task_count_change_pct"] = (curr_total_count - prev_total_count) / prev_total_count * 100.0
    else:
        trend["task_count_change_pct"] = None

    if prev_avg_dur != 0.0:
        trend["avg_duration_change_pct"] = (curr_avg_dur - prev_avg_dur) / prev_avg_dur * 100.0
    else:
        trend["avg_duration_change_pct"] = None

    return trend


# ---------------------------------------------------------------------------
# Snapshot I/O
# ---------------------------------------------------------------------------


def save_snapshot(data: dict[str, Any], snapshot_dir: Path, week: str) -> None:
    """스냅샷 데이터를 JSON 파일로 저장한다. 디렉토리가 없으면 자동 생성한다."""
    snapshot_dir.mkdir(parents=True, exist_ok=True)
    filepath = snapshot_dir / f"week-{week}.json"
    filepath.write_text(json.dumps(data, ensure_ascii=False, indent=2))


def load_snapshot(snapshot_dir: Path, week: str) -> Optional[dict[str, Any]]:
    """지정된 주의 스냅샷 파일을 읽어 반환한다. 없으면 None을 반환한다."""
    filepath = snapshot_dir / f"week-{week}.json"
    if not filepath.exists():
        return None
    try:
        return json.loads(filepath.read_text())  # type: ignore[return-value]
    except (json.JSONDecodeError, OSError):
        return None


# ---------------------------------------------------------------------------
# Core build_report
# ---------------------------------------------------------------------------


def build_report(
    week: str,
    workspace: Path,
    tasks_file: Path,
    snapshot_dir: Path,
) -> dict[str, Any]:
    """주어진 week에 대한 회고 리포트를 생성하고 스냅샷으로 저장한 뒤 반환한다."""

    monday, sunday = get_week_period(week)
    since_str = str(monday)
    until_str = str(sunday)

    # 1. 태스크 데이터 로드 및 필터링
    all_tasks = load_tasks(tasks_file)
    week_tasks = filter_tasks_by_week(all_tasks, week)

    # 2. 팀별 메트릭 계산
    team_metrics = compute_team_metrics(week_tasks)

    # 3. git log 분석
    git_output = fetch_git_log(workspace, since_str, until_str)
    commits = parse_git_log(git_output)

    # 4. fix_pct를 팀 메트릭에 반영 (전체 fix_pct를 모든 팀에 동일 적용)
    global_fix_pct = compute_fix_pct(commits)
    for team_data in team_metrics.values():
        team_data["fix_pct"] = global_fix_pct

    # 5. 이전 주 스냅샷 로드
    prev_week = get_prev_week(week)
    prev_snapshot = load_snapshot(snapshot_dir, prev_week)
    prev_teams = prev_snapshot.get("teams") if prev_snapshot else None

    # 6. 트렌드 계산
    trend = compute_trend(team_metrics, prev_teams)

    # 7. 이상치 감지
    anomalies = detect_anomalies(team_metrics, prev_teams)

    # 8. 리포트 조립
    report: dict[str, Any] = {
        "week": week,
        "period": {
            "start": since_str,
            "end": until_str,
        },
        "teams": team_metrics,
        "commits": commits,
        "trend": trend,
        "anomalies": anomalies,
    }

    # 9. 스냅샷 저장
    save_snapshot(report, snapshot_dir, week)

    return report


# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------


def main() -> None:
    parser = argparse.ArgumentParser(description="주간 회고 시스템 — task-timer 데이터와 git log를 분석한다.")
    parser.add_argument(
        "--week",
        default=None,
        help="분석할 주 (YYYY-WW 형식, 기본: 현재 주)",
    )
    parser.add_argument(
        "--workspace",
        default=os.environ.get("WORKSPACE_ROOT", str(Path(__file__).resolve().parent.parent)),
        help="워크스페이스 루트 경로 (기본: $WORKSPACE_ROOT 또는 /home/jay/workspace)",
    )
    args = parser.parse_args()

    week = args.week or get_current_week()
    workspace = Path(args.workspace)
    tasks_file = workspace / "memory" / "task-timers.json"
    snapshot_dir = workspace / "memory" / "whisper" / "retro-snapshots"

    report = build_report(
        week=week,
        workspace=workspace,
        tasks_file=tasks_file,
        snapshot_dir=snapshot_dir,
    )

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


if __name__ == "__main__":
    main()
