"""file-cleanup.py - 파일 자동 정리 스크립트.

4가지 카테고리:
1. cokacdir 업로드 파일 정리 (30일 초과 미디어)
2. .done.clear 파일 정리 (30일 초과)
3. dispatch 지시서 정리 (90일 초과)
4. 시스템 로그 정리 (60일 초과)

실행 모드:
  python3 file-cleanup.py           → dry-run (삭제 대상만 출력)
  python3 file-cleanup.py --execute → 실제 삭제
  python3 file-cleanup.py --report  → 디스크 사용량 + 정리 가능 용량
  python3 file-cleanup.py --organize            → cokacdir 파일 정리 (dry-run)
  python3 file-cleanup.py --organize --execute  → 실제 이동
"""

import argparse
import logging
import os
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

COKACDIR_MEDIA_EXTENSIONS = {".pdf", ".png", ".jpg", ".jpeg"}
COKACDIR_PROTECTED_EXTENSIONS = {".py", ".sh", ".md"}
COKACDIR_THRESHOLD_DAYS = 30

DONE_CLEAR_THRESHOLD_DAYS = 30
DISPATCH_THRESHOLD_DAYS = 90
LOG_THRESHOLD_DAYS = 60

PROTECTED_FILENAMES = {"CLAUDE.md", "MEMORY.md", ".env", ".env.keys"}
PROTECTED_DIR_PARTS = {
    "reports",
    "research",
    "specs",
    "meetings",
    "plans",
    ".git",
    ".worktrees",
    "projects",
}

CLEANUP_LOG_FILENAME = "file-cleanup.log"

# ---------------------------------------------------------------------------
# Safety Checker
# ---------------------------------------------------------------------------


class SafetyChecker:
    """파일이 보호 대상인지 확인한다."""

    def __init__(self, base_dir: Path) -> None:
        self.base_dir = base_dir

    def is_protected(self, path: Path) -> bool:
        """주어진 경로가 보호 대상이면 True를 반환한다."""
        # 파일명 기준 보호
        if path.name in PROTECTED_FILENAMES:
            return True

        # 경로 내 보호 디렉토리 포함 여부
        parts = set(path.parts)
        if parts & PROTECTED_DIR_PARTS:
            return True

        # memory/reports 등 하위 경로 문자열 포함 여부
        path_str = str(path)
        for protected in ("memory/reports", "memory/research", "memory/specs", "memory/meetings", "memory/plans"):
            if protected in path_str:
                return True

        return False


# ---------------------------------------------------------------------------
# 1. CokacDir Upload Cleanup
# ---------------------------------------------------------------------------


def find_cokacdir_cleanup_candidates(workspace_dir: Path) -> list[dict[str, Any]]:
    """cokacdir workspace에서 30일 초과 미디어 파일을 찾아 반환한다.

    Args:
        workspace_dir: ~/.cokacdir/workspace/ 경로

    Returns:
        삭제 후보 목록. 각 항목은 path, size_bytes, days_old, category 포함.
    """
    if not workspace_dir.exists():
        return []

    candidates: list[dict[str, Any]] = []
    now = datetime.now().timestamp()

    # workspace/*/ 하위의 파일을 검색
    for project_dir in workspace_dir.iterdir():
        if not project_dir.is_dir():
            continue
        for file_path in project_dir.rglob("*"):
            if not file_path.is_file():
                continue
            suffix = file_path.suffix.lower()
            # 보호된 확장자 스킵
            if file_path.name in PROTECTED_FILENAMES:
                continue
            if file_path.suffix.lower() in COKACDIR_PROTECTED_EXTENSIONS:
                continue
            # 미디어 파일만
            if suffix not in COKACDIR_MEDIA_EXTENSIONS:
                continue
            # 30일 초과만
            mtime = file_path.stat().st_mtime
            age_seconds = now - mtime
            days_old = age_seconds / (24 * 3600)
            # 30일 초과만 대상 (정수 일수 기준, 정확히 30일은 제외)
            if int(days_old) <= COKACDIR_THRESHOLD_DAYS:
                continue
            candidates.append(
                {
                    "path": str(file_path),
                    "size_bytes": file_path.stat().st_size,
                    "days_old": days_old,
                    "category": "cokacdir",
                }
            )

    return candidates


# ---------------------------------------------------------------------------
# 2. .done.clear Cleanup
# ---------------------------------------------------------------------------


def find_done_clear_candidates(events_dir: Path) -> list[dict[str, Any]]:
    """memory/events/*.done.clear 파일 중 30일 초과 파일을 찾아 반환한다.

    Args:
        events_dir: memory/events/ 경로

    Returns:
        삭제 후보 목록.
    """
    if not events_dir.exists():
        return []

    candidates: list[dict[str, Any]] = []
    now = datetime.now().timestamp()

    for file_path in events_dir.iterdir():
        if not file_path.is_file():
            continue
        if not file_path.name.endswith(".done.clear"):
            continue
        mtime = file_path.stat().st_mtime
        age_seconds = now - mtime
        days_old = age_seconds / (24 * 3600)
        # 30일 초과만 대상 (정수 일수 기준, 정확히 30일은 제외)
        if int(days_old) <= DONE_CLEAR_THRESHOLD_DAYS:
            continue
        candidates.append(
            {
                "path": str(file_path),
                "size_bytes": file_path.stat().st_size,
                "days_old": days_old,
                "category": "done_clear",
            }
        )

    return candidates


# ---------------------------------------------------------------------------
# 3. Dispatch Task Cleanup
# ---------------------------------------------------------------------------


def find_dispatch_candidates(tasks_dir: Path) -> list[dict[str, Any]]:
    """memory/tasks/dispatch-*.md 파일 중 90일 초과 파일을 찾아 반환한다.

    Args:
        tasks_dir: memory/tasks/ 경로

    Returns:
        삭제 후보 목록.
    """
    if not tasks_dir.exists():
        return []

    candidates: list[dict[str, Any]] = []
    now = datetime.now().timestamp()

    for file_path in tasks_dir.iterdir():
        if not file_path.is_file():
            continue
        if not file_path.name.startswith("dispatch-"):
            continue
        if file_path.suffix.lower() != ".md":
            continue
        mtime = file_path.stat().st_mtime
        age_seconds = now - mtime
        days_old = age_seconds / (24 * 3600)
        # 90일 초과만 대상 (정수 일수 기준, 정확히 90일은 제외)
        if int(days_old) <= DISPATCH_THRESHOLD_DAYS:
            continue
        candidates.append(
            {
                "path": str(file_path),
                "size_bytes": file_path.stat().st_size,
                "days_old": days_old,
                "category": "dispatch",
            }
        )

    return candidates


# ---------------------------------------------------------------------------
# 4. System Log Cleanup
# ---------------------------------------------------------------------------


def find_log_candidates(logs_dir: Path) -> list[dict[str, Any]]:
    """workspace/logs/*.log 파일 중 60일 초과 파일을 찾아 반환한다.
    오늘 수정된 파일(활성 로그)은 스킵한다.
    file-cleanup.log 자체는 제외한다.

    Args:
        logs_dir: workspace/logs/ 경로

    Returns:
        삭제 후보 목록.
    """
    if not logs_dir.exists():
        return []

    candidates: list[dict[str, Any]] = []
    now = datetime.now().timestamp()
    today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()

    for file_path in logs_dir.iterdir():
        if not file_path.is_file():
            continue
        if file_path.suffix.lower() != ".log":
            continue
        # file-cleanup.log 자체 제외
        if file_path.name == CLEANUP_LOG_FILENAME:
            continue
        mtime = file_path.stat().st_mtime
        # 오늘 수정된 파일(활성 로그) 스킵
        if mtime >= today_start:
            continue
        age_seconds = now - mtime
        days_old = age_seconds / (24 * 3600)
        # 60일 초과만 대상 (정수 일수 기준, 정확히 60일은 제외)
        if int(days_old) <= LOG_THRESHOLD_DAYS:
            continue
        candidates.append(
            {
                "path": str(file_path),
                "size_bytes": file_path.stat().st_size,
                "days_old": days_old,
                "category": "logs",
            }
        )

    return candidates


# ---------------------------------------------------------------------------
# Minimum Keep Rule
# ---------------------------------------------------------------------------


def apply_minimum_keep(candidates: list[dict[str, Any]], keep: int = 1) -> list[dict[str, Any]]:
    """각 카테고리에서 최소 keep개 파일을 보존하도록 후보 목록을 축소한다.

    가장 최근 파일(days_old가 가장 작은 것)을 보존 대상으로 선택한다.

    Args:
        candidates: 삭제 후보 목록
        keep: 카테고리별 보존할 최소 파일 수

    Returns:
        축소된 삭제 후보 목록.
    """
    if not candidates:
        return []

    # 카테고리별로 그룹화
    by_category: dict[str, list[dict[str, Any]]] = {}
    for c in candidates:
        cat = c.get("category", "unknown")
        by_category.setdefault(cat, []).append(c)

    result: list[dict[str, Any]] = []
    for cat, items in by_category.items():
        if len(items) <= keep:
            # 보존 개수 이하면 모두 보존 (삭제 대상 없음)
            continue
        # days_old 오름차순 정렬 후 최근 keep개 제외
        sorted_items = sorted(items, key=lambda x: x["days_old"])
        # 최근 것 keep개 보존 → 나머지 삭제 대상
        result.extend(sorted_items[keep:])

    return result


# ---------------------------------------------------------------------------
# Execute Cleanup
# ---------------------------------------------------------------------------


def execute_cleanup(
    candidates: list[dict[str, Any]],
    dry_run: bool = True,
    cleanup_log_path: Path | None = None,
) -> dict[str, Any]:
    """후보 파일들을 삭제하거나(execute) 목록만 출력한다(dry-run).

    Args:
        candidates: 삭제 후보 목록
        dry_run: True면 실제 삭제 안 함 (기본값)
        cleanup_log_path: 삭제 로그 파일 경로 (None이면 기본 경로 미사용)

    Returns:
        결과 딕셔너리: deleted_count, dry_run, deleted_files, total_size_bytes
    """
    deleted_files: list[str] = []
    total_size = 0

    if not dry_run:
        for c in candidates:
            p = Path(c["path"])
            if p.exists():
                try:
                    p.unlink()
                    deleted_files.append(c["path"])
                    total_size += c.get("size_bytes", 0)
                except OSError as e:
                    logging.warning("삭제 실패: %s - %s", c["path"], e)

        # 로그 기록
        if cleanup_log_path and deleted_files:
            cleanup_log_path.parent.mkdir(parents=True, exist_ok=True)
            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            with cleanup_log_path.open("a", encoding="utf-8") as f:
                f.write(f"\n[{timestamp}] Cleanup executed - {len(deleted_files)} files deleted\n")
                for fp in deleted_files:
                    f.write(f"  DELETED: {fp}\n")

    return {
        "deleted_count": len(deleted_files),
        "dry_run": dry_run,
        "deleted_files": deleted_files,
        "total_size_bytes": total_size,
    }


# ---------------------------------------------------------------------------
# Collect All Candidates
# ---------------------------------------------------------------------------


def collect_all_candidates(
    cokacdir_workspace: Path,
    events_dir: Path,
    tasks_dir: Path,
    logs_dir: Path,
) -> dict[str, list[dict[str, Any]]]:
    """4가지 카테고리의 모든 정리 후보를 수집한다.

    Args:
        cokacdir_workspace: ~/.cokacdir/workspace/ 경로
        events_dir: memory/events/ 경로
        tasks_dir: memory/tasks/ 경로
        logs_dir: workspace/logs/ 경로

    Returns:
        카테고리별 후보 딕셔너리.
    """
    return {
        "cokacdir": find_cokacdir_cleanup_candidates(cokacdir_workspace),
        "done_clear": find_done_clear_candidates(events_dir),
        "dispatch": find_dispatch_candidates(tasks_dir),
        "logs": find_log_candidates(logs_dir),
    }


# ---------------------------------------------------------------------------
# Report Mode
# ---------------------------------------------------------------------------


def generate_report(
    base_dir: Path | None = None,
    logs_dir: Path | None = None,
    cokacdir_workspace: Path | None = None,
    events_dir: Path | None = None,
    tasks_dir: Path | None = None,
) -> dict[str, Any]:
    """현재 디스크 사용량과 정리 가능 용량을 보고한다.

    Args:
        base_dir: 기본 디렉토리 (None이면 /home/jay/workspace 사용)
        logs_dir: 로그 디렉토리 (None이면 base_dir/logs 사용)
        cokacdir_workspace: cokacdir workspace 경로
        events_dir: events 디렉토리 경로
        tasks_dir: tasks 디렉토리 경로

    Returns:
        disk_usage, cleanup_candidates, total_reclaimable_mb 포함 딕셔너리.
    """
    if base_dir is None:
        base_dir = Path(os.environ.get("WORKSPACE_ROOT", str(Path(__file__).resolve().parent.parent)))

    _logs_dir = logs_dir if logs_dir is not None else base_dir / "logs"
    _cokacdir_ws = cokacdir_workspace if cokacdir_workspace is not None else Path("/home/jay/.cokacdir/workspace")
    _events_dir = events_dir if events_dir is not None else base_dir / "memory" / "events"
    _tasks_dir = tasks_dir if tasks_dir is not None else base_dir / "memory" / "tasks"

    # 디스크 사용량
    stat = shutil.disk_usage(str(base_dir) if base_dir.exists() else "/")
    total_mb = stat.total / (1024 * 1024)
    used_mb = stat.used / (1024 * 1024)
    free_mb = stat.free / (1024 * 1024)

    disk_usage = {
        "total_mb": round(total_mb, 2),
        "used_mb": round(used_mb, 2),
        "free_mb": round(free_mb, 2),
    }

    # 정리 후보 수집
    all_candidates = collect_all_candidates(
        cokacdir_workspace=_cokacdir_ws,
        events_dir=_events_dir,
        tasks_dir=_tasks_dir,
        logs_dir=_logs_dir,
    )

    # 정리 가능 총 용량 (MB)
    total_bytes = sum(c.get("size_bytes", 0) for items in all_candidates.values() for c in items)
    total_reclaimable_mb = round(total_bytes / (1024 * 1024), 2)

    return {
        "disk_usage": disk_usage,
        "cleanup_candidates": all_candidates,
        "total_reclaimable_mb": total_reclaimable_mb,
    }


# ---------------------------------------------------------------------------
# Organize Mode
# ---------------------------------------------------------------------------


def find_organize_candidates(
    cokacdir_workspace: Path,
    uploads_base: Path,
    projects_base: Path,
) -> list[dict[str, Any]]:
    """cokacdir workspace의 미디어 파일을 적절한 위치로 이동할 후보를 찾는다.

    Args:
        cokacdir_workspace: ~/.cokacdir/workspace/ 경로
        uploads_base: /home/jay/workspace/uploads/ 기본 경로
        projects_base: /home/jay/projects/ 경로

    Returns:
        이동 후보 목록. 각 항목은 source, destination, reason 포함.
    """
    if not cokacdir_workspace.exists():
        return []

    candidates: list[dict[str, Any]] = []
    year_month = datetime.now().strftime("%Y-%m")

    for project_dir in cokacdir_workspace.iterdir():
        if not project_dir.is_dir():
            continue
        for file_path in project_dir.rglob("*"):
            if not file_path.is_file():
                continue
            suffix = file_path.suffix.lower()
            if suffix not in COKACDIR_MEDIA_EXTENSIONS:
                continue
            if file_path.name in PROTECTED_FILENAMES:
                continue

            # 기본: uploads/YYYY-MM/ 으로 이동
            destination = uploads_base / year_month / file_path.name
            reason = f"기본 업로드 폴더로 이동 ({year_month})"

            candidates.append(
                {
                    "source": str(file_path),
                    "destination": str(destination),
                    "reason": reason,
                }
            )

    return candidates


def execute_organize(
    moves: list[dict[str, Any]],
    dry_run: bool = True,
) -> dict[str, Any]:
    """이동 목록을 실행하거나(execute) 목록만 출력한다(dry-run).

    Args:
        moves: 이동 후보 목록
        dry_run: True면 실제 이동 안 함 (기본값)

    Returns:
        결과 딕셔너리: moved_count, dry_run, moved_files
    """
    moved_files: list[str] = []

    if not dry_run:
        for m in moves:
            src = Path(m["source"])
            dst = Path(m["destination"])
            if src.exists():
                dst.parent.mkdir(parents=True, exist_ok=True)
                try:
                    shutil.move(str(src), str(dst))
                    moved_files.append(m["source"])
                except OSError as e:
                    logging.warning("이동 실패: %s -> %s - %s", src, dst, e)

    return {
        "moved_count": len(moved_files),
        "dry_run": dry_run,
        "moved_files": moved_files,
    }


# ---------------------------------------------------------------------------
# Output Formatting
# ---------------------------------------------------------------------------


def _print_table(
    title: str,
    candidates: list[dict[str, Any]],
    columns: list[str],
) -> None:
    """후보 목록을 테이블 형식으로 출력한다."""
    print(f"\n{'=' * 70}")
    print(f"  {title}")
    print(f"{'=' * 70}")
    if not candidates:
        print("  (정리 대상 없음)")
        return

    # 헤더
    col_widths = {col: max(len(col), max(len(str(c.get(col, ""))) for c in candidates)) for col in columns}
    header = "  " + "  ".join(col.ljust(col_widths[col]) for col in columns)
    print(header)
    print("  " + "-" * (sum(col_widths.values()) + 2 * len(columns)))
    for c in candidates:
        row = "  " + "  ".join(str(c.get(col, "")).ljust(col_widths[col]) for col in columns)
        print(row)

    total_mb = sum(c.get("size_bytes", 0) for c in candidates) / (1024 * 1024)
    print(f"\n  총 {len(candidates)}개 파일, {total_mb:.2f} MB")


# ---------------------------------------------------------------------------
# Main Entry Point
# ---------------------------------------------------------------------------


def main() -> None:
    """CLI 진입점."""
    parser = argparse.ArgumentParser(
        description="파일 자동 정리 스크립트",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "--execute",
        action="store_true",
        help="실제 삭제/이동 실행 (없으면 dry-run)",
    )
    parser.add_argument(
        "--report",
        action="store_true",
        help="디스크 사용량 및 정리 가능 용량 보고",
    )
    parser.add_argument(
        "--organize",
        action="store_true",
        help="cokacdir workspace 파일 정리",
    )
    args = parser.parse_args()

    # 기본 경로 설정
    home = Path.home()
    workspace = home / "workspace"
    cokacdir_workspace = home / ".cokacdir" / "workspace"
    events_dir = workspace / "memory" / "events"
    tasks_dir = workspace / "memory" / "tasks"
    logs_dir = workspace / "logs"
    uploads_base = workspace / "uploads"
    projects_base = home / "projects"
    cleanup_log_path = logs_dir / CLEANUP_LOG_FILENAME

    dry_run = not args.execute

    # --report 모드
    if args.report:
        report = generate_report(
            base_dir=workspace,
            logs_dir=logs_dir,
            cokacdir_workspace=cokacdir_workspace,
            events_dir=events_dir,
            tasks_dir=tasks_dir,
        )
        print("\n" + "=" * 70)
        print("  디스크 사용량 보고")
        print("=" * 70)
        du = report["disk_usage"]
        print(f"  전체:  {du['total_mb']:>10.2f} MB")
        print(f"  사용:  {du['used_mb']:>10.2f} MB")
        print(f"  여유:  {du['free_mb']:>10.2f} MB")
        print(f"\n  정리 가능 총 용량: {report['total_reclaimable_mb']:.2f} MB")
        all_c = report["cleanup_candidates"]
        for cat, items in all_c.items():
            print(f"    [{cat}] {len(items)}개 파일")
        return

    # --organize 모드
    if args.organize:
        moves = find_organize_candidates(
            cokacdir_workspace=cokacdir_workspace,
            uploads_base=uploads_base,
            projects_base=projects_base,
        )
        mode_label = "실행 모드" if not dry_run else "DRY-RUN 모드"
        print(f"\n[organize - {mode_label}]")
        if not moves:
            print("  이동 대상 파일 없음.")
            return
        for m in moves:
            print(f"  {m['source']}")
            print(f"    -> {m['destination']}  ({m['reason']})")
        if not dry_run:
            result = execute_organize(moves, dry_run=False)
            print(f"\n  {result['moved_count']}개 파일 이동 완료.")
        else:
            print(f"\n  (dry-run) {len(moves)}개 파일 이동 예정. --execute 로 실행하세요.")
        return

    # 기본: 정리 dry-run 또는 execute
    all_candidates = collect_all_candidates(
        cokacdir_workspace=cokacdir_workspace,
        events_dir=events_dir,
        tasks_dir=tasks_dir,
        logs_dir=logs_dir,
    )

    # 최소 보존 규칙 적용
    all_flat: list[dict[str, Any]] = []
    for cat, items in all_candidates.items():
        safe = apply_minimum_keep(items, keep=1)
        all_flat.extend(safe)
        _print_table(
            f"[{cat}] 정리 대상",
            safe,
            ["path", "days_old", "size_bytes"],
        )

    mode_label = "실행 모드" if not dry_run else "DRY-RUN 모드"
    total_mb = sum(c.get("size_bytes", 0) for c in all_flat) / (1024 * 1024)
    print(f"\n{'=' * 70}")
    print(f"  {mode_label}: 총 {len(all_flat)}개 파일, {total_mb:.2f} MB")
    print("=" * 70)

    if not dry_run:
        result = execute_cleanup(all_flat, dry_run=False, cleanup_log_path=cleanup_log_path)
        print(f"  삭제 완료: {result['deleted_count']}개 파일")
    else:
        print("  --execute 플래그 없음. 실제 삭제를 원하면 --execute 를 추가하세요.")


if __name__ == "__main__":
    main()
