"""v3-Engine: läuft die 4-Layer-Pipeline auf einem DSE-Text (Art. 13/14 DSGVO).

  Layer 0  — Regex-Boost (die kuratierten Art-13-Patterns aus mcs.py)
  Layer 1  — MC-Laden + Keyword-Match. Das LADEN delegiert an die Main-Tool-
              Engine (rag_document_checker._load_controls, doc_type='dse'):
              eine Quelle der Wahrheit inkl. P72-Scope, check_type='text'
              (267 von 571) und fits_doc_type/scope_requires aus dem Sidecar.
  Layer 2  — BGE-M3 Embedding-Match (mc_embedding_matcher, shared)
  Layer 0 Override — failed MCs, deren criteria zu einem gebooster field_id
              passen, werden zu PASS überschrieben.

Zusätzlich am Agent-Rand: subtraktives Sektor-/Themen-Gate (_filter_controls)
— das Sektor-Gate (Branchen-Prefix GOV/FIN/MED…) verwirft branchenfremde MCs,
das Themen-Gate fremd-getaggte. Analog impressum/v3_engine.py.

Output: Liste Result-Dicts kompatibel mit rag_document_checker. Der Agent
konvertiert sie zu Finding-Objekten.
"""

from __future__ import annotations

import logging
from typing import Any

from .regex_boost import (
    compute_regex_boosts,
    criteria_on_topic,
)

logger = logging.getLogger(__name__)

# Branchen-Prefix -> erwarteter Scope-Token. Reuse aus dem Mail-V2-Scope-
# Filter, damit Agent-Pfad und Report-Pfad dieselbe Quelle nutzen. Import
# defensiv: faellt der Mail-Pfad weg, bleibt der Agent lauffaehig.
try:
    from compliance.services.mail_render_v2._scope_filter import (
        SECTOR_PREFIXES,
    )
except Exception:  # pragma: no cover - defensiver Fallback
    SECTOR_PREFIXES = {}


async def run_v3_pipeline(
    text: str,
    business_scope: set[str],
    db_url: str = "",
    skip_embedding: bool = False,
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    """Returns (results, telemetry).

    results: pro DB-MC ein dict {control_id, passed, severity, ...}
    telemetry: counters für Frontend-Anzeige (Layer-Aufschlüsselung)

    skip_embedding: Layer-2 (BGE-M3 Recall) überspringen. Nur für Unit-Tests
    ohne Embedding-Service. Im Betrieb läuft die Recall-Schicht: sie ist
    gecacht (pro Doc-Hash) und Reachability-gegated, blockiert also nie.
    """
    if not text or len(text) < 100:
        return [], {"reason": "text too short"}

    # Layer 0: kuratierte Art-13-Patterns
    boosts = compute_regex_boosts(text, business_scope)
    boost_field_ids = sorted(boosts)
    logger.info("dse v3 Layer-0 boosts: %d hits — %s",
                len(boost_field_ids), boost_field_ids)

    # Layer 1: MC-Laden DELEGIERT an die Main-Tool-Engine (Scope-Schutz inkl.).
    try:
        from compliance.services.rag_document_checker import _load_controls
        controls = await _load_controls(
            "dse", db_url, 0, business_scope,
        )
    except Exception as e:
        logger.warning("dse v3 load via main-tool engine failed: %s", e)
        controls = []
    _normalize_criteria(controls)
    # Agent-Rand-Backstop: Sektor-Gate (Branchen-Prefix) + Themen-Gate.
    controls, drop_stats = _filter_controls(controls, business_scope)
    # Applicability-Gate: hochsichere organisatorische Controls (laut
    # control_classification NICHT DSE, needs_review=false) aus dem
    # FINDINGS-Scan nehmen -> organisatorische Checkliste statt False-FEHLT.
    # Fail-safe: needs_review bleiben drin. Defensiv: fehlt die Tabelle, kein
    # Filter (Prod-sicher). Siehe _classification_gate.
    from ._classification_gate import apply_gate, load_dse_gate
    gate = await load_dse_gate(db_url)
    organizational: list[dict[str, Any]] = []
    if gate:
        controls, organizational = apply_gate(controls, gate)
    results: list[dict[str, Any]] = []
    if controls:
        try:
            from compliance.services.rag_document_checker import (
                _check_mc_deterministic,
            )
            text_lower = text.lower().replace("\xad", "")
            for mc in controls:
                r = _check_mc_deterministic(text_lower, mc)
                if r:
                    r["_pass_criteria"] = mc.get("pass_criteria")
                    r["_fail_criteria"] = mc.get("fail_criteria")
                    results.append(r)
        except Exception as e:
            logger.warning("layer-1 keyword check failed: %s", e)
            results = []

    layer_1_pass = sum(1 for r in results if r.get("passed"))

    # Layer 2: DETERMINISTISCHE semantische Recall-Schicht (BGE-M3, gecacht).
    # Ersetzt den früheren Regex-Boost, der auf vollständigen DSE-Dokumenten
    # massiv über-passte (BMW: 71/94 Boost-Overrides → 49% Übereinstimmung).
    # Embedding ist genauer (BMW-GT: KW|EMB@0.65 = 75%) UND deterministisch
    # (feste Funktion, reproduzierbar — kein Keyword-Katalog, kein LLM).
    embedding_passes = 0
    if not skip_embedding:
        failed_cids = [r.get("control_id") for r in results
                       if r and not r.get("passed") and r.get("control_id")]
        if failed_cids:
            try:
                from ._embedding_recall import embedding_recall
                semantic = await embedding_recall(text, failed_cids)
            except Exception as e:
                logger.warning("dse embedding recall failed: %s", e)
                semantic = set()
            for r in results:
                if (r.get("control_id") in semantic
                        and not r.get("passed")):
                    r["passed"] = True
                    r["matched_text"] = "[semantisch erkannt — Embedding]"
                    r["source"] = (r.get("source") or "") + "+embedding"
                    embedding_passes += 1

    # Layer 3: getierte 3-Status-Auswertung (nur Controls mit tiered_criteria).
    # Reproduzierbar: EMBEDDING-Präsenz (deterministisch) + GECACHTER Haiku-Judge
    # nur für Sufficiency. UNBESTIMMT → Legacy-Pass bleibt. Gated + fail-safe.
    tiered_evaluated = 0
    try:
        from compliance.services.checkers.base import DocContext
        from ._tiered_eval import (
            evaluate_tiered, fetch_tiered_criteria, prepare_doc,
        )
        result_cids = [r.get("control_id") for r in results if r.get("control_id")]
        tiered_map = await fetch_tiered_criteria(result_cids, db_url)
        if tiered_map:
            ctx = await prepare_doc(text)
            doc_ctx = DocContext(text=text)
            for r in results:
                tc = tiered_map.get(r.get("control_id"))
                if not tc:
                    continue
                ev = await evaluate_tiered(r["control_id"], tc, ctx, doc_ctx)
                if ev["status"] == "UNBESTIMMT":
                    continue
                r["compliance_status"] = ev["status"]
                r["recommendations"] = ev["recommendations"]
                r["tier_lm"] = f"{ev['lm_met']}/{ev['lm_total']}"
                r["passed"] = ev["status"] == "ERFÜLLT"
                tiered_evaluated += 1
    except Exception as e:
        logger.warning("dse tiered eval skipped: %s", e)

    # Layer 4 (SHADOW): Obligation-Aggregation NUR in die Telemetrie. Greift NICHT
    # in `results` ein — nutzer-sichtbare Findings bleiben unverändert. Liefert die
    # Vergleichszahlen für den späteren Umschalt-Entscheid (collapse_factor etc.).
    obligation_shadow: dict[str, Any] = {}
    try:
        from ._obligation_shadow import build_obligation_shadow
        obligation_shadow = await build_obligation_shadow(results, text, db_url)
    except Exception as e:
        logger.warning("dse obligation shadow skipped: %s", e)
        obligation_shadow = {"error": str(e)}

    telemetry = {
        "layer_0_field_hits": len(boost_field_ids),
        "layer_0_field_ids": boost_field_ids,
        "layer_1_pass": layer_1_pass,
        "embedding_passes": embedding_passes,
        "tiered_evaluated": tiered_evaluated,
        "total_mcs": len(results),
        "sector_dropped": drop_stats.get("sector_dropped", 0),
        "offtopic_dropped": drop_stats.get("offtopic_dropped", 0),
        "gate_excluded": len(organizational),
        "organizational_checklist": organizational,
        "obligation_shadow": obligation_shadow,
    }
    logger.info("dse v3 telemetry: %s", telemetry)
    return results, telemetry


def _filter_controls(
    controls: list[dict[str, Any]],
    business_scope: set[str],
) -> tuple[list[dict[str, Any]], dict[str, int]]:
    """Subtraktiver Scope-Filter VOR der Bewertung.

      1. Sektor-Gate — MCs deren control_id-Prefix eine Branche bezeichnet
         (FIN/GOV/MED/INS/EDU/LEG/REL/POL), die NICHT im business_scope liegt
         UND die nicht on-topic ist, werden verworfen.
      2. Themen-Gate — MCs ohne DSE-Themenüberlapp werden verworfen.

    Rein subtraktiv: entfernt nur falsch-positive Kandidaten.
    """
    scope_lc = {s.lower() for s in (business_scope or set())}
    kept: list[dict[str, Any]] = []
    sector_dropped = 0
    offtopic_dropped = 0
    for c in controls:
        cid = c.get("control_id") or ""
        prefix = cid.split("-")[0].upper() if "-" in cid else ""
        on_topic = criteria_on_topic(c.get("pass_criteria"),
                                     c.get("fail_criteria"))
        required = SECTOR_PREFIXES.get(prefix)
        # Sektor-Gate nur fuer NICHT-on-topic Controls: ein klar DSE-
        # thematischer Control (z.B. GOV-Prefix aus der Domain-Erkennung)
        # darf nicht am Branchen-Prefix scheitern.
        if required and not (scope_lc & required) and not on_topic:
            sector_dropped += 1
            continue
        if not on_topic:
            offtopic_dropped += 1
            continue
        kept.append(c)
    if sector_dropped or offtopic_dropped:
        logger.info(
            "dse v3 scope-filter: -%d Branchen-MCs, -%d themenfremde MCs "
            "(scope=%s)", sector_dropped, offtopic_dropped,
            sorted(scope_lc) or "leer",
        )
    return kept, {
        "sector_dropped": sector_dropped,
        "offtopic_dropped": offtopic_dropped,
    }


def _normalize_criteria(controls: list[dict[str, Any]]) -> None:
    """asyncpg liefert JSONB-Spalten (pass_criteria/fail_criteria) als
    Roh-String. In echte Listen parsen, damit Sektor-/Themen-Gate und der
    Boost-Layer Element-weise iterieren."""
    import json
    for c in controls:
        for key in ("pass_criteria", "fail_criteria"):
            v = c.get(key)
            if isinstance(v, list):
                continue
            if isinstance(v, str):
                try:
                    parsed = json.loads(v)
                    c[key] = parsed if isinstance(parsed, list) else [v]
                except Exception:
                    c[key] = [v] if v else []
            else:
                c[key] = []
