"""
Cross-site meta-playbook (browser memory tier 3).

Tier 2 (`browser_playbook`) learns per-SITE strategy. This tier learns the
SITE-AGNOSTIC patterns that transfer everywhere, e.g. "a composer clears on send,
so the cleared box IS the confirmation; do not hunt the thread for the text" or
"an opener like Message/DM just opens the box, only Send is irreversible". So the
VERY FIRST task on a brand-new site already benefits from what was learned on
every other site, the generalizable answer to "make it learn to learn".

Cheap by construction: the per-site distill (one aux call we already make) ALSO
returns a `universal` list; we just MERGE those here (dedup + cap), NO extra LLM
call. Same fail-safe as the per-site playbook: it is ADVISORY text seeded into the
prompt and re-verified by the agent, never auto-executed, so a wrong universal
bullet can only mildly mislead and is corrected as more sites confirm the truth.
"""

import json
import logging
import os
import tempfile
import time

from backend.apps.agents.browser.browser_playbook import clean_bullet

logger = logging.getLogger(__name__)

P_VERSION = 1
MAX_BULLETS = 10          # a touch larger than per-site: these earn their keep everywhere
P_FILE = "meta_playbook.json"

CACHE: list[str] | None = None


def p_dir() -> str | None:
    base = os.environ.get("OPENSWARM_BROWSER_META_DIR")
    if not base:
        try:
            from backend.config.paths import DATA_ROOT
            base = os.path.join(DATA_ROOT, "browser_meta")
        except Exception:
            return None
    try:
        os.makedirs(base, mode=0o700, exist_ok=True)
    except Exception:
        return None
    return base


def p_path() -> str | None:
    d = p_dir()
    return os.path.join(d, P_FILE) if d else None


def load() -> list[str]:
    path = p_path()
    if not path or not os.path.exists(path):
        return []
    try:
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
        if data.get("version") != P_VERSION:
            return []
        return [b for b in (data.get("bullets") or []) if isinstance(b, str)]
    except Exception:
        return []


def persist(bullets: list[str]) -> None:
    path = p_path()
    if not path:
        return
    try:
        d = os.path.dirname(path)
        fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            json.dump({"version": P_VERSION, "bullets": bullets, "updated_at": time.time()}, f)
        os.replace(tmp, path)  # atomic
    except Exception as e:
        logger.debug(f"[browser-meta] persist failed: {e}")


def get_meta() -> list[str]:
    """The cross-site bullets (cheap, no LLM). Cached after first read."""
    global CACHE
    if CACHE is None:
        CACHE = load() or list(P_SEED)
    return CACHE


def format_for_prompt() -> str:
    """The block injected into EVERY run's prompt, or '' if empty. Kept short and
    clearly framed as general priors so it never overrides what the live page shows."""
    bullets = get_meta()
    if not bullets:
        return ""
    lines = "\n".join(f"- {b}" for b in bullets[:MAX_BULLETS])
    return (
        "\n\n## General web priors (learned across many sites, verify against THIS page)\n"
        + lines
    )


def absorb(universal_bullets: list[str]) -> bool:
    """Merge site-agnostic lessons from a run into the meta-playbook. No LLM call.
    Only GENUINELY new bullets (not already present, case-insensitive) move the
    needle, so a re-confirmed lesson doesn't churn the list. New content goes first
    so it wins the cap over a stale prior. Returns True only if something changed."""
    if not universal_bullets:
        return False
    existing = get_meta()
    existing_lower = {b.lower() for b in existing}
    truly_new: list[str] = []
    seen: set[str] = set()
    for b in universal_bullets:
        cb = clean_bullet(b)
        if cb and cb.lower() not in existing_lower and cb.lower() not in seen:
            seen.add(cb.lower())
            truly_new.append(cb)
    if not truly_new:
        return False
    merged = (truly_new + existing)[:MAX_BULLETS]
    global CACHE
    CACHE = merged
    persist(merged)
    logger.info(f"[browser-meta] {len(merged)} cross-site prior(s) (was {len(existing)})")
    return True


def clear(wipe_disk: bool = False) -> None:
    """Test/maintenance reset of the in-memory cache (and optionally disk)."""
    global CACHE
    CACHE = None
    if wipe_disk:
        path = p_path()
        if path and os.path.exists(path):
            try:
                os.remove(path)
            except Exception:
                pass


# Shipped starting priors: the hard-won universal lessons from this codebase's own browser work, so tier 3 is useful on day one and accrues more as sites confirm them.
P_SEED = (
    "A message composer CLEARS when the send goes through; the empty box IS your "
    "confirmation, do not hunt the thread for the sent text to 'verify'.",
    "An opener (Message/DM/Compose) only OPENS the box and is reversible; only the "
    "actual Send/Submit/Post is irreversible, so opening it freely is safe.",
    "If the target's thread/composer is already open, commit to it; do not navigate "
    "away to a profile or re-search just to re-confirm the recipient.",
    "In rich-text composers, Enter usually inserts a newline; click the Send button "
    "rather than pressing Enter.",
    "The Send button often renders a beat AFTER the text commits; settle briefly and "
    "re-list once rather than concluding it vanished and hunting via CSS/JS.",
    "Construct deep search URLs directly (site.com/search?q=...) instead of driving "
    "the homepage search UI when you know the pattern.",
)
