#!/usr/bin/python3
# storage.py: tell the user exactly how much space the recordings use, and keep it under a cap they set.
#
# Two jobs, both for the 24/7 buffer the app owns:
#   1) usage  — size of the rolling buffer, the kept clips (Saved/), and the transcripts/sidecars,
#               plus free space on the disk, so the UI can show "X of your Y GB cap".
#   2) cap    — evict the OLDEST buffer clips (never Saved/, never the file being written, never the
#               transcripts) until the buffer is under the cap. Report-only by default; --enforce acts.
#
# This is the app pruning its OWN ephemeral buffer (the always-on feature), the same model as the
# time-based retention purge in main.swift, not Claude disposing of a user's files. Pure stdlib.
#
#   storage.py --rec-dir DIR [--cap-gb 5] [--enforce]
import sys, os, json, time, shutil

HOME = os.path.expanduser("~")
DEFAULT_REC = os.path.join(HOME, "Recordings/Epiphani")
FRESH_SEC = 90      # never touch a clip written in the last 90s (it may still be recording)
GB = 1024 ** 3


def _human(n):
    for unit in ("B", "KB", "MB", "GB", "TB"):
        if n < 1024 or unit == "TB":
            return ("%.0f %s" % (n, unit)) if unit in ("B", "KB") else ("%.1f %s" % (n, unit))
        n /= 1024.0


def _aac_in(d):
    """(total_bytes, [(path, size, mtime)]) for .aac directly in d (not subdirs). Missing dir -> empty."""
    out, total = [], 0
    try:
        for name in os.listdir(d):
            if not name.endswith(".aac"):
                continue
            p = os.path.join(d, name)
            try:
                st = os.stat(p)
            except OSError:
                continue
            out.append((p, st.st_size, st.st_mtime))
            total += st.st_size
    except OSError:
        pass
    return total, out


def _tree_size(d):
    total = 0
    for root, _dirs, files in os.walk(d):
        for f in files:
            try:
                total += os.path.getsize(os.path.join(root, f))
            except OSError:
                pass
    return total


def usage(rec_dir):
    buffer_bytes, buffer_files = _aac_in(rec_dir)
    saved_bytes = _tree_size(os.path.join(rec_dir, "Saved"))
    org_bytes = _tree_size(os.path.join(rec_dir, ".organized"))
    total = buffer_bytes + saved_bytes + org_bytes
    free = 0
    try:
        free = shutil.disk_usage(rec_dir if os.path.isdir(rec_dir) else HOME).free
    except OSError:
        pass
    return {
        "rec_dir": rec_dir,
        "buffer_bytes": buffer_bytes, "buffer_human": _human(buffer_bytes), "buffer_clips": len(buffer_files),
        "saved_bytes": saved_bytes, "saved_human": _human(saved_bytes),
        "transcripts_bytes": org_bytes, "transcripts_human": _human(org_bytes),
        "total_bytes": total, "total_human": _human(total),
        "disk_free_bytes": free, "disk_free_human": _human(free),
    }


def _protected(rec_dir):
    """Clip filenames the user asked to keep forever, at the CONVERSATION level (engine/meta.py's
    `kept` flag resolved to every clip in that conversation). Never a hard dependency: if meta.py
    cannot be imported or read, eviction proceeds exactly as it did before."""
    try:
        import meta
        return set(meta.kept_files(rec_dir, meta.load_meta(rec_dir)))
    except Exception:  # noqa: BLE001 — a protection lookup must never break the cap
        return set()


def plan_eviction(rec_dir, cap_bytes):
    """Oldest-first list of buffer clips to remove to get the BUFFER under cap_bytes. Saved/ untouched,
    and so is any clip in a conversation the user marked Keep."""
    buffer_bytes, files = _aac_in(rec_dir)
    if buffer_bytes <= cap_bytes:
        return [], buffer_bytes, buffer_bytes
    now = time.time()
    keep = _protected(rec_dir)
    # oldest first; never the file still being written, never a kept conversation's clips
    elig = sorted((f for f in files
                   if now - f[2] > FRESH_SEC and os.path.basename(f[0]) not in keep),
                  key=lambda f: f[2])
    plan, freed = [], 0
    for p, size, mtime in elig:
        if buffer_bytes - freed <= cap_bytes:
            break
        plan.append({"path": p, "bytes": size, "human": _human(size),
                     "age_hours": round((now - mtime) / 3600, 1)})
        freed += size
    return plan, buffer_bytes, buffer_bytes - freed


def enforce(rec_dir, cap_bytes):
    plan, before, after = plan_eviction(rec_dir, cap_bytes)
    removed, errors = [], []
    for item in plan:
        try:
            os.remove(item["path"])
            removed.append(os.path.basename(item["path"]))
        except OSError as e:
            errors.append({"file": os.path.basename(item["path"]), "error": str(e)[:160]})
    return {"ok": True, "removed": removed, "removed_count": len(removed),
            "freed_bytes": sum(i["bytes"] for i in plan if os.path.basename(i["path"]) in removed),
            "before_human": _human(before), "errors": errors}


def main(argv):
    rec_dir = os.path.expanduser(argv[argv.index("--rec-dir") + 1]) if "--rec-dir" in argv else DEFAULT_REC
    rep = {"usage": usage(rec_dir)}
    if "--cap-gb" in argv:
        try:
            cap_bytes = int(float(argv[argv.index("--cap-gb") + 1]) * GB)
        except (ValueError, IndexError):
            cap_bytes = 0
        rep["cap_gb"] = cap_bytes / GB
        if "--enforce" in argv:
            rep["enforced"] = enforce(rec_dir, cap_bytes)
        else:
            plan, before, after = plan_eviction(rec_dir, cap_bytes)
            rep["plan"] = {"would_remove": len(plan), "to_human": _human(after),
                           "from_human": _human(before), "files": plan[:50]}
    sys.stdout.write(json.dumps(rep, ensure_ascii=False))


if __name__ == "__main__":
    main(sys.argv[1:])
