"""Task oracle cache — tracks verified oracle scores per task hash.

A task is considered "verified" when its oracle score == 1.0 and the stored
hash matches the current state of its key files.  Any file change bumps the
hash and forces a re-run.

Cache file: <tasks_dir>/.verified_tasks.json
Schema:
    {
      "<task-name>": {
        "hash":        "<sha256 hex>",
        "oracle_score": 1.0,
        "floor":        0.0,
        "verified_at": "2026-03-14T10:00:00"
      }
    }
"""

from __future__ import annotations

import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path

# Files that determine task behaviour — changes to any of these invalidate the cache.
_HASH_FILES = [
    "environment/Dockerfile",
    "environment/docker-compose.yaml",
    "tests/evaluate.py",
    "task.toml",
    "solution/solve.sh",
    "instruction.md",
]


def task_hash(task_dir: Path) -> str:
    """Return a SHA-256 hex digest over the task's key files.

    Files are processed in a fixed order; missing files contribute an empty
    string so the hash is still stable across tasks that lack optional files.
    """
    h = hashlib.sha256()
    for rel in _HASH_FILES:
        p = task_dir / rel
        h.update(rel.encode())          # include filename so renames matter
        h.update(p.read_bytes() if p.exists() else b"")
    # Also hash any extra data/*.py files (e.g. needles.py) so data changes
    # are caught without enumerating them explicitly.
    data_dir = task_dir / "data"
    if data_dir.is_dir():
        for p in sorted(data_dir.glob("*.py")):
            h.update(p.name.encode())
            h.update(p.read_bytes())
    return h.hexdigest()


def load_cache(tasks_dir: Path) -> dict:
    """Load the verified-tasks cache; return {} if missing or malformed."""
    cache_path = tasks_dir / ".verified_tasks.json"
    if not cache_path.exists():
        return {}
    try:
        return json.loads(cache_path.read_text())
    except (json.JSONDecodeError, OSError):
        return {}


def save_cache(tasks_dir: Path, cache: dict) -> None:
    """Persist the cache atomically."""
    cache_path = tasks_dir / ".verified_tasks.json"
    tmp = cache_path.with_suffix(".json.tmp")
    tmp.write_text(json.dumps(cache, indent=2))
    tmp.replace(cache_path)


def is_verified(task_name: str, task_dir: Path, cache: dict) -> bool:
    """Return True if the task has a cached oracle score of 1.0 and hash matches."""
    entry = cache.get(task_name)
    if not entry:
        return False
    if entry.get("oracle_score", 0.0) < 1.0:
        return False
    return entry.get("hash") == task_hash(task_dir)


def record_result(
    task_name: str,
    task_dir: Path,
    oracle_score: float,
    cache: dict,
    floor: float | None = None,
) -> None:
    """Upsert an oracle result into the in-memory cache dict.

    Args:
        floor: Pre-solve baseline score (evaluate before solve). Some tasks
               start with partial credit; storing the floor lets evals measure
               the *delta* the agent actually contributed.
    """
    entry: dict = {
        "hash": task_hash(task_dir),
        "oracle_score": oracle_score,
        "verified_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
    }
    if floor is not None:
        entry["floor"] = floor
    cache[task_name] = entry
