"""
Per-thread tool working set (tool-schema subsetting, Point 5).

The agent carries a slim always-on core of tool schemas (``tools.CORE_TOOLS``)
instead of the full tier-0 set. Every other tool joins this thread's *working
set* when ``find_tools`` surfaces it, the model invokes it by name, or a
surface stamps it. The working set is LRU-bounded and persisted next to the
working buffer so a durable task resumed tomorrow wakes up with its kit
already loaded — awareness stays constant, carriage is earned.

All functions are best-effort and never raise: losing the ledger degrades to
"rediscover via find_tools", never to a broken run.
"""

from __future__ import annotations

import json
import logging
import os
import re
import threading

_log = logging.getLogger(__name__)

_LOCK = threading.Lock()
DEFAULT_WORKING_SET_CAP = 15
_LEDGER_VERSION = 1
_FILENAME_UNSAFE = re.compile(r"[^A-Za-z0-9._-]+")


def _ledger_path(thread_id: str | None) -> str | None:
    try:
        from core import get_workspace_path

        workspace = (get_workspace_path() or "").strip()
        if not workspace:
            return None
        safe = _FILENAME_UNSAFE.sub("_", str(thread_id or "").strip()) or "default"
        return os.path.join(
            os.path.expanduser(workspace), "memory", f"tool-working-set-{safe}.json"
        )
    except Exception:
        _log.debug("working set ledger path resolution failed", exc_info=True)
        return None


def load_working_set(thread_id: str | None, cap: int = DEFAULT_WORKING_SET_CAP) -> list[str]:
    """Return this thread's working-set tool names, oldest first (LRU order)."""
    path = _ledger_path(thread_id)
    if not path or not os.path.isfile(path):
        return []
    try:
        with _LOCK:
            with open(path, encoding="utf-8") as f:
                data = json.load(f)
        names = data.get("tools") if isinstance(data, dict) else None
        if not isinstance(names, list):
            return []
        out: list[str] = []
        seen: set[str] = set()
        for n in names:
            s = str(n or "").strip()
            if s and s not in seen:
                out.append(s)
                seen.add(s)
        return out[-max(1, int(cap)):]
    except Exception:
        _log.debug("working set ledger read failed (%s)", path, exc_info=True)
        return []


def record_tools(
    thread_id: str | None,
    names: list[str] | tuple[str, ...] | set[str],
    cap: int = DEFAULT_WORKING_SET_CAP,
) -> list[str]:
    """Move ``names`` to the fresh end of the thread's working set and persist.

    Returns the updated working set (oldest first). LRU semantics: re-recording
    an existing name bumps it away from eviction; overflow evicts the oldest.
    """
    cleaned = [s for s in (str(n or "").strip() for n in (names or [])) if s]
    if not cleaned:
        return load_working_set(thread_id, cap=cap)
    path = _ledger_path(thread_id)
    current = load_working_set(thread_id, cap=cap)
    updated = [n for n in current if n not in cleaned]
    for n in cleaned:
        if n not in updated:
            updated.append(n)
    updated = updated[-max(1, int(cap)):]
    if path:
        try:
            with _LOCK:
                os.makedirs(os.path.dirname(path), exist_ok=True)
                tmp = path + ".tmp"
                with open(tmp, "w", encoding="utf-8") as f:
                    json.dump({"version": _LEDGER_VERSION, "tools": updated}, f, indent=1)
                os.replace(tmp, path)
        except Exception:
            _log.debug("working set ledger write failed (%s)", path, exc_info=True)
    return updated
