"""Auto-Learning Pattern Library.

User-Vorgabe 2026-06-09: jedes Label das der Semantic-Validator
findet (z.B. 'Telefonnr.' für kontakt_telefon) wird als auto-
generated Pattern persistiert. Beim nächsten Run prüft der Agent
zuerst die gelernten Patterns — der LLM-Call wird nur noch für
WIRKLICH neue Labels gebraucht.

Storage: JSON-Datei (default /tmp/breakpilot/agent_learned_patterns.json).
Format:
  {
    "version": "1",
    "updated_at": "2026-06-09T08:30:00Z",
    "patterns": [
      {
        "field_id": "kontakt_telefon",
        "label_used": "Telefonnr.",
        "regex_pattern": "\\bTelefonnr\\.?\\s*[:.\\s]?\\s*[\\+\\d]",
        "first_seen": "2026-06-09T08:30:00Z",
        "last_seen": "2026-06-09T08:30:00Z",
        "observed_count": 1,
        "confidence_sum": 0.9,
        "agent_id": "impressum"
      }
    ]
  }

Lifecycle:
  - record(): SVL-Treffer aufnehmen oder zähler erhöhen
  - load_patterns_for(field_id, agent_id): kompilierte Patterns liefern
  - prune_low_confidence(): nach 100 Runs Patterns mit <0.5 Avg-
    Confidence rauswerfen (Halluzinations-Schutz)

NOT thread-safe — eine Instanz pro Backend-Process. Reads gehen über
Cache mit mtime-Invalidierung.
"""

from __future__ import annotations

import json
import logging
import os
import re
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Pattern


logger = logging.getLogger(__name__)


def _library_path() -> Path:
    """Resolved at call time so tests can monkeypatch the env var."""
    return Path(os.environ.get(
        "AGENT_PATTERN_LIBRARY",
        "/tmp/breakpilot/agent_learned_patterns.json",
    ))


_lock = threading.Lock()
_cache: dict[str, list[dict]] = {}
_cache_mtime: float = 0.0


def _load_raw() -> dict:
    p = _library_path()
    if not p.exists():
        return {"version": "1", "patterns": []}
    try:
        return json.loads(p.read_text())
    except Exception as e:
        logger.warning("pattern library corrupt, reset: %s", e)
        return {"version": "1", "patterns": []}


def _save_raw(data: dict) -> None:
    p = _library_path()
    p.parent.mkdir(parents=True, exist_ok=True)
    data["updated_at"] = datetime.now(timezone.utc).isoformat()
    tmp = p.with_suffix(".json.tmp")
    tmp.write_text(json.dumps(data, indent=2, default=str))
    tmp.replace(p)


def _label_to_regex(label: str) -> str:
    """Generates a permissive regex from a label string.

    'Telefonnr.' → r"\\bTelefonnr\\.?\\s*[:.\\s(]?\\s*[\\+\\d]"
    'Funkanschluss' → r"\\bFunkanschluss\\s*[:.\\s(]?\\s*[\\+\\d]"
    'Geschäftsleitung' → r"\\bGeschäftsleitung\\s*[:.\\s(]"
    """
    base = re.escape(label.strip())
    # Strip escape of optional trailing period — we make it optional
    if base.endswith(r"\."):
        base = base[:-2] + r"\.?"
    # Heuristik: Telefon-Felder enden mit Nummer; sonstige mit Trennzeichen
    label_lc = label.lower()
    if any(k in label_lc for k in ("tel", "phone", "fon", "anschluss",
                                     "rufnummer", "rufnr")):
        return rf"\b{base}\s*[:.\s(]?\s*[\+\d]"
    if any(k in label_lc for k in ("email", "e-mail", "mail")):
        return rf"\b{base}\s*[:.\s(]?\s*[\w.+-]+@"
    return rf"\b{base}\s*[:.\s(]"


def _invalidate_cache() -> None:
    global _cache, _cache_mtime
    _cache = {}
    _cache_mtime = 0.0


def _refresh_cache() -> None:
    """Re-read library if file mtime changed."""
    global _cache, _cache_mtime
    p = _library_path()
    if not p.exists():
        _cache = {}
        _cache_mtime = 0.0
        return
    mtime = p.stat().st_mtime
    if mtime == _cache_mtime and _cache:
        return
    data = _load_raw()
    new_cache: dict[str, list[dict]] = {}
    for pat in data.get("patterns", []):
        key = f"{pat.get('agent_id', '')}/{pat.get('field_id', '')}"
        new_cache.setdefault(key, []).append(pat)
    _cache = new_cache
    _cache_mtime = mtime


def record(
    field_id: str,
    label_used: str,
    confidence: float,
    agent_id: str,
) -> None:
    """Persist a learned label. Idempotent — increments observed_count
    if (field_id, label_used, agent_id) already exists."""
    if not field_id or not label_used or not agent_id:
        return
    label_used = label_used.strip()[:60]
    if len(label_used) < 2:
        return
    with _lock:
        data = _load_raw()
        patterns = data.setdefault("patterns", [])
        now = datetime.now(timezone.utc).isoformat()
        match = None
        for p in patterns:
            if (p.get("field_id") == field_id and
                    p.get("label_used", "").strip().lower()
                    == label_used.lower() and
                    p.get("agent_id") == agent_id):
                match = p
                break
        if match:
            match["observed_count"] = int(match.get("observed_count", 0)) + 1
            match["confidence_sum"] = (
                float(match.get("confidence_sum", 0.0)) + float(confidence)
            )
            match["last_seen"] = now
        else:
            patterns.append({
                "field_id": field_id,
                "label_used": label_used,
                "regex_pattern": _label_to_regex(label_used),
                "first_seen": now,
                "last_seen": now,
                "observed_count": 1,
                "confidence_sum": float(confidence),
                "agent_id": agent_id,
            })
        _save_raw(data)
        _invalidate_cache()


def load_patterns_for(
    field_id: str,
    agent_id: str,
    min_observed: int = 1,
    min_avg_confidence: float = 0.5,
) -> list[Pattern[str]]:
    """Returns compiled regex patterns gelernt für (field_id, agent_id)."""
    _refresh_cache()
    key = f"{agent_id}/{field_id}"
    raws = _cache.get(key, [])
    out: list[Pattern[str]] = []
    for p in raws:
        obs = int(p.get("observed_count", 0))
        conf_sum = float(p.get("confidence_sum", 0.0))
        avg = conf_sum / obs if obs else 0.0
        if obs < min_observed or avg < min_avg_confidence:
            continue
        try:
            out.append(re.compile(p["regex_pattern"], re.IGNORECASE))
        except Exception:
            continue
    return out


def list_all() -> list[dict]:
    """Debug/Frontend: liefert alle gelernten Patterns."""
    _refresh_cache()
    flat: list[dict] = []
    for key, patterns in _cache.items():
        for p in patterns:
            obs = int(p.get("observed_count", 0))
            avg = (float(p.get("confidence_sum", 0.0)) / obs
                   if obs else 0.0)
            flat.append({**p, "avg_confidence": round(avg, 3)})
    return sorted(flat, key=lambda x: x.get("observed_count", 0),
                   reverse=True)


def prune_low_confidence(min_avg: float = 0.5,
                          min_runs_before_prune: int = 100) -> int:
    """Halluzinations-Schutz: löscht Patterns mit zu niedriger
    Avg-Confidence nach ausreichend Runs."""
    with _lock:
        data = _load_raw()
        before = len(data.get("patterns", []))
        kept = []
        for p in data.get("patterns", []):
            obs = int(p.get("observed_count", 0))
            avg = (float(p.get("confidence_sum", 0.0)) / obs
                   if obs else 0.0)
            if obs >= min_runs_before_prune and avg < min_avg:
                continue
            kept.append(p)
        data["patterns"] = kept
        _save_raw(data)
        _invalidate_cache()
        return before - len(kept)
