"""MEMORY.md 관리 모듈 - Frozen Snapshot 패턴.

Hermes Agent의 memory_tool.py MemoryStore 설계를 참고하여
dev2-team 시스템에 맞게 재설계한 경량 메모리 관리 모듈.

주요 기능:
  - Frozen Snapshot: 세션 시작 시 파일을 읽어 FrozenMemory 객체로 캐시.
    재호출 시 동일 객체 반환(캐시). 파일 변경이 세션 중 반영되지 않음.
  - 문자 제한: update_memory()에서 max_chars 초과 시 거부.
  - 인젝션 탐지: update_memory() 호출 시 injection_guard 재사용.
  - 파일 잠금: fcntl.flock 사용 (Linux 전용).

Usage:
    from utils.memory_manager import load_frozen_memory, update_memory

    snapshot = load_frozen_memory("/path/to/MEMORY.md")
    print(snapshot.content)

    ok = update_memory("/path/to/MEMORY.md", "new content")
"""

import fcntl
import os
import tempfile
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Union

from utils.injection_guard import scan_content


@dataclass
class FrozenMemory:
    """세션 시작 시 캡처된 메모리 스냅샷 (불변)."""

    content: str
    snapshot_time: datetime
    char_count: int


# ---------------------------------------------------------------------------
# 모듈 수준 캐시: 경로 → FrozenMemory
# 세션 중 동일 경로 재호출 시 동일 객체 반환
# ---------------------------------------------------------------------------
_snapshot_cache: dict[str, FrozenMemory] = {}


def load_frozen_memory(path: Union[str, Path]) -> FrozenMemory:
    """MEMORY.md를 읽어 FrozenMemory 스냅샷을 반환합니다.

    동일 경로를 재호출하면 캐시된 동일 객체를 반환합니다.
    파일이 없으면 빈 FrozenMemory를 반환합니다.

    Args:
        path: MEMORY.md 파일 경로 (str 또는 Path).

    Returns:
        FrozenMemory: 캡처된 스냅샷 객체.
    """
    resolved = str(Path(path).resolve())

    if resolved in _snapshot_cache:
        return _snapshot_cache[resolved]

    file_path = Path(resolved)
    if file_path.exists():
        try:
            content = file_path.read_text(encoding="utf-8")
        except (OSError, IOError):
            content = ""
    else:
        content = ""

    snapshot = FrozenMemory(
        content=content,
        snapshot_time=datetime.now(),
        char_count=len(content),
    )
    _snapshot_cache[resolved] = snapshot
    return snapshot


def update_memory(
    path: Union[str, Path],
    new_content: str,
    max_chars: int = 2200,
) -> bool:
    """MEMORY.md를 새 내용으로 갱신합니다.

    인젝션 패턴 탐지와 문자 수 제한을 검사합니다.
    파일 잠금(fcntl.flock)으로 동시 쓰기를 방지합니다.

    Args:
        path: MEMORY.md 파일 경로 (str 또는 Path).
        new_content: 저장할 새 내용.
        max_chars: 최대 허용 문자 수 (기본값 2200).

    Returns:
        bool: 성공 시 True, 실패 시 False.
    """
    file_path = Path(path).resolve()

    # 1. 인젝션 검사
    scan_result = scan_content(new_content)
    if not scan_result.is_safe:
        return False

    # 2. 문자 수 제한 검사
    if len(new_content) > max_chars:
        return False

    # 3. 부모 디렉토리 생성
    try:
        file_path.parent.mkdir(parents=True, exist_ok=True)
    except OSError:
        return False

    # 4. 잠금 파일 경로
    lock_path = file_path.with_suffix(file_path.suffix + ".lock")

    try:
        lock_fd = open(lock_path, "w")
        try:
            fcntl.flock(lock_fd, fcntl.LOCK_EX)
            _atomic_write(file_path, new_content)
        finally:
            fcntl.flock(lock_fd, fcntl.LOCK_UN)
            lock_fd.close()
    except (OSError, IOError, RuntimeError):
        return False

    return True


def scan_memory_injection(content: str) -> list[str]:
    """메모리 내용에서 인젝션 패턴을 탐지합니다.

    injection_guard.scan_content()를 재사용하여 탐지된
    패턴 이름 목록을 반환합니다.

    Args:
        content: 검사할 문자열.

    Returns:
        list[str]: 탐지된 위협의 pattern_name 목록.
                   안전하면 빈 리스트.
    """
    result = scan_content(content)
    if result.is_safe:
        return []
    return [threat.pattern_name for threat in result.threats]


# ---------------------------------------------------------------------------
# 내부 헬퍼
# ---------------------------------------------------------------------------


def _atomic_write(path: Path, content: str) -> None:
    """임시 파일 + os.replace()로 원자적 쓰기."""
    fd, tmp_path = tempfile.mkstemp(
        dir=str(path.parent),
        suffix=".tmp",
        prefix=".mem_",
    )
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(content)
            f.flush()
            os.fsync(f.fileno())
        os.replace(tmp_path, str(path))
    except BaseException:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise
