"""Persistent code cache for in-progress coding problems.

Stores user code per problem in ~/.skilark/code_cache/{language}.yaml.
"""

import os
from pathlib import Path

import yaml


class CodeCache:
    """Per-language code cache backed by a YAML file on disk."""

    MAX_HISTORY = 5

    def __init__(self, language: str, config_dir: Path | None = None):
        if config_dir is not None:
            base = Path(config_dir)
        else:
            env_val = os.environ.get("SKILARK_CONFIG_DIR")
            base = Path(env_val) if env_val else Path.home() / ".skilark"
        self._cache_dir = base / "code_cache"
        self._file = self._cache_dir / f"{language}.yaml"

    def _read_all(self) -> dict:
        if not self._file.exists():
            return {}
        with open(self._file) as f:
            data = yaml.safe_load(f)
        return data if isinstance(data, dict) else {}

    def _write_all(self, data: dict) -> None:
        self._cache_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
        content = yaml.dump(data, default_flow_style=False, allow_unicode=True)
        fd = os.open(self._file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
        try:
            os.write(fd, content.encode())
            os.fsync(fd)
        finally:
            os.close(fd)

    def _get_entry(self, data: dict, problem_id: str) -> dict:
        entry = data.get(problem_id)
        if not isinstance(entry, dict):
            return {"current": None, "attempts": [], "passed": []}
        return {
            "current": entry.get("current"),
            "attempts": entry.get("attempts") or [],
            "passed": entry.get("passed") or [],
        }

    def load(self, problem_id: str) -> dict:
        """Return {current: str|None, attempts: list[str], passed: list[str]}."""
        data = self._read_all()
        return self._get_entry(data, problem_id)

    def save_current(self, problem_id: str, code: str) -> None:
        """Update the current (in-progress) code for a problem."""
        data = self._read_all()
        entry = self._get_entry(data, problem_id)
        entry["current"] = code
        data[problem_id] = entry
        self._write_all(data)

    def clear_current(self, problem_id: str) -> None:
        """Remove the current field (called on solve)."""
        data = self._read_all()
        if problem_id not in data:
            return
        entry = self._get_entry(data, problem_id)
        entry["current"] = None
        data[problem_id] = entry
        self._write_all(data)

    def record_attempt(self, problem_id: str, code: str, *, passed: bool) -> None:
        """Append code to attempts (fail) or passed (success). Dedup + cap at 5."""
        data = self._read_all()
        entry = self._get_entry(data, problem_id)
        key = "passed" if passed else "attempts"
        history = entry[key]
        if history and history[-1] == code:
            return
        history = [h for h in history if h != code]
        history.append(code)
        entry[key] = history[-self.MAX_HISTORY:]
        data[problem_id] = entry
        self._write_all(data)

    def is_new_attempt(self, problem_id: str, code: str) -> bool:
        """Return True if code differs from the last entry in both attempts and passed."""
        entry = self.load(problem_id)
        return self._is_new(entry, code, check_lists=["attempts", "passed"])

    def is_new_pass(self, problem_id: str, code: str) -> bool:
        """Return True if code differs from the last entry in the passed list only."""
        entry = self.load(problem_id)
        return self._is_new(entry, code, check_lists=["passed"])

    @staticmethod
    def _is_new(entry: dict, code: str, *, check_lists: list[str]) -> bool:
        for key in check_lists:
            history = entry[key]
            if history and history[-1] == code:
                return False
        return True
