"""Content-addressed cache — the margin lever.

Identical audio is analyzed ONCE and served forever. The key is a content id =
sha256 of the DECODED audio PCM (so re-encodes / re-uploads of the same sound
dedupe), with engine params folded into a separate params_hash (so different
settings cache separately). A youtube-id/URL alias resolves to a content id so a
repeat source can skip the download (#29).

Artifacts are TRANSIENT (freebox SSOT, rebuildable) → an LRU/size-cap eviction
keeps the footprint bounded. Cheap JSON results live inline; big binaries are
referenced by path (filled in by #27).
"""
import hashlib
import json
import time
from pathlib import Path

import db


# ── addressing ──────────────────────────────────────────────────────────────
def content_id(audio_path: str | Path) -> str:
    """sha256 of decoded PCM (+ samplerate). Falls back to raw file bytes if the
    format can't be decoded here (still dedupes identical files).

    STREAMED (#7): hashes the decoded PCM block-by-block instead of reading the
    whole file into RAM — the concatenated float32 bytes are identical to the old
    whole-array hash, so existing cache ids stay valid. Bounded memory either way."""
    try:
        import numpy as np
        import soundfile as sf
        h = hashlib.sha256()
        with sf.SoundFile(str(audio_path)) as f:
            sr = f.samplerate
            for block in f.blocks(blocksize=1 << 18, dtype="float32", always_2d=False):
                h.update(np.ascontiguousarray(block).tobytes())
        h.update(str(sr).encode())
        return "cid_" + h.hexdigest()[:32]
    except Exception:
        h = hashlib.sha256()
        with open(audio_path, "rb") as fp:
            for chunk in iter(lambda: fp.read(1 << 20), b""):
                h.update(chunk)
        return "raw_" + h.hexdigest()[:32]


def params_hash(params: dict | None) -> str:
    if not params:
        return ""
    blob = json.dumps(params, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(blob.encode()).hexdigest()[:16]


# ── get / put ─────────────────────────────────────────────────────────────────
def get(content_id: str, kind: str, params: dict | None = None) -> dict | None:
    """Return the cached entry (and bump last_access/hits), or None on miss.
    For JSON kinds the result is under 'result'; binaries carry 'result_ref'."""
    ph = params_hash(params)
    with db.cursor() as c:
        row = c.execute(
            "SELECT id, result_json, result_ref, bytes FROM cache "
            "WHERE content_id=? AND kind=? AND params_hash=?",
            (content_id, kind, ph)).fetchone()
        if not row:
            return None
        c.execute("UPDATE cache SET last_access=?, hits=hits+1 WHERE id=?",
                  (time.time(), row["id"]))
    return {
        "content_id": content_id, "kind": kind,
        "result": json.loads(row["result_json"]) if row["result_json"] else None,
        "result_ref": row["result_ref"], "bytes": row["bytes"],
    }


def put(content_id: str, kind: str, *, result: dict | None = None,
        result_ref: str | None = None, bytes_: int = 0,
        params: dict | None = None) -> None:
    """Insert/replace a cache entry. Pass `result` for JSON, `result_ref` for a
    big artifact path."""
    ph = params_hash(params)
    now = time.time()
    rj = json.dumps(result) if result is not None else None
    with db.cursor() as c:
        c.execute(
            "INSERT INTO cache(content_id, kind, params_hash, result_json, "
            "result_ref, bytes, created, last_access, hits) "
            "VALUES(?,?,?,?,?,?,?,?,0) "
            "ON CONFLICT(content_id, kind, params_hash) DO UPDATE SET "
            "result_json=excluded.result_json, result_ref=excluded.result_ref, "
            "bytes=excluded.bytes, last_access=excluded.last_access",
            (content_id, kind, ph, rj, result_ref, bytes_, now, now))


# ── aliases (yt-id / url → content_id) ─────────────────────────────────────────
def resolve_alias(alias: str) -> str | None:
    with db.cursor() as c:
        row = c.execute("SELECT content_id FROM aliases WHERE alias=?",
                        (alias,)).fetchone()
        return row["content_id"] if row else None


def put_alias(alias: str, content_id: str) -> None:
    with db.cursor() as c:
        c.execute("INSERT INTO aliases(alias, content_id, created) VALUES(?,?,?) "
                  "ON CONFLICT(alias) DO UPDATE SET content_id=excluded.content_id",
                  (alias, content_id, time.time()))


# ── stats / eviction ──────────────────────────────────────────────────────────
def stats() -> list[dict]:
    with db.cursor() as c:
        rows = c.execute(
            "SELECT kind, COUNT(*) n, COALESCE(SUM(bytes),0) bytes, "
            "COALESCE(SUM(hits),0) hits FROM cache GROUP BY kind ORDER BY bytes DESC"
        ).fetchall()
        return [dict(r) for r in rows]


def gc(kind: str, max_bytes: int) -> dict:
    """Evict coldest (LRU) entries of `kind` until total bytes ≤ max_bytes.
    Returns {evicted, freed}. Also unlinks any result_ref files."""
    evicted, freed = 0, 0
    with db.cursor() as c:
        total = c.execute("SELECT COALESCE(SUM(bytes),0) b FROM cache WHERE kind=?",
                          (kind,)).fetchone()["b"]
        if total <= max_bytes:
            return {"evicted": 0, "freed": 0}
        rows = c.execute("SELECT id, bytes, result_ref FROM cache WHERE kind=? "
                         "ORDER BY last_access ASC", (kind,)).fetchall()
        for r in rows:
            if total - freed <= max_bytes:
                break
            if r["result_ref"]:
                p = Path(r["result_ref"])
                if p.exists():
                    p.unlink()
            c.execute("DELETE FROM cache WHERE id=?", (r["id"],))
            evicted += 1
            freed += r["bytes"]
    return {"evicted": evicted, "freed": freed}
