"""
Pure aggregation module — maps existing analysis results to three compliance frameworks.
No new ML; all inputs are pre-computed results from the other data-backend modules.
"""

from fastapi import APIRouter
from pydantic import BaseModel
from typing import Optional, Any
import uuid
from datetime import datetime, timezone

router = APIRouter()


# ── Input: all existing analysis results passed in as raw dicts ───────────────

class ComplianceReportRequest(BaseModel):
    tq:   Optional[dict[str, Any]] = None   # TextQualityResult
    pii:  Optional[dict[str, Any]] = None   # PIIResult
    tox:  Optional[dict[str, Any]] = None   # ToxicityResult
    cr:   Optional[dict[str, Any]] = None   # CopyrightResult
    syn:  Optional[dict[str, Any]] = None   # SyntheticResult
    bias: Optional[dict[str, Any]] = None   # BiasSurfaceResult
    oo:   Optional[dict[str, Any]] = None   # OptOutResult
    dataset_name: Optional[str] = None
    hf_id: Optional[str] = None


# ── Helpers ───────────────────────────────────────────────────────────────────

RISK_SCORE = {"critical": 0.0, "high": 0.25, "medium": 0.55, "low": 0.80, "none": 1.0}
RISK_LEVEL = {"critical": "fail", "high": "fail", "medium": "warn", "low": "pass", "none": "pass"}
BIAS_SCORE = {"high": 0.0, "elevated": 0.25, "moderate": 0.55, "low": 1.0}
OO_SCORE   = {"opted_out": 0.0, "restricted": 0.5, "clear": 1.0}


def _pct_to_score(pct: float) -> float:
    """Convert a flagged-percentage to a 0–1 health score."""
    return max(0.0, 1.0 - pct / 100)


def _status(score: float) -> str:
    if score >= 0.75: return "pass"
    if score >= 0.45: return "warn"
    return "fail"


def _badge(score: float) -> str:
    if score >= 0.75: return "compliant"
    if score >= 0.45: return "partial"
    return "non-compliant"


# ── EU AI Act ─────────────────────────────────────────────────────────────────

def build_eu_ai_act(tq, pii, tox, cr, syn, bias, oo):
    articles = []

    # Article 10 — Data and data governance
    art10_items = []
    art10_scores = []

    # 10(2)(a) relevant, representative, free of errors
    if tq and not tq.get("error"):
        dedup = tq.get("dedup") or {}
        near_dup_rate = dedup.get("near_duplicate_rate", 0)
        s = _pct_to_score(near_dup_rate)
        art10_items.append({
            "clause": "Art. 10(2)(a) — Representative & free of errors",
            "signal": "Text Quality: near-duplicate rate",
            "value": f"{near_dup_rate:.1f}% near-duplicates",
            "score": round(s, 2),
            "status": _status(s),
        })
        art10_scores.append(s)

        lang = tq.get("language") or {}
        if lang:
            multilingual = lang.get("multilingual", False)
            s2 = 0.6 if multilingual else 1.0
            art10_items.append({
                "clause": "Art. 10(2)(b) — Relevant for intended purpose",
                "signal": "Text Quality: language consistency",
                "value": f"Multilingual: {multilingual}, dominant: {lang.get('dominant_lang') or 'n/a'}",
                "score": round(s2, 2),
                "status": _status(s2),
            })
            art10_scores.append(s2)

    # 10(2)(f) examination for biases
    if bias and not bias.get("error"):
        bs = BIAS_SCORE.get(bias.get("overall_bias_risk", "low"), 0.5)
        protected = bias.get("protected_columns_found", 0)
        art10_items.append({
            "clause": "Art. 10(2)(f) — Examination for possible biases",
            "signal": "Bias Surface: protected attribute imbalance",
            "value": f"{protected} protected attribute column(s), risk: {bias.get('overall_bias_risk')}",
            "score": round(bs, 2),
            "status": _status(bs),
        })
        art10_scores.append(bs)

    # 10(3) — special categories (PII / sensitive data)
    if pii and not pii.get("error"):
        ps = RISK_SCORE.get(pii.get("overall_risk", "none"), 0.8)
        art10_items.append({
            "clause": "Art. 10(3) — Special categories of personal data",
            "signal": "PII: sensitive entity presence",
            "value": f"{pii.get('pii_row_pct', 0):.1f}% rows with PII, risk: {pii.get('overall_risk')}",
            "score": round(ps, 2),
            "status": _status(ps),
        })
        art10_scores.append(ps)

    # 10(5) — jurisdiction / GDPR exposure from text quality
    if tq and tq.get("jurisdiction"):
        jur = tq["jurisdiction"]
        gdpr_pct = jur.get("gdpr_pct") or 0
        s = _pct_to_score(min(gdpr_pct, 100))
        art10_items.append({
            "clause": "Art. 10(5) — Geographic / jurisdictional scope",
            "signal": "Text Quality: GDPR-region URL exposure",
            "value": f"{gdpr_pct:.1f}% URLs from GDPR-covered regions",
            "score": round(s, 2),
            "status": _status(s),
        })
        art10_scores.append(s)

    art10_score = round(sum(art10_scores) / len(art10_scores), 2) if art10_scores else None
    articles.append({
        "id": "Art. 10",
        "title": "Data and Data Governance",
        "score": art10_score,
        "badge": _badge(art10_score) if art10_score is not None else "not-assessed",
        "items": art10_items,
    })

    # Article 11 — Technical documentation
    art11_items = []
    art11_scores = []

    # 11(1)(d) — data sets used, including provenance and license
    if cr and not cr.get("error"):
        lic_score = 1.0 - (cr.get("license", {}).get("score") or 0)
        art11_items.append({
            "clause": "Art. 11(1)(d) — Data provenance and licensing",
            "signal": "Copyright Risk: license clarity",
            "value": f"License: {cr.get('license', {}).get('raw') or cr.get('license', {}).get('label') or 'unknown'}, risk: {cr.get('overall_risk')}",
            "score": round(lic_score, 2),
            "status": _status(lic_score),
        })
        art11_scores.append(lic_score)

    if oo and not oo.get("error"):
        oo_s = OO_SCORE.get(oo.get("status", "clear"), 1.0)
        art11_items.append({
            "clause": "Art. 11(1)(d) — Data origin & opt-out registry status",
            "signal": "Opt-Out: Spawning AI / robots.txt registry",
            "value": f"Status: {oo.get('status')}, {oo.get('urls_checked', 0)} URLs checked",
            "score": round(oo_s, 2),
            "status": _status(oo_s),
        })
        art11_scores.append(oo_s)

    art11_score = round(sum(art11_scores) / len(art11_scores), 2) if art11_scores else None
    articles.append({
        "id": "Art. 11",
        "title": "Technical Documentation",
        "score": art11_score,
        "badge": _badge(art11_score) if art11_score is not None else "not-assessed",
        "items": art11_items,
    })

    # Article 12 — Record keeping & traceability
    art12_items = []
    art12_scores = []

    if syn and not syn.get("error"):
        flagged_pct = syn.get("flagged_pct", 0)
        s = _pct_to_score(flagged_pct)
        art12_items.append({
            "clause": "Art. 12(1) — Logging & traceability of training data",
            "signal": "Synthetic: AI-generated content presence",
            "value": f"{flagged_pct:.1f}% rows flagged synthetic, verdict: {syn.get('verdict')}",
            "score": round(s, 2),
            "status": _status(s),
        })
        art12_scores.append(s)

    if tox and not tox.get("error"):
        flagged_pct = tox.get("flagged_pct", 0)
        s = _pct_to_score(flagged_pct)
        art12_items.append({
            "clause": "Art. 12(2) — Automatic logging of safety-relevant events",
            "signal": "Toxicity: harmful content rate",
            "value": f"{flagged_pct:.1f}% rows flagged, severity: {tox.get('overall_severity')}",
            "score": round(s, 2),
            "status": _status(s),
        })
        art12_scores.append(s)

    art12_score = round(sum(art12_scores) / len(art12_scores), 2) if art12_scores else None
    articles.append({
        "id": "Art. 12",
        "title": "Record Keeping",
        "score": art12_score,
        "badge": _badge(art12_score) if art12_score is not None else "not-assessed",
        "items": art12_items,
    })

    scored = [a["score"] for a in articles if a["score"] is not None]
    overall = round(sum(scored) / len(scored), 2) if scored else None
    return {"framework": "EU AI Act", "overall_score": overall, "overall_badge": _badge(overall) if overall is not None else "not-assessed", "articles": articles}


# ── India DPDPA ───────────────────────────────────────────────────────────────

INDIA_PII_ENTITIES = {"PHONE_NUMBER", "EMAIL_ADDRESS", "PERSON", "LOCATION", "DATE_TIME"}
INDIA_SENSITIVE = {
    "AADHAAR": "Aadhaar number (12-digit UID)",
    "PAN": "PAN card",
    "PASSPORT": "Passport number",
    "MEDICAL_LICENSE": "Health/medical identifier",
    "CREDIT_CARD": "Financial instrument",
    "IBAN_CODE": "Financial instrument",
    "US_SSN": "Government-issued ID",
}


def build_dpdpa(tq, pii, tox, cr, syn, bias, oo):
    sections = []

    # Section 4 — Lawful processing / personal data present
    s4_items = []
    s4_scores = []
    if pii and not pii.get("error"):
        ps = RISK_SCORE.get(pii.get("overall_risk", "none"), 0.8)
        india_entities = [e for e in (pii.get("global_entities") or []) if e.get("entity") in INDIA_PII_ENTITIES]
        s4_items.append({
            "clause": "Section 4 — Lawful basis for processing personal data",
            "signal": "PII: personal data presence",
            "value": f"{pii.get('pii_row_pct', 0):.1f}% rows contain personal data",
            "score": round(ps, 2),
            "status": _status(ps),
            "detail": f"{len(india_entities)} DPDPA-relevant entity type(s) detected",
        })
        s4_scores.append(ps)
    sections.append({
        "id": "Sec. 4",
        "title": "Lawful Processing of Personal Data",
        "score": round(sum(s4_scores)/len(s4_scores), 2) if s4_scores else None,
        "items": s4_items,
    })

    # Section 8 — Obligations of data fiduciary (quality, accuracy)
    s8_items = []
    s8_scores = []
    if tq and not tq.get("error"):
        dedup = tq.get("dedup") or {}
        dup_rate = dedup.get("near_duplicate_rate", 0)
        s = _pct_to_score(dup_rate)
        s8_items.append({
            "clause": "Section 8(3) — Completeness and accuracy of data",
            "signal": "Text Quality: near-duplicate / data hygiene",
            "value": f"{dup_rate:.1f}% near-duplicates; {dedup.get('exact_duplicates', 0)} exact duplicates",
            "score": round(s, 2),
            "status": _status(s),
        })
        s8_scores.append(s)

    if bias and not bias.get("error"):
        bs = BIAS_SCORE.get(bias.get("overall_bias_risk", "low"), 0.5)
        s8_items.append({
            "clause": "Section 8(4) — Data not used beyond stated purpose",
            "signal": "Bias Surface: protected attribute exposure",
            "value": f"Bias risk: {bias.get('overall_bias_risk')}, {bias.get('protected_columns_found', 0)} protected column(s)",
            "score": round(bs, 2),
            "status": _status(bs),
        })
        s8_scores.append(bs)

    sections.append({
        "id": "Sec. 8",
        "title": "Obligations of Data Fiduciary",
        "score": round(sum(s8_scores)/len(s8_scores), 2) if s8_scores else None,
        "items": s8_items,
    })

    # Section 9 — Special provisions for sensitive personal data
    s9_items = []
    s9_scores = []
    if pii and not pii.get("error"):
        sensitive_hits = [e for e in (pii.get("global_entities") or []) if e.get("entity") in INDIA_SENSITIVE]
        if sensitive_hits:
            s = 0.1
            s9_items.append({
                "clause": "Section 9 — Sensitive personal data detected",
                "signal": "PII: sensitive entity types",
                "value": ", ".join(f"{e['entity']} ×{e['count']}" for e in sensitive_hits[:5]),
                "score": s,
                "status": "fail",
                "detail": "Sensitive data requires explicit consent under DPDPA",
            })
            s9_scores.append(s)
        else:
            s9_items.append({
                "clause": "Section 9 — Sensitive personal data",
                "signal": "PII: no sensitive entity types detected",
                "value": "No Aadhaar / PAN / health / financial identifiers found",
                "score": 1.0,
                "status": "pass",
            })
            s9_scores.append(1.0)

    sections.append({
        "id": "Sec. 9",
        "title": "Sensitive Personal Data",
        "score": round(sum(s9_scores)/len(s9_scores), 2) if s9_scores else None,
        "items": s9_items,
    })

    # Section 16 — Cross-border transfer
    s16_items = []
    s16_scores = []
    if tq and tq.get("jurisdiction"):
        jur = tq["jurisdiction"]
        gdpr_pct = jur.get("gdpr_pct") or 0
        non_india = sum(v for k, v in (jur.get("gdpr_exposure") or {}).items() if k not in ("APAC", "OTHER"))
        s = 1.0 - min(non_india / max(jur.get("url_rows_analyzed", 1), 1), 1.0)
        s16_items.append({
            "clause": "Section 16 — Cross-border transfer restriction",
            "signal": "Text Quality: geographic URL exposure",
            "value": f"{gdpr_pct:.1f}% URLs from EU/EEA/US regions — potential cross-border transfer",
            "score": round(s, 2),
            "status": _status(s),
        })
        s16_scores.append(s)

    sections.append({
        "id": "Sec. 16",
        "title": "Cross-Border Data Transfer",
        "score": round(sum(s16_scores)/len(s16_scores), 2) if s16_scores else None,
        "items": s16_items,
    })

    scored = [s["score"] for s in sections if s["score"] is not None]
    overall = round(sum(scored) / len(scored), 2) if scored else None
    for sec in sections:
        sec["badge"] = _badge(sec["score"]) if sec["score"] is not None else "not-assessed"
    return {"framework": "India DPDPA", "overall_score": overall, "overall_badge": _badge(overall) if overall is not None else "not-assessed", "sections": sections}


# ── NIST AI RMF ───────────────────────────────────────────────────────────────

def build_nist_ai_rmf(tq, pii, tox, cr, syn, bias, oo):
    functions = []

    # GOVERN — policies, accountability
    gov_items = []
    gov_scores = []

    if cr and not cr.get("error"):
        s = 1.0 - (cr.get("composite_score") or 0)
        gov_items.append({
            "function": "GOVERN 1.1 — Policies for data sourcing & IP",
            "signal": "Copyright Risk: license + domain + content signals",
            "value": f"Overall copyright risk: {cr.get('overall_risk')}, composite score: {cr.get('composite_score', 0):.2f}",
            "score": round(s, 2),
            "status": _status(s),
        })
        gov_scores.append(s)

    if oo and not oo.get("error"):
        s = OO_SCORE.get(oo.get("status", "clear"), 1.0)
        gov_items.append({
            "function": "GOVERN 1.2 — Data use consent & opt-out compliance",
            "signal": "Opt-Out: Spawning AI, robots.txt, HF registry",
            "value": f"Opt-out status: {oo.get('status')}, {oo.get('spawning', {}).get('opted_out_count', 0)} opted-out URLs",
            "score": round(s, 2),
            "status": _status(s),
        })
        gov_scores.append(s)

    functions.append({
        "id": "GOVERN",
        "title": "Govern — Policies & Accountability",
        "score": round(sum(gov_scores)/len(gov_scores), 2) if gov_scores else None,
        "items": gov_items,
    })

    # MAP — risk identification
    map_items = []
    map_scores = []

    if pii and not pii.get("error"):
        ps = RISK_SCORE.get(pii.get("overall_risk", "none"), 0.8)
        map_items.append({
            "function": "MAP 2.2 — Identify risks: privacy",
            "signal": "PII: entity presence and risk level",
            "value": f"{pii.get('pii_row_pct', 0):.1f}% PII rows, {pii.get('total_entities', 0)} entities",
            "score": round(ps, 2),
            "status": _status(ps),
        })
        map_scores.append(ps)

    if tox and not tox.get("error"):
        s = _pct_to_score(tox.get("flagged_pct", 0))
        map_items.append({
            "function": "MAP 2.3 — Identify risks: harmful content",
            "signal": "Toxicity: harmful content rate",
            "value": f"{tox.get('flagged_pct', 0):.1f}% flagged, {tox.get('total_severe', 0)} severe",
            "score": round(s, 2),
            "status": _status(s),
        })
        map_scores.append(s)

    if bias and not bias.get("error"):
        bs = BIAS_SCORE.get(bias.get("overall_bias_risk", "low"), 0.5)
        map_items.append({
            "function": "MAP 2.5 — Identify risks: bias and fairness",
            "signal": "Bias Surface: protected attribute imbalance",
            "value": f"Bias risk: {bias.get('overall_bias_risk')}, {bias.get('protected_columns_found', 0)} protected column(s)",
            "score": round(bs, 2),
            "status": _status(bs),
        })
        map_scores.append(bs)

    functions.append({
        "id": "MAP",
        "title": "Map — Risk Identification",
        "score": round(sum(map_scores)/len(map_scores), 2) if map_scores else None,
        "items": map_items,
    })

    # MEASURE — quantitative assessment
    meas_items = []
    meas_scores = []

    if tq and not tq.get("error"):
        dedup = tq.get("dedup") or {}
        dup_rate = dedup.get("near_duplicate_rate", 0)
        s = _pct_to_score(dup_rate)
        meas_items.append({
            "function": "MEASURE 2.5 — Data quality metrics",
            "signal": "Text Quality: deduplication + language",
            "value": f"Near-duplicate rate: {dup_rate:.1f}%, {dedup.get('exact_duplicates', 0)} exact duplicates",
            "score": round(s, 2),
            "status": _status(s),
        })
        meas_scores.append(s)

    if syn and not syn.get("error"):
        s = _pct_to_score(syn.get("flagged_pct", 0))
        meas_items.append({
            "function": "MEASURE 2.7 — Data authenticity & provenance",
            "signal": "Synthetic: AI-generated content score",
            "value": f"Verdict: {syn.get('verdict')}, avg score: {syn.get('avg_score', 0)*100:.0f}%, {syn.get('flagged_pct', 0):.1f}% flagged",
            "score": round(s, 2),
            "status": _status(s),
        })
        meas_scores.append(s)

    if bias and not bias.get("error"):
        label_imbalance = bias.get("label_imbalance") or []
        worst_gini = max((l.get("gini", 0) for l in label_imbalance), default=0)
        s = 1.0 - worst_gini
        if label_imbalance:
            meas_items.append({
                "function": "MEASURE 2.11 — Fairness metrics (label imbalance)",
                "signal": "Bias Surface: label distribution Gini",
                "value": f"Worst Gini: {worst_gini:.2f}, {len(label_imbalance)} label column(s)",
                "score": round(s, 2),
                "status": _status(s),
            })
            meas_scores.append(s)

    functions.append({
        "id": "MEASURE",
        "title": "Measure — Quantitative Assessment",
        "score": round(sum(meas_scores)/len(meas_scores), 2) if meas_scores else None,
        "items": meas_items,
    })

    # MANAGE — remediation priorities
    manage_items = []
    manage_scores = []
    all_scores_flat = []

    if pii and not pii.get("error"):
        all_scores_flat.append(("Privacy / PII", RISK_SCORE.get(pii.get("overall_risk", "none"), 0.8)))
    if tox and not tox.get("error"):
        all_scores_flat.append(("Safety / Toxicity", _pct_to_score(tox.get("flagged_pct", 0))))
    if bias and not bias.get("error"):
        all_scores_flat.append(("Fairness / Bias", BIAS_SCORE.get(bias.get("overall_bias_risk", "low"), 0.5)))
    if cr and not cr.get("error"):
        all_scores_flat.append(("Copyright Risk", 1.0 - (cr.get("composite_score") or 0)))
    if syn and not syn.get("error"):
        all_scores_flat.append(("Authenticity", _pct_to_score(syn.get("flagged_pct", 0))))
    if oo and not oo.get("error"):
        all_scores_flat.append(("Opt-Out", OO_SCORE.get(oo.get("status", "clear"), 1.0)))

    # Surface the lowest-scoring dimensions as remediation priorities
    sorted_dims = sorted(all_scores_flat, key=lambda x: x[1])
    for dim, s in sorted_dims[:4]:
        manage_items.append({
            "function": f"MANAGE 3.1 — Remediation priority: {dim}",
            "signal": dim,
            "value": f"Score: {s:.2f} — {'Critical action needed' if s < 0.45 else 'Review recommended' if s < 0.75 else 'Acceptable'}",
            "score": round(s, 2),
            "status": _status(s),
        })
        manage_scores.append(s)

    functions.append({
        "id": "MANAGE",
        "title": "Manage — Remediation Priorities",
        "score": round(sum(manage_scores)/len(manage_scores), 2) if manage_scores else None,
        "items": manage_items,
    })

    scored = [f["score"] for f in functions if f["score"] is not None]
    overall = round(sum(scored) / len(scored), 2) if scored else None
    for fn in functions:
        fn["badge"] = _badge(fn["score"]) if fn["score"] is not None else "not-assessed"
    return {"framework": "NIST AI RMF", "overall_score": overall, "overall_badge": _badge(overall) if overall is not None else "not-assessed", "functions": functions}


# ── Route ─────────────────────────────────────────────────────────────────────

@router.post("/dataset/compliance-report")
def compliance_report(req: ComplianceReportRequest):
    tq   = req.tq
    pii  = req.pii
    tox  = req.tox
    cr   = req.cr
    syn  = req.syn
    bias = req.bias
    oo   = req.oo

    eu   = build_eu_ai_act(tq, pii, tox, cr, syn, bias, oo)
    dpdpa = build_dpdpa(tq, pii, tox, cr, syn, bias, oo)
    nist  = build_nist_ai_rmf(tq, pii, tox, cr, syn, bias, oo)

    dimensions_assessed = sum(1 for x in [tq, pii, tox, cr, syn, bias, oo] if x is not None)

    return {
        "dataset_name": req.dataset_name,
        "dimensions_assessed": dimensions_assessed,
        "eu_ai_act": eu,
        "india_dpdpa": dpdpa,
        "nist_ai_rmf": nist,
    }


# ── Audit Trail ───────────────────────────────────────────────────────────────

class AuditTrailRequest(BaseModel):
    tq:   Optional[dict[str, Any]] = None
    pii:  Optional[dict[str, Any]] = None
    tox:  Optional[dict[str, Any]] = None
    cr:   Optional[dict[str, Any]] = None
    syn:  Optional[dict[str, Any]] = None
    bias: Optional[dict[str, Any]] = None
    oo:   Optional[dict[str, Any]] = None
    dataset_name: Optional[str] = None
    hf_id: Optional[str] = None


def _evidence_pii(pii: dict) -> dict:
    col_summaries = pii.get("column_summaries") or []
    flagged_cols = [
        {
            "column": c["column"],
            "pii_rows": c["pii_rows"],
            "pii_row_pct": round(c.get("pii_row_pct", 0), 2),
            "peak_risk": c.get("peak_risk"),
            "top_entities": [e["entity"] for e in (c.get("top_entities") or [])[:5]],
        }
        for c in col_summaries if c.get("pii_rows", 0) > 0
    ]
    global_entities = [
        {"entity": e["entity"], "count": e["count"], "risk": e["risk"]}
        for e in (pii.get("global_entities") or [])[:10]
    ]
    return {
        "rows_with_pii": pii.get("rows_with_pii", 0),
        "pii_row_pct": round(pii.get("pii_row_pct", 0), 2),
        "overall_risk": pii.get("overall_risk"),
        "total_entities": pii.get("total_entities", 0),
        "top_entity_types": global_entities,
        "affected_columns": flagged_cols,
    }


def _evidence_tox(tox: dict) -> dict:
    flagged_rows = tox.get("flagged_rows") or []
    col_summaries = tox.get("column_summaries") or []
    return {
        "flagged_pct": round(tox.get("flagged_pct", 0), 2),
        "overall_severity": tox.get("overall_severity"),
        "total_flagged": tox.get("total_flagged", 0),
        "total_severe": tox.get("total_severe", 0),
        "flagged_row_indices": [r["row_idx"] for r in flagged_rows[:50]],
        "peak_labels": [
            {"column": c["column"], "peak_label": c.get("peak_label"), "peak_score": round(c.get("peak_score", 0), 3)}
            for c in col_summaries if c.get("flagged", 0) > 0
        ],
    }


def _evidence_syn(syn: dict) -> dict:
    flagged_rows = syn.get("flagged_rows") or []
    col_summaries = syn.get("column_summaries") or []
    return {
        "flagged_pct": round(syn.get("flagged_pct", 0), 2),
        "verdict": syn.get("verdict"),
        "avg_score": round(syn.get("avg_score", 0), 3),
        "total_flagged": syn.get("total_flagged", 0),
        "flagged_row_indices": [r["row_idx"] for r in flagged_rows[:50]],
        "high_confidence_count": syn.get("total_high_conf", 0),
        "affected_columns": [
            {"column": c["column"], "flagged": c["flagged"], "flagged_pct": round(c.get("flagged_pct", 0), 2), "avg_score": round(c.get("avg_score", 0), 3)}
            for c in col_summaries if c.get("flagged", 0) > 0
        ],
    }


def _evidence_bias(bias: dict) -> dict:
    protected_attrs = bias.get("protected_attrs") or []
    label_imbalance = bias.get("label_imbalance") or []
    return {
        "overall_bias_risk": bias.get("overall_bias_risk"),
        "protected_columns_found": bias.get("protected_columns_found", 0),
        "summary_flags": bias.get("summary_flags") or [],
        "protected_attributes": [
            {
                "column": a["column"],
                "attribute_type": a.get("attribute_type"),
                "imbalance_severity": a.get("imbalance_severity"),
                "imbalance_ratio": round(a.get("imbalance_ratio", 0), 3),
                "gini": round(a.get("gini", 0), 3),
                "top_values": a.get("top_values") or [],
            }
            for a in protected_attrs[:10]
        ],
        "label_imbalance": [
            {
                "column": l["column"],
                "severity": l.get("severity"),
                "imbalance_ratio": round(l.get("imbalance_ratio", 0), 3),
                "minority_class": l.get("minority_class"),
                "minority_pct": round(l.get("minority_pct", 0), 2),
                "majority_class": l.get("majority_class"),
                "majority_pct": round(l.get("majority_pct", 0), 2),
            }
            for l in label_imbalance[:10]
        ],
    }


def _evidence_cr(cr: dict) -> dict:
    license_info = cr.get("license") or {}
    domain_info  = cr.get("domain") or {}
    content_info = cr.get("content") or {}
    return {
        "overall_risk": cr.get("overall_risk"),
        "composite_score": round(cr.get("composite_score", 0), 3),
        "license": {
            "raw": license_info.get("raw"),
            "label": license_info.get("label"),
            "resolved_key": license_info.get("resolved_key"),
            "risk_score": round(license_info.get("score", 0), 3),
        },
        "domain": {
            "urls_analyzed": domain_info.get("urls_analyzed", 0),
            "risk_score": round(domain_info.get("score") or 0, 3),
            "top_reasons": domain_info.get("top_reasons") or [],
        },
        "content": {
            "copyright_pct": round(content_info.get("copyright_pct", 0), 2),
            "open_pct": round(content_info.get("open_pct", 0), 2),
            "risk_score": round(content_info.get("score", 0), 3),
            "signals": content_info.get("signals") or [],
        },
    }


def _evidence_oo(oo: dict) -> dict:
    spawning = oo.get("spawning") or {}
    robots   = oo.get("robots") or {}
    hf       = oo.get("hf_dataset")
    return {
        "status": oo.get("status"),
        "urls_checked": oo.get("urls_checked", 0),
        "spawning": {
            "opted_out_count": spawning.get("opted_out_count", 0),
            "opted_out_urls": (spawning.get("opted_out_urls") or [])[:20],
            "top_domains": (spawning.get("domain_breakdown") or [])[:10],
        },
        "robots_txt": {
            "domains_blocked": robots.get("domains_blocked", 0),
            "blocked_domains": [b["domain"] for b in (robots.get("blocked") or [])[:10]],
        },
        "hf_dataset": {
            "dataset_id": hf["dataset_id"] if hf else None,
            "opted_out": hf["opted_out"] if hf else None,
        } if hf is not None else None,
    }


def _evidence_tq(tq: dict) -> dict:
    dedup = tq.get("dedup") or {}
    lang  = tq.get("language") or {}
    jur   = tq.get("jurisdiction") or {}
    lic   = tq.get("license") or {}
    return {
        "deduplication": {
            "total_rows": dedup.get("total_rows", 0),
            "duplicate_groups": dedup.get("duplicate_groups", 0),
            "duplicate_row_count": dedup.get("duplicate_row_count", 0),
            "exact_duplicates": dedup.get("exact_duplicates", 0),
            "near_duplicate_rate": round(dedup.get("near_duplicate_rate", 0), 2),
        },
        "language": {
            "dominant_lang": lang.get("dominant_lang"),
            "multilingual": lang.get("multilingual", False),
            "total_detected": lang.get("total_detected", 0),
            "distribution": (lang.get("distribution") or [])[:8],
        },
        "jurisdiction": {
            "gdpr_pct": round(jur.get("gdpr_pct") or 0, 2),
            "url_rows_analyzed": jur.get("url_rows_analyzed", 0),
            "gdpr_exposure": jur.get("gdpr_exposure") or {},
            "region_distribution": (jur.get("region_distribution") or [])[:10],
        },
        "license": {
            "raw": (lic.get("dataset_license") or {}).get("raw"),
            "spdx": (lic.get("dataset_license") or {}).get("spdx"),
            "use": (lic.get("dataset_license") or {}).get("use"),
            "commercial": (lic.get("dataset_license") or {}).get("commercial"),
        },
    }


def build_audit_trail(req: AuditTrailRequest) -> dict:
    tq   = req.tq
    pii  = req.pii
    tox  = req.tox
    cr   = req.cr
    syn  = req.syn
    bias = req.bias
    oo   = req.oo

    run_id = str(uuid.uuid4())
    generated_at = datetime.now(timezone.utc).isoformat()

    # Rebuild compliance scores for per-clause evidence linkage
    eu    = build_eu_ai_act(tq, pii, tox, cr, syn, bias, oo)
    dpdpa = build_dpdpa(tq, pii, tox, cr, syn, bias, oo)
    nist  = build_nist_ai_rmf(tq, pii, tox, cr, syn, bias, oo)

    # Per-dimension evidence blocks
    evidence_blocks = {}
    if pii  and not pii.get("error"):  evidence_blocks["pii"]  = _evidence_pii(pii)
    if tox  and not tox.get("error"):  evidence_blocks["tox"]  = _evidence_tox(tox)
    if syn  and not syn.get("error"):  evidence_blocks["syn"]  = _evidence_syn(syn)
    if bias and not bias.get("error"): evidence_blocks["bias"] = _evidence_bias(bias)
    if cr   and not cr.get("error"):   evidence_blocks["cr"]   = _evidence_cr(cr)
    if oo   and not oo.get("error"):   evidence_blocks["oo"]   = _evidence_oo(oo)
    if tq   and not tq.get("error"):   evidence_blocks["tq"]   = _evidence_tq(tq)

    # Flatten all clause findings across frameworks with evidence references
    def _clauses_from_groups(groups: list, group_key: str) -> list:
        findings = []
        for group in groups:
            for item in (group.get("items") or []):
                clause_ref = item.get("clause") or item.get("function") or ""
                signal = item.get("signal", "")
                # Map signal prefix → evidence block key
                sig_lower = signal.lower()
                if "pii" in sig_lower or "privacy" in sig_lower:
                    ev_key = "pii"
                elif "tox" in sig_lower or "harmful" in sig_lower or "safety" in sig_lower:
                    ev_key = "tox"
                elif "synthetic" in sig_lower or "authenticity" in sig_lower:
                    ev_key = "syn"
                elif "bias" in sig_lower or "fairness" in sig_lower or "protected" in sig_lower or "label" in sig_lower or "gini" in sig_lower:
                    ev_key = "bias"
                elif "copyright" in sig_lower or "license" in sig_lower or "domain" in sig_lower or "content" in sig_lower:
                    ev_key = "cr"
                elif "opt-out" in sig_lower or "spawning" in sig_lower or "robots" in sig_lower or "opt_out" in sig_lower:
                    ev_key = "oo"
                elif "text quality" in sig_lower or "dedup" in sig_lower or "duplicate" in sig_lower or "language" in sig_lower or "jurisdiction" in sig_lower or "gdpr" in sig_lower:
                    ev_key = "tq"
                else:
                    ev_key = None

                findings.append({
                    "framework": group_key,
                    "group_id": group.get("id"),
                    "group_title": group.get("title"),
                    "clause": clause_ref,
                    "signal": signal,
                    "value": item.get("value", ""),
                    "score": item.get("score"),
                    "status": item.get("status"),
                    "detail": item.get("detail"),
                    "evidence_dimension": ev_key,
                    "evidence": evidence_blocks.get(ev_key) if ev_key else None,
                })
        return findings

    findings = []
    findings += _clauses_from_groups(eu.get("articles") or [], "EU AI Act")
    findings += _clauses_from_groups(dpdpa.get("sections") or [], "India DPDPA")
    findings += _clauses_from_groups(nist.get("functions") or [], "NIST AI RMF")

    # Summary: worst findings first
    failed   = [f for f in findings if f["status"] == "fail"]
    warned   = [f for f in findings if f["status"] == "warn"]
    passed   = [f for f in findings if f["status"] == "pass"]

    overall_scores = [f["score"] for f in findings if f["score"] is not None]
    overall_health = round(sum(overall_scores) / len(overall_scores), 3) if overall_scores else None

    return {
        "run_id": run_id,
        "generated_at": generated_at,
        "dataset_name": req.dataset_name or req.hf_id or "unknown",
        "hf_id": req.hf_id,
        "dimensions_assessed": sum(1 for x in [tq, pii, tox, cr, syn, bias, oo] if x is not None),
        "overall_health_score": overall_health,
        "summary": {
            "total_clauses": len(findings),
            "failed": len(failed),
            "warned": len(warned),
            "passed": len(passed),
        },
        "framework_scores": {
            "eu_ai_act":    {"score": eu.get("overall_score"),    "badge": eu.get("overall_badge")},
            "india_dpdpa":  {"score": dpdpa.get("overall_score"), "badge": dpdpa.get("overall_badge")},
            "nist_ai_rmf":  {"score": nist.get("overall_score"),  "badge": nist.get("overall_badge")},
        },
        "findings": findings,
        "evidence": evidence_blocks,
    }


@router.post("/dataset/audit-trail")
def audit_trail(req: AuditTrailRequest):
    return build_audit_trail(req)
