"""v3-Engine: läuft die 4-Layer-Pipeline auf einem Impressum-Text.

  Layer 0  — Regex-Boost (die 12 deterministischen Agent-Patterns)
  Layer 1  — MC-Laden + Keyword-Match. Das LADEN delegiert an die
              Main-Tool-Engine (rag_document_checker._load_controls):
              eine Quelle der Wahrheit inkl. P72-Scope, check_type='text'
              und fits_doc_type/scope_requires aus dem Sidecar. KEINE
              parallele Roh-Query mehr.
  Layer 2  — BGE-M3 Embedding-Match (mc_embedding_matcher, shared)
  Layer 3  — Semantic-Validator (LLM) im Agent (hier nur durchgereicht)

Zusätzlich am Agent-Rand: subtraktives Sektor-/Themen-Gate
(_filter_controls) — das Sektor-Gate (Branchen-Prefix) macht die Engine
nicht, es lebt sonst im Mail-V2-Report.

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 (
    boost_matches_db_mc,
    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
# (keine divergente Zweit-Logik). Import defensiv: faellt der Mail-Pfad
# weg, bleibt der Agent lauffaehig (ohne Sektor-Gate).
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 = "",
) -> 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)
    """
    if not text or len(text) < 100:
        return [], {"reason": "text too short"}

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

    # Layer 1: MC-Laden DELEGIERT an die Main-Tool-Engine. Damit erbt der
    # Agent automatisch deren Scope-Schutz (P72 canonical-scope,
    # check_type='text', fits_doc_type/scope_requires) — genau die Filter,
    # an denen die alte parallele Roh-Query vorbeilief.
    try:
        from compliance.services.rag_document_checker import _load_controls
        controls = await _load_controls(
            "impressum", db_url, 0, business_scope,
        )
    except Exception as e:
        logger.warning("v3 load via main-tool engine failed: %s", e)
        controls = []
    _normalize_criteria(controls)
    # Agent-Rand-Backstop (DB-unabhaengig): Sektor-Gate (Branchen-Prefix,
    # macht die Engine nicht) + Themen-Gate (falls der Sidecar leer ist).
    controls, drop_stats = _filter_controls(controls, business_scope)
    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:
                    # pass_criteria im Result behalten für Boost-Layer
                    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 2: Embedding-Match für die failed MCs
    failed_for_embed = [c for c, r in zip(controls, results)
                        if r and not r.get("passed")]
    if failed_for_embed:
        try:
            from compliance.services.mc_embedding_matcher import (
                ensure_mc_embeddings, embedding_match,
            )
            await ensure_mc_embeddings()
            semantic_passes = await embedding_match(
                text, failed_for_embed, doc_type="impressum",
            )
            if semantic_passes:
                for r in results:
                    cid = r.get("control_id")
                    if cid in semantic_passes and not r.get("passed"):
                        r["passed"] = True
                        r["matched_text"] = "[layer-2 embedding match]"
                        r["source"] = (r.get("source") or "") + "+embedding"
        except Exception as e:
            logger.warning("layer-2 embedding skipped: %s", e)

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

    # Layer 0 Override: failed MCs deren pass/fail_criteria zu einem meiner
    # gebooster field_ids passen → überschreiben zu PASS. Wir haben
    # pass_criteria + fail_criteria in r drin (Layer-1 hat sie behalten).
    boost_overrides = 0
    for r in results:
        if r.get("passed"):
            continue
        pass_crit = r.get("_pass_criteria") or []
        fail_crit = r.get("_fail_criteria") or []
        if not pass_crit and not fail_crit:
            pass_crit = [r.get("hint") or r.get("label") or ""]
        matched_field = boost_matches_db_mc(boosts, pass_crit, fail_crit)
        if matched_field:
            r["passed"] = True
            r["matched_text"] = (
                f"[regex-boost layer 0 — field {matched_field}]"
            )
            existing_hint = r.get("hint") or ""
            r["hint"] = (existing_hint +
                          " (passed via regex-boost layer 0)").strip()
            r["source"] = (r.get("source") or "") + "+regex_boost"
            boost_overrides += 1

    telemetry = {
        "layer_0_field_hits": len(boost_field_ids),
        "layer_0_field_ids": boost_field_ids,
        "layer_1_pass": layer_1_pass,
        "layer_1_fail": layer_1_fail,
        "layer_0_boost_overrides": boost_overrides,
        "total_mcs": len(results),
        "sector_dropped": drop_stats.get("sector_dropped", 0),
        "offtopic_dropped": drop_stats.get("offtopic_dropped", 0),
    }
    logger.info("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, werden verworfen. Fuer einen OEM/Zulieferer/Maschinenbauer
         (kein Behoerden-/Finanz-/Medizin-Scope) fallen GOV/FIN/MED-MCs so
         heraus — derselbe Mechanismus wie im Mail-V2-Report.
      2. Themen-Gate — MCs ohne Impressum-Themenueberlapp werden verworfen
         (faengt fremd-getaggte MCs wie 'Bestellbestaetigung').

    Rein subtraktiv: entfernt nur falsch-positive Kandidaten, erzeugt nie
    neue Findings.
    """
    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 ""
        required = SECTOR_PREFIXES.get(prefix)
        if required and not (scope_lc & required):
            sector_dropped += 1
            continue
        if not criteria_on_topic(c.get("pass_criteria"),
                                 c.get("fail_criteria")):
            offtopic_dropped += 1
            continue
        kept.append(c)
    if sector_dropped or offtopic_dropped:
        logger.info(
            "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 (nicht Zeichen-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] = []
