"""Marginalize low-salience chat atoms — v0.22 D22-M3."""

from __future__ import annotations

import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from niannian.memory.meta import parse_atom_meta

MARGINAL_WEIGHT = 0.1
PRUNE_DAYS = 90


def _parse_ts(value: str | None) -> float:
    if not value:
        return 0.0
    try:
        return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
    except ValueError:
        return 0.0


def atom_salience(atom: dict[str, Any], *, now: float | None = None) -> float:
    meta = atom.get("meta") if isinstance(atom.get("meta"), dict) else {}
    parsed = parse_atom_meta(meta)
    if parsed.subject_eligible:
        return 1e9
    if meta.get("belief_active"):
        return 1e9
    if meta.get("vault_pointer"):
        return 1e9
    if meta.get("marginalized_at"):
        return 0.05

    source = str(meta.get("source") or atom.get("source") or "chat")
    source_weight = {
        "chat": 0.4,
        "manual_remember": 1.2,
        "remember": 1.2,
        "vault": 1.0,
    }.get(source, 0.6)

    ts = _parse_ts(str(atom.get("created_at") or meta.get("created_at") or ""))
    now_ts = now if now is not None else datetime.now(timezone.utc).timestamp()
    age_days = max(0.0, (now_ts - ts) / 86400.0) if ts else 30.0
    recency = max(0.1, 1.0 - min(age_days / 365.0, 0.9))
    access = float(meta.get("access_count") or 1)
    access_factor = min(1.5, 0.8 + access * 0.05)
    return recency * source_weight * access_factor


def _load_atoms(home: Path) -> list[dict[str, Any]]:
    path = home / "left" / "atoms.jsonl"
    if not path.is_file():
        return []
    rows: list[dict[str, Any]] = []
    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            row = json.loads(line)
        except json.JSONDecodeError:
            continue
        if isinstance(row, dict):
            rows.append(row)
    return rows


def _save_atoms(home: Path, rows: list[dict[str, Any]]) -> None:
    path = home / "left" / "atoms.jsonl"
    path.parent.mkdir(parents=True, exist_ok=True)
    text = "".join(json.dumps(r, ensure_ascii=False) + "\n" for r in rows)
    path.write_text(text, encoding="utf-8")


def preview_marginalize(home: Path, *, percent: float) -> dict[str, Any]:
    pct = max(5.0, min(30.0, float(percent)))
    atoms = _load_atoms(home)
    scored: list[tuple[float, dict[str, Any]]] = []
    for row in atoms:
        meta = row.get("meta") if isinstance(row.get("meta"), dict) else {}
        if meta.get("marginalized_at"):
            continue
        sal = atom_salience(row)
        if sal >= 1e8:
            continue
        scored.append((sal, row))
    scored.sort(key=lambda x: x[0])
    take = max(1, int(len(scored) * pct / 100.0)) if scored else 0
    sample = [
        {
            "id": r.get("id"),
            "salience": round(s, 4),
            "preview": str(r.get("raw_text") or "")[:80],
        }
        for s, r in scored[:take]
    ]
    return {"percent": pct, "candidate_count": take, "sample": sample[:10]}


def apply_marginalize(home: Path, *, percent: float) -> dict[str, Any]:
    pct = max(5.0, min(30.0, float(percent)))
    atoms = _load_atoms(home)
    scored: list[tuple[float, dict[str, Any]]] = []
    for row in atoms:
        meta = row.get("meta") if isinstance(row.get("meta"), dict) else {}
        if meta.get("marginalized_at"):
            continue
        sal = atom_salience(row)
        if sal >= 1e8:
            continue
        scored.append((sal, row))
    scored.sort(key=lambda x: x[0])
    take = max(1, int(len(scored) * pct / 100.0)) if scored else 0
    ids = {r.get("id") for _, r in scored[:take] if r.get("id")}

    now = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
    updated = 0
    for row in atoms:
        if row.get("id") not in ids:
            continue
        meta = dict(row.get("meta") or {})
        meta["marginalized_at"] = now
        meta["retrieval_weight"] = MARGINAL_WEIGHT
        row["meta"] = meta
        updated += 1
    if updated:
        _save_atoms(home, atoms)
    return {"marginalized": updated, "percent": pct}


def prune_marginalized_atoms(home: Path, *, days: int = PRUNE_DAYS) -> dict[str, Any]:
    """Dream hook — remove marginalized atoms older than N days."""
    cutoff = datetime.now(timezone.utc).timestamp() - days * 86400
    atoms = _load_atoms(home)
    kept: list[dict[str, Any]] = []
    pruned = 0
    for row in atoms:
        meta = row.get("meta") if isinstance(row.get("meta"), dict) else {}
        at = meta.get("marginalized_at")
        weight = float(meta.get("retrieval_weight") or 1.0)
        if at and weight <= MARGINAL_WEIGHT and _parse_ts(str(at)) < cutoff:
            pruned += 1
            continue
        kept.append(row)
    if pruned:
        _save_atoms(home, kept)
    return {"pruned": pruned, "remaining": len(kept)}
