"""
Incremental processing cache — skips files whose SHA-256 + RunConfig hash
match a previous successful run.

Cache file: .extractor_cache.json  (sibling of the --out directory)
Format:
{
  "schema_version": "1.0",
  "entries": {
    "<source_sha256>:<run_config_hash>": {
      "output_jsonl": "relative/path.elements.jsonl",
      "manifest_path": "relative/path.manifest.json",
      "extracted_at": "2025-03-24T10:00:00Z",
      "source_sha256": "...",
      "run_config_hash": "..."
    }
  }
}
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

_CACHE_SCHEMA_VERSION = "1.0"


@dataclass
class CacheEntry:
    source_sha256: str
    run_config_hash: str
    output_jsonl: str  # relative path string
    manifest_path: str  # relative path string
    extracted_at: str


@dataclass
class ManifestCache:
    cache_path: Path
    _entries: dict[str, CacheEntry] = field(default_factory=dict)

    @classmethod
    def load(cls, cache_path: Path) -> ManifestCache:
        obj = cls(cache_path=cache_path)
        if not cache_path.exists():
            return obj
        try:
            data = json.loads(cache_path.read_text(encoding="utf-8"))
            if data.get("schema_version") != _CACHE_SCHEMA_VERSION:
                return obj  # invalidate on version mismatch
            for key, entry in data.get("entries", {}).items():
                obj._entries[key] = CacheEntry(**entry)
        except Exception:
            pass  # corrupt cache → start fresh
        return obj

    def _key(self, source_sha256: str, run_config_hash: str) -> str:
        return f"{source_sha256}:{run_config_hash}"

    def get(self, source_sha256: str, run_config_hash: str) -> CacheEntry | None:
        return self._entries.get(self._key(source_sha256, run_config_hash))

    def put(self, entry: CacheEntry) -> None:
        key = self._key(entry.source_sha256, entry.run_config_hash)
        self._entries[key] = entry

    def save(self) -> None:
        data: dict[str, Any] = {
            "schema_version": _CACHE_SCHEMA_VERSION,
            "entries": {
                key: {
                    "source_sha256": e.source_sha256,
                    "run_config_hash": e.run_config_hash,
                    "output_jsonl": e.output_jsonl,
                    "manifest_path": e.manifest_path,
                    "extracted_at": e.extracted_at,
                }
                for key, e in self._entries.items()
            },
        }
        tmp = self.cache_path.with_suffix(".tmp")
        tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
        tmp.rename(self.cache_path)

    def clear_older_than(self, days: int) -> int:
        """Remove entries older than `days` days. Returns count removed."""
        from datetime import timedelta

        cutoff = datetime.now(timezone.utc) - timedelta(days=days)
        to_remove = []
        for key, entry in self._entries.items():
            try:
                dt = datetime.fromisoformat(entry.extracted_at)
                if dt < cutoff:
                    to_remove.append(key)
            except Exception:
                pass
        for key in to_remove:
            del self._entries[key]
        return len(to_remove)

    def clear_all(self) -> int:
        count = len(self._entries)
        self._entries.clear()
        return count


def run_config_hash(run_config) -> str:
    """Stable SHA-256 hash of a RunConfig for cache keying."""
    data = run_config.model_dump(exclude_none=True)
    # Remove fields that don't affect output content
    data.pop("workers", None)
    canonical = json.dumps(data, sort_keys=True, ensure_ascii=True)
    return hashlib.sha256(canonical.encode()).hexdigest()[:16]
