"""learnings-archiver.py - learnings.jsonl TTL 아카이브 + 스킬별 격리 필터 유틸리티.

learnings.jsonl에서 만료된 항목을 learnings-archive.jsonl로 이동하고,
스킬별 격리 필터로 활성 learnings를 반환하며,
새 learning을 추가하는 기능을 제공한다.
"""

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

# WORKSPACE_ROOT: 환경변수 우선, 없으면 스크립트 위치 기준으로 추론
# scripts/ 상위 디렉토리 = workspace root
WORKSPACE_ROOT = Path(os.environ.get("WORKSPACE_ROOT", Path(__file__).resolve().parent.parent))

LEARNINGS_PATH = WORKSPACE_ROOT / "memory" / "skill-learning" / "learnings.jsonl"
ARCHIVE_PATH = WORKSPACE_ROOT / "memory" / "skill-learning" / "learnings-archive.jsonl"

VALID_SOURCES = {
    "self-review",
    "cross-model",
    "jay-feedback",
    "online-expert",
    "github-learn",
    "champion-battle",
}


def _load_jsonl(path: Path) -> list[dict[object, object]]:
    """JSONL 파일을 읽어 dict 리스트로 반환한다. 파일이 없거나 비어있으면 빈 리스트."""
    if not path.exists():
        return []
    lines = path.read_text(encoding="utf-8").strip().splitlines()
    if not lines:
        return []
    entries: list[dict[object, object]] = []
    for line in lines:
        stripped = line.strip()
        if stripped:
            entries.append(json.loads(stripped))
    return entries


def _write_jsonl(path: Path, entries: list[dict[object, object]]) -> None:
    """dict 리스트를 JSONL 파일로 쓴다 (덮어쓰기)."""
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8") as f:
        for entry in entries:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")


def _append_jsonl(path: Path, entries: list[dict[object, object]]) -> None:
    """dict 리스트를 JSONL 파일에 append한다."""
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as f:
        for entry in entries:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")


def _generate_id(existing_entries: list[dict[object, object]]) -> str:
    """learn-NNN 형식의 id를 생성한다. 충돌 시 UUID를 사용한다."""
    existing_ids = {str(e.get("id", "")) for e in existing_entries}
    # 기존 learn-NNN 중 최대값 탐색
    max_num = 0
    for entry_id in existing_ids:
        if isinstance(entry_id, str) and entry_id.startswith("learn-"):
            try:
                num = int(entry_id[6:])
                if num > max_num:
                    max_num = num
            except ValueError:
                pass
    candidate = f"learn-{max_num + 1:03d}"
    if candidate not in existing_ids:
        return candidate
    # 충돌 시 UUID 사용
    return str(uuid.uuid4())


def _is_expired(entry: dict[object, object], now: datetime) -> bool:
    """항목이 만료되었는지 확인한다.

    jay-feedback 예외: source가 jay-feedback이면 TTL과 무관하게 영구 유지 (immutable).
    """
    # jay-feedback는 TTL과 무관하게 영구 유지 (immutable)
    if entry.get("source") == "jay-feedback":
        return False
    expires_at = entry.get("expires_at")
    if expires_at is None:
        return False
    expires_dt = datetime.fromisoformat(str(expires_at))
    return expires_dt < now


def archive_expired(
    learnings_path: Optional[Path] = None,
    archive_path: Optional[Path] = None,
) -> int:
    """learnings.jsonl에서 만료된 항목을 learnings-archive.jsonl로 이동한다.

    jay-feedback 예외: source가 jay-feedback이고 expires_at이 null인 항목은 아카이브 안 함.
    append-only 보장: 원본에서 삭제만, archive에 추가만.

    Args:
        learnings_path: learnings.jsonl 경로 (기본값: LEARNINGS_PATH)
        archive_path: learnings-archive.jsonl 경로 (기본값: ARCHIVE_PATH)

    Returns:
        아카이브된 항목 수
    """
    lpath = learnings_path if learnings_path is not None else LEARNINGS_PATH
    apath = archive_path if archive_path is not None else ARCHIVE_PATH

    entries = _load_jsonl(lpath)
    if not entries:
        return 0

    now = datetime.now()
    active: list[dict[object, object]] = []
    expired: list[dict[object, object]] = []

    for entry in entries:
        if _is_expired(entry, now):
            expired.append(entry)
        else:
            active.append(entry)

    if not expired:
        return 0

    # archive에 만료 항목 append
    _append_jsonl(apath, expired)

    # 원본을 활성 항목만으로 덮어쓰기
    _write_jsonl(lpath, active)

    return len(expired)


def get_learnings(
    skill_name: str,
    learnings_path: Optional[Path] = None,
) -> list[dict[object, object]]:
    """특정 스킬의 활성 learnings만 반환한다.

    활성 조건: expires_at > now 또는 expires_at == null.
    다른 스킬의 learning은 절대 포함하지 않는다 (크로스 오염 방지).

    Args:
        skill_name: 조회할 스킬 이름
        learnings_path: learnings.jsonl 경로 (기본값: LEARNINGS_PATH)

    Returns:
        해당 스킬의 활성 learning dict 리스트
    """
    lpath = learnings_path if learnings_path is not None else LEARNINGS_PATH

    entries = _load_jsonl(lpath)
    if not entries:
        return []

    now = datetime.now()
    result: list[dict[object, object]] = []

    for entry in entries:
        # 스킬 필터 (크로스 오염 방지)
        if entry.get("skill_name") != skill_name:
            continue
        # 만료 필터 (활성 항목만)
        if _is_expired(entry, now):
            continue
        result.append(entry)

    return result


def add_learning(
    skill_name: str,
    source: str,
    learning: str,
    learnings_path: Optional[Path] = None,
    archive_path: Optional[Path] = None,
) -> dict[object, object]:
    """새 learning을 learnings.jsonl에 추가한다.

    Args:
        skill_name: 스킬 이름
        source: 학습 출처 (유효값: self-review, cross-model, jay-feedback,
                online-expert, github-learn, champion-battle)
        learning: 학습 내용 텍스트
        learnings_path: learnings.jsonl 경로 (기본값: LEARNINGS_PATH)
        archive_path: learnings-archive.jsonl 경로 (기본값: ARCHIVE_PATH)

    Returns:
        추가된 learning entry dict

    Raises:
        ValueError: source가 유효하지 않은 경우
    """
    if source not in VALID_SOURCES:
        raise ValueError(f"Invalid source: '{source}'. " f"Must be one of: {', '.join(sorted(VALID_SOURCES))}")

    lpath = learnings_path if learnings_path is not None else LEARNINGS_PATH

    existing_entries = _load_jsonl(lpath)
    entry_id = _generate_id(existing_entries)

    now = datetime.now()
    created_at = now.isoformat()

    # jay-feedback은 expires_at = null (영구 유지)
    if source == "jay-feedback":
        expires_at = None
    else:
        expires_at = (now + timedelta(days=60)).isoformat()

    entry: dict[object, object] = {
        "id": entry_id,
        "skill_name": skill_name,
        "source": source,
        "learning": learning,
        "created_at": created_at,
        "expires_at": expires_at,
    }

    _append_jsonl(lpath, [entry])
    return entry


def _cmd_archive(args: argparse.Namespace) -> None:
    """archive 서브커맨드 핸들러."""
    count = archive_expired()
    if count == 0:
        print("아카이브할 만료 항목이 없습니다.")
    else:
        print(f"{count}개 항목을 아카이브했습니다.")


def _cmd_get(args: argparse.Namespace) -> None:
    """get 서브커맨드 핸들러."""
    skill_name: str = args.skill
    learnings = get_learnings(skill_name)
    if not learnings:
        print(f"'{skill_name}' 스킬의 활성 learnings가 없습니다.")
        return
    print(f"'{skill_name}' 스킬의 활성 learnings ({len(learnings)}개):")
    for entry in learnings:
        print(json.dumps(entry, ensure_ascii=False, indent=2))


def _cmd_add(args: argparse.Namespace) -> None:
    """add 서브커맨드 핸들러."""
    skill_name: str = args.skill
    source: str = args.source
    learning_text: str = args.learning
    try:
        entry = add_learning(
            skill_name=skill_name,
            source=source,
            learning=learning_text,
        )
        print(f"Learning 추가 완료: {entry['id']}")
        print(json.dumps(entry, ensure_ascii=False, indent=2))
    except ValueError as e:
        print(f"오류: {e}", file=sys.stderr)
        sys.exit(1)


def main() -> None:
    """CLI 진입점."""
    parser = argparse.ArgumentParser(description="learnings.jsonl TTL 아카이브 + 스킬별 격리 필터 유틸리티")
    subparsers = parser.add_subparsers(dest="command", help="명령어")

    # archive 서브커맨드
    subparsers.add_parser(
        "archive",
        help="만료된 learnings를 learnings-archive.jsonl로 이동",
    )

    # get 서브커맨드
    get_parser = subparsers.add_parser(
        "get",
        help="특정 스킬의 활성 learnings 조회",
    )
    get_parser.add_argument(
        "--skill",
        required=True,
        help="조회할 스킬 이름",
    )

    # add 서브커맨드
    add_parser = subparsers.add_parser(
        "add",
        help="새 learning 추가",
    )
    add_parser.add_argument(
        "--skill",
        required=True,
        help="스킬 이름",
    )
    add_parser.add_argument(
        "--source",
        required=True,
        help=("학습 출처 " "(self-review|cross-model|jay-feedback|online-expert|github-learn|champion-battle)"),
    )
    add_parser.add_argument(
        "--learning",
        required=True,
        help="학습 내용 텍스트",
    )

    args = parser.parse_args()

    if args.command == "archive":
        _cmd_archive(args)
    elif args.command == "get":
        _cmd_get(args)
    elif args.command == "add":
        _cmd_add(args)
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()
