#!/usr/bin/env python3
"""
drive-change-log.py - Google Drive 업로드 변경사항 로컬 로그 관리

Cloud Functions의 crawlYoutubeChannels가 Drive에 파일을 업로드할 때,
해당 변경사항을 로컬 로그에 기록하여 project-map incremental 시스템이 참조할 수 있게 합니다.

Usage:
    # 변경 기록 추가
    python3 drive-change-log.py add --project insuwiki --path "04_유튜브요약/보험명의정닥터/260301_영상제목_요약.md" --action upload

    # 미처리 변경 목록 조회
    python3 drive-change-log.py list --project insuwiki [--unprocessed]

    # 처리 완료 마킹
    python3 drive-change-log.py mark-processed --project insuwiki --ids "id1,id2"

    # 로그 정리 (30일 이상 된 처리 완료 항목 삭제)
    python3 drive-change-log.py cleanup --project insuwiki [--days 30]
"""

import argparse
import fcntl
import json
import os
import sys
import uuid
from datetime import datetime, timedelta
from pathlib import Path

# 로그 파일 기본 디렉토리
_WORKSPACE_ROOT = os.environ.get("WORKSPACE_ROOT", str(Path(__file__).resolve().parent.parent))
DEFAULT_LOG_DIR = str(Path(_WORKSPACE_ROOT) / "memory" / "drive-changes")


def get_log_path(project_id: str, log_dir: str = DEFAULT_LOG_DIR) -> Path:
    """프로젝트 ID에 해당하는 로그 파일 경로 반환."""
    return Path(log_dir) / f"{project_id}.jsonl"


def read_log_lines(log_path: Path) -> list:
    """JSONL 파일에서 모든 항목 읽기."""
    if not log_path.exists():
        return []
    entries = []
    with open(log_path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                entries.append(json.loads(line))
            except json.JSONDecodeError:
                # 손상된 라인 건너뜀
                continue
    return entries


def write_log_lines(log_path: Path, entries: list) -> None:
    """JSONL 파일에 항목 전체 다시 쓰기 (덮어쓰기)."""
    log_path.parent.mkdir(parents=True, exist_ok=True)
    tmp_path = str(log_path) + ".tmp"
    try:
        with open(tmp_path, "w", encoding="utf-8") as f:
            for entry in entries:
                f.write(json.dumps(entry, ensure_ascii=False) + "\n")
        os.rename(tmp_path, str(log_path))
    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise


def cmd_add(args) -> dict:
    """변경 기록 추가."""
    log_path = get_log_path(args.project, args.log_dir)
    log_path.parent.mkdir(parents=True, exist_ok=True)

    entry = {
        "id": str(uuid.uuid4()),
        "project": args.project,
        "path": args.path,
        "action": args.action,
        "timestamp": datetime.now().isoformat(timespec="seconds"),
        "processed": False,
    }

    # fcntl LOCK_EX로 동시 접근 방지
    lock_path = str(log_path) + ".lock"
    try:
        with open(lock_path, "w") as lock_fd:
            fcntl.flock(lock_fd, fcntl.LOCK_EX)
            try:
                log_path.parent.mkdir(parents=True, exist_ok=True)
                with open(str(log_path), "a", encoding="utf-8") as f:
                    f.write(json.dumps(entry, ensure_ascii=False) + "\n")
            finally:
                fcntl.flock(lock_fd, fcntl.LOCK_UN)
    except OSError as e:
        return {"status": "error", "message": f"로그 쓰기 실패: {e}"}

    return {"status": "ok", "entry": entry}


def cmd_list(args) -> dict:
    """변경사항 목록 조회."""
    log_path = get_log_path(args.project, args.log_dir)

    lock_path = str(log_path) + ".lock"
    try:
        with open(lock_path, "w") as lock_fd:
            fcntl.flock(lock_fd, fcntl.LOCK_SH)
            try:
                entries = read_log_lines(log_path)
            finally:
                fcntl.flock(lock_fd, fcntl.LOCK_UN)
    except OSError as e:
        return {"status": "error", "message": f"로그 읽기 실패: {e}"}

    if args.unprocessed:
        entries = [e for e in entries if not e.get("processed", False)]

    return {"status": "ok", "project": args.project, "count": len(entries), "entries": entries}


def cmd_mark_processed(args) -> dict:
    """특정 ID의 변경사항을 처리 완료로 마킹."""
    log_path = get_log_path(args.project, args.log_dir)

    if not log_path.exists():
        return {"status": "error", "message": f"로그 파일 없음: {log_path}"}

    ids_to_mark = set(i.strip() for i in args.ids.split(",") if i.strip())
    if not ids_to_mark:
        return {"status": "error", "message": "마킹할 ID가 없습니다."}

    lock_path = str(log_path) + ".lock"
    marked_count = 0
    not_found = []

    try:
        with open(lock_path, "w") as lock_fd:
            fcntl.flock(lock_fd, fcntl.LOCK_EX)
            try:
                entries = read_log_lines(log_path)
                id_set = {e.get("id") for e in entries}
                for target_id in ids_to_mark:
                    if target_id not in id_set:
                        not_found.append(target_id)

                updated = []
                for entry in entries:
                    if entry.get("id") in ids_to_mark:
                        entry["processed"] = True
                        marked_count += 1
                    updated.append(entry)

                write_log_lines(log_path, updated)
            finally:
                fcntl.flock(lock_fd, fcntl.LOCK_UN)
    except OSError as e:
        return {"status": "error", "message": f"처리 완료 마킹 실패: {e}"}

    result = {"status": "ok", "project": args.project, "marked": marked_count}
    if not_found:
        result["not_found"] = not_found
    return result


def cmd_cleanup(args) -> dict:
    """오래된 처리 완료 항목 삭제."""
    log_path = get_log_path(args.project, args.log_dir)

    if not log_path.exists():
        return {"status": "ok", "project": args.project, "deleted": 0, "message": "로그 파일 없음"}

    days = args.days
    cutoff = datetime.now() - timedelta(days=days)

    lock_path = str(log_path) + ".lock"
    deleted_count = 0

    try:
        with open(lock_path, "w") as lock_fd:
            fcntl.flock(lock_fd, fcntl.LOCK_EX)
            try:
                entries = read_log_lines(log_path)
                kept = []
                for entry in entries:
                    # 처리 완료이고 오래된 항목만 삭제
                    if entry.get("processed", False):
                        try:
                            ts = datetime.fromisoformat(entry.get("timestamp", ""))
                            if ts < cutoff:
                                deleted_count += 1
                                continue
                        except (ValueError, TypeError):
                            # 타임스탬프 파싱 실패 시 보존
                            pass
                    kept.append(entry)
                write_log_lines(log_path, kept)
            finally:
                fcntl.flock(lock_fd, fcntl.LOCK_UN)
    except OSError as e:
        return {"status": "error", "message": f"정리 실패: {e}"}

    return {
        "status": "ok",
        "project": args.project,
        "deleted": deleted_count,
        "days": days,
    }


def parse_args():
    parser = argparse.ArgumentParser(
        description="Google Drive 업로드 변경사항 로컬 로그 관리"
    )
    parser.add_argument(
        "--log-dir",
        default=DEFAULT_LOG_DIR,
        help=f"로그 파일 디렉토리 (기본값: {DEFAULT_LOG_DIR})",
    )

    subparsers = parser.add_subparsers(dest="command", help="명령어")
    subparsers.required = True

    # add 명령
    add_parser = subparsers.add_parser("add", help="변경 기록 추가")
    add_parser.add_argument("--project", required=True, help="프로젝트 ID (예: insuwiki)")
    add_parser.add_argument("--path", required=True, help="Drive 파일 경로 (상대 경로)")
    add_parser.add_argument(
        "--action",
        default="upload",
        choices=["upload", "update", "delete"],
        help="변경 유형 (기본값: upload)",
    )

    # list 명령
    list_parser = subparsers.add_parser("list", help="변경사항 목록 조회")
    list_parser.add_argument("--project", required=True, help="프로젝트 ID")
    list_parser.add_argument(
        "--unprocessed",
        action="store_true",
        default=False,
        help="미처리 변경 건만 조회",
    )

    # mark-processed 명령
    mark_parser = subparsers.add_parser("mark-processed", help="처리 완료 마킹")
    mark_parser.add_argument("--project", required=True, help="프로젝트 ID")
    mark_parser.add_argument("--ids", required=True, help="처리 완료할 ID 목록 (쉼표 구분)")

    # cleanup 명령
    cleanup_parser = subparsers.add_parser("cleanup", help="오래된 처리 완료 항목 삭제")
    cleanup_parser.add_argument("--project", required=True, help="프로젝트 ID")
    cleanup_parser.add_argument(
        "--days",
        type=int,
        default=30,
        help="삭제 기준 일수 (기본값: 30일)",
    )

    return parser.parse_args()


def main():
    args = parse_args()

    command_map = {
        "add": cmd_add,
        "list": cmd_list,
        "mark-processed": cmd_mark_processed,
        "cleanup": cmd_cleanup,
    }

    handler = command_map.get(args.command)
    if handler is None:
        result = {"status": "error", "message": f"알 수 없는 명령: {args.command}"}
    else:
        try:
            result = handler(args)
        except Exception as e:
            result = {"status": "error", "message": str(e)}

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

    if result.get("status") != "ok":
        sys.exit(1)


if __name__ == "__main__":
    main()
