"""Problem log — what went wrong, in the app, in plain language.

Background workers fail silently by design (a broken scanner must not stop
a pass), which is right for robustness and terrible for the user: the app
looks idle when it is actually stuck. Every one of those swallowed
exceptions now also lands here, capped and persisted, so Settings can show
"3 problems" instead of nothing and support means "paste your diagnostics"
rather than "find the console window you closed".
"""
import json
import threading
import time
import traceback

from clipshelf.config import DATA

LOG = DATA / "problems.json"
MAX = 60
_lock = threading.Lock()
_items: list = []
_mtime = -1.0


def _load():
    """The FILE is the source of truth, re-read whenever it changes — a
    cached-forever list would go stale against any other writer (a CLI
    task, a helper process) and silently drop their entries."""
    global _items, _mtime
    try:
        m = LOG.stat().st_mtime
    except OSError:
        if _mtime != -1.0:
            _items, _mtime = [], -1.0
        return
    if m == _mtime:
        return
    try:
        _items = json.loads(LOG.read_text(encoding="utf-8"))[:MAX]
        _mtime = m
    except Exception:
        _items = []


def record(source: str, message, hint: str = ""):
    """Log a problem, and — only if the user opted in — queue a crash report
    for it. Hooked here because this is already the one place every
    swallowed worker exception passes through."""
    try:
        from clipshelf import crashreport
        crashreport.record(source, message, hint)
    except Exception:
        pass                    # reporting must never break logging
    return _record(source, message, hint)


def _record(source: str, message, hint: str = ""):
    """Note a failure. `message` may be an exception."""
    if isinstance(message, BaseException):
        detail = "".join(traceback.format_exception_only(type(message), message)).strip()
    else:
        detail = str(message)
    with _lock:
        _load()
        now = int(time.time())
        # collapse a repeating failure into one row with a count — a worker
        # retrying every 60s must not bury everything else
        for it in _items:
            if it["source"] == source and it["detail"] == detail[:400]:
                it["count"] += 1
                it["last"] = now
                break
        else:
            _items.insert(0, {"source": str(source)[:40], "detail": detail[:400],
                              "hint": str(hint)[:200], "first": now, "last": now,
                              "count": 1})
            del _items[MAX:]
        try:
            global _mtime
            tmp = LOG.with_suffix(".json.tmp")
            tmp.write_text(json.dumps(_items), encoding="utf-8")
            tmp.replace(LOG)
            _mtime = LOG.stat().st_mtime
        except Exception:
            pass
    print(f"[{time.strftime('%X')}] [problem] {source}: {detail[:200]}", flush=True)


def items() -> list:
    with _lock:
        _load()
        return list(_items)


def clear():
    global _items, _mtime
    with _lock:
        _items = []
        _mtime = -1.0
        try:
            LOG.unlink(missing_ok=True)
        except Exception:
            pass
