#!/usr/bin/env python3
"""Evidence-qualified quality confidence shared by CARBON result reports."""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

CONCLUSIVE = {"passed", "failed"}
TERMINAL = {"passed", "failed", "blocked", "skipped", "canceled"}
BROWSER_ADAPTER_TOKENS = {
    "browser", "playwright", "selenium", "cypress", "visual", "accessibility"
}
DIMENSION_RULES = {
    "functional": {
        "label": "Functional behavior",
        "tokens": {"functional", "flow", "browser", "api", "contract", "integration", "e2e", "intent", "positive", "negative", "boundary"},
    },
    "security": {
        "label": "Security and privacy",
        "tokens": {"security", "privacy", "authorization", "authentication", "auth", "tenant", "permission", "injection", "secret", "pii", "xss", "script", "innerhtml", "onerror", "unsafe-rendering"},
    },
    "accessibility": {
        "label": "Accessibility",
        "tokens": {"accessibility", "a11y", "wcag", "keyboard", "screenreader", "screen-reader", "contrast", "aria"},
    },
    "performance": {
        "label": "Performance and efficiency",
        "tokens": {"performance", "latency", "load", "throughput", "lcp", "inp", "cls", "memory", "cost", "token"},
    },
    "reliability": {
        "label": "Reliability and recovery",
        "tokens": {"reliability", "resilience", "retry", "recovery", "timeout", "idempotency", "concurrency", "race", "offline", "failover"},
    },
    "usability": {
        "label": "Usability and product experience",
        "tokens": {"usability", "ux", "persona", "content", "visual", "responsive", "navigation", "friction", "copy"},
    },
    "data": {
        "label": "Data and state integrity",
        "tokens": {"data", "schema", "database", "migration", "persistence", "storage", "serialization", "validation", "state"},
    },
    "operational": {
        "label": "Operational readiness",
        "tokens": {"operational", "monitoring", "observability", "trace", "logging", "rollback", "canary", "deployment", "environment", "dependency"},
    },
}

CATEGORY_DIMENSIONS = {
    "security": {"security"},
    "privacy": {"security"},
    "accessibility": {"accessibility"},
    "performance": {"performance"},
    "visual": {"usability"},
    "content": {"usability"},
    "persona": {"usability"},
    "data": {"data"},
    "storage": {"data"},
    "console": {"operational"},
    "network": {"reliability", "operational"},
}
NON_DECISION_CATEGORIES = {"correlation", "exploration"}


def human_decision_context(state: dict[str, Any]) -> dict[str, Any]:
    """Load only the decision-context summary; answers remain local and explicit.

    Runs retain their technical evidence semantics. This optional layer makes
    the human judgment boundary visible without treating unanswered questions
    as a failed test or blocking a confidence assessment.
    """
    roots = [state.get("targetRoot"), state.get("stateRoot"), state.get("projectRoot")]
    expected_path = ""
    for value in roots:
        if not value:
            continue
        try:
            path = Path(str(value)).expanduser().resolve() / ".carbon" / "human" / "decision-context.json"
            expected_path = str(path)
            raw = json.loads(path.read_text(encoding="utf-8"))
        except (OSError, ValueError, json.JSONDecodeError):
            continue
        if not isinstance(raw, dict) or raw.get("schema") != "carbon.human-decision-context/v1":
            continue
        questions = [row for row in raw.get("questions", []) if isinstance(row, dict)]
        open_questions = [row for row in questions if row.get("status") == "open"]
        answered = [row for row in questions if row.get("status") == "answered"]
        return {
            "status": "pending" if open_questions else ("recorded" if answered else "not-recorded"),
            "path": str(path),
            "open": len(open_questions),
            "answered": len(answered),
            "openP0": sum(1 for row in open_questions if row.get("priority") == "P0"),
            "pendingQuestionIds": [str(row.get("id")) for row in open_questions[:8]],
            "qualifier": "Human decision context is pending; confidence covers observed technical evidence but not unrecorded intent, risk appetite, taste, consequence, domain judgment, or accountability." if open_questions or not questions else "Human decision context is recorded; answers guide planning and interpretation but do not turn unobserved behavior into a pass.",
        }
    return {
        "status": "not-recorded",
        "path": expected_path,
        "open": 0,
        "answered": 0,
        "openP0": 0,
        "pendingQuestionIds": [],
        "qualifier": "Human decision context has not been recorded; confidence covers observed technical evidence only, not unrecorded intent, risk appetite, taste, consequence, domain judgment, or accountability.",
    }


def clean_strings(value: Any, limit: int = 50) -> list[str]:
    if not isinstance(value, list):
        return []
    result: list[str] = []
    for item in value:
        text = " ".join(str(item or "").split())[:1000]
        if text and text not in result:
            result.append(text)
        if len(result) >= limit:
            break
    return result


def item_id(test: dict[str, Any], index: int) -> str:
    return str(test.get("id") or test.get("testId") or f"TEST-{index + 1:03d}")


def latest_result(test: dict[str, Any]) -> dict[str, Any]:
    attempts = test.get("attempts")
    if isinstance(attempts, list) and attempts and isinstance(attempts[-1], dict):
        return attempts[-1]
    return test


def has_evidence(test: dict[str, Any]) -> bool:
    result = latest_result(test)
    actual = str(result.get("actual") or "").strip()
    evidence = result.get("evidence")
    return bool(actual and isinstance(evidence, list) and any(str(value).strip() for value in evidence))


def searchable_test(test: dict[str, Any]) -> set[str]:
    fields = [
        test.get("type"), test.get("title"), test.get("reason"), test.get("adapter"),
        test.get("flow"), test.get("item"), test.get("oracle"),
    ]
    flags = test.get("flags") if isinstance(test.get("flags"), dict) else {}
    fields.extend(key for key, enabled in flags.items() if enabled)
    text = " ".join(str(value or "").lower() for value in fields)
    return set(text.replace("/", " ").replace("_", " ").replace("-", " ").split())


def behavior_rows(tests: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Use child observations when a broad surface check contains mixed behavior."""
    rows: list[dict[str, Any]] = []
    for test in tests:
        observations = [row for row in test.get("observations", []) if isinstance(row, dict)]
        if not observations:
            rows.append(test)
            continue
        for observation in observations:
            row = dict(test)
            row.update(observation)
            row["adapter"] = observation.get("adapter") or test.get("adapter")
            row["priority"] = observation.get("priority") or test.get("priority")
            row["flags"] = test.get("flags", {})
            rows.append(row)
    return rows


def finding_dimensions(finding: dict[str, Any]) -> set[str]:
    explicit = finding.get("dimensions")
    if isinstance(explicit, list):
        selected = {str(value).lower() for value in explicit if str(value).lower() in DIMENSION_RULES}
        if selected:
            return selected
    category = str(finding.get("category") or "").lower()
    if category in CATEGORY_DIMENSIONS:
        return set(CATEGORY_DIMENSIONS[category])
    if category in NON_DECISION_CATEGORIES:
        return set()
    text = " ".join(str(finding.get(key) or "").lower() for key in ("title", "detail", "codeLocation"))
    words = set(text.replace("/", " ").replace("_", " ").replace("-", " ").split())
    return {
        identifier for identifier, rule in DIMENSION_RULES.items()
        if words & set(rule["tokens"])
    }


def decision_issues(findings: list[dict[str, Any]], artifact_issues: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Remove correlated/derived duplicates before computing release confidence."""
    selected: list[dict[str, Any]] = []
    seen: set[tuple[str, str, str, str]] = set()
    for finding in findings + artifact_issues:
        category = str(finding.get("category") or "").lower()
        if category in NON_DECISION_CATEGORIES:
            continue
        key = (
            category,
            str(finding.get("severity") or "").lower(),
            str(finding.get("title") or "").strip().lower(),
            str(finding.get("detail") or "").strip().lower(),
        )
        if key in seen:
            continue
        seen.add(key)
        selected.append(finding)
    return selected


def confidence_dimensions(
    tests: list[dict[str, Any]],
    findings: list[dict[str, Any]],
    artifact_issues: list[dict[str, Any]],
) -> dict[str, dict[str, Any]]:
    dimensions: dict[str, dict[str, Any]] = {}
    assessed_tests = behavior_rows(tests)
    all_findings = decision_issues(findings, artifact_issues)
    for identifier, rule in DIMENSION_RULES.items():
        tokens = set(rule["tokens"])
        relevant_tests = []
        for test in assessed_tests:
            explicit = {str(value).lower() for value in test.get("dimensions", [])}
            if (explicit and identifier in explicit) or (not explicit and searchable_test(test) & tokens):
                relevant_tests.append(test)
        relevant_findings = [finding for finding in all_findings if identifier in finding_dimensions(finding)]
        counts = {name: 0 for name in ("planned", "queued", "running", "passed", "failed", "blocked", "skipped", "canceled", "needs-repair")}
        for test in relevant_tests:
            status = str(test.get("status") or "planned").lower()
            counts[status] = counts.get(status, 0) + 1
        conclusive = counts["passed"] + counts["failed"]
        evidence_backed = sum(1 for test in relevant_tests if str(test.get("status")) in CONCLUSIVE and has_evidence(test))
        severe = [row for row in relevant_findings if str(row.get("severity")).lower() in {"critical", "high"}]
        gaps: list[str] = []
        if not relevant_tests and not relevant_findings:
            gaps.append("No dimension-specific tests or findings are recorded.")
        unfinished = sum(counts[name] for name in ("planned", "queued", "running", "blocked", "needs-repair"))
        if unfinished:
            gaps.append(f"{unfinished} relevant checks remain unfinished or blocked.")
        if conclusive > evidence_backed:
            gaps.append(f"{conclusive - evidence_backed} conclusive result(s) lack an actual result plus evidence handle.")
        if severe or counts["failed"]:
            band, score = "low", 20
            basis = "Observed failures or severe findings contradict this dimension."
        elif not conclusive:
            band, score = "not-assessed", None
            basis = "No conclusive dimension-specific execution is recorded."
        elif gaps:
            band, score = "guarded", 55
            basis = "Some evidence supports this dimension, but material gaps remain."
        elif conclusive >= 2 and evidence_backed == conclusive:
            band, score = "high", 90
            basis = "Multiple conclusive results are evidence-backed with no recorded failure or material gap."
        else:
            band, score = "moderate", 72
            basis = "A limited conclusive evidence set supports this dimension without a recorded failure."
        dimensions[identifier] = {
            "label": rule["label"],
            "band": band,
            "score": score,
            "basis": basis,
            "tests": len(relevant_tests),
            "conclusive": conclusive,
            "evidenceBacked": evidence_backed,
            "passed": counts["passed"],
            "failed": counts["failed"],
            "severeFindings": len(severe),
            "gaps": gaps[:8],
            "isProbabilityOfCorrectness": False,
        }
    return dimensions


def assess_quality_confidence(
    state: dict[str, Any] | None,
    evidence_manifest: dict[str, Any] | None = None,
    model_coverage_gaps: list[Any] | None = None,
) -> dict[str, Any]:
    """Assess scoped quality evidence without presenting a probability of correctness."""
    state = state if isinstance(state, dict) else {}
    manifest = evidence_manifest if isinstance(evidence_manifest, dict) else {}
    tests = [row for row in state.get("tests", []) if isinstance(row, dict)]
    counts = {name: 0 for name in ("planned", "queued", "running", "passed", "failed", "blocked", "skipped", "canceled", "needs-repair")}
    for test in tests:
        status = str(test.get("status") or "planned").lower()
        counts[status] = counts.get(status, 0) + 1
    total = len(tests)
    conclusive = counts.get("passed", 0) + counts.get("failed", 0)
    terminal = sum(counts.get(name, 0) for name in TERMINAL)
    unfinished = max(0, total - terminal)
    evidence_backed = sum(1 for test in tests if str(test.get("status")) in CONCLUSIVE and has_evidence(test))
    evidence_missing_ids = [
        item_id(test, index) for index, test in enumerate(tests)
        if str(test.get("status")) in CONCLUSIVE and not has_evidence(test)
    ]
    failed_high = [
        item_id(test, index) for index, test in enumerate(tests)
        if str(test.get("status")) == "failed" and str(test.get("priority") or "P2").upper() in {"P0", "P1"}
    ]
    failed_p0 = [
        item_id(test, index) for index, test in enumerate(tests)
        if str(test.get("status")) == "failed" and str(test.get("priority") or "P2").upper() == "P0"
    ]
    findings = [row for row in state.get("findings", []) if isinstance(row, dict)]
    artifact_issues = [row for row in manifest.get("issues", []) if isinstance(row, dict)]
    dimensions = confidence_dimensions(tests, findings, artifact_issues)
    human_context = human_decision_context(state)
    material_issues = decision_issues(findings, artifact_issues)
    critical = [row for row in material_issues if str(row.get("severity")).lower() == "critical"]
    high = [row for row in material_issues if str(row.get("severity")).lower() == "high"]

    missing: list[str] = []
    if unfinished:
        missing.append(f"{unfinished} planned or running tests do not have terminal results")
    if counts.get("blocked"):
        missing.append(f"{counts['blocked']} tests are blocked")
    if counts.get("skipped"):
        missing.append(f"{counts['skipped']} tests were skipped")
    if counts.get("canceled"):
        missing.append(f"{counts['canceled']} tests were canceled")
    if counts.get("needs-repair"):
        missing.append(f"{counts['needs-repair']} imported tests still need definition repair")
    if evidence_missing_ids:
        preview = ", ".join(evidence_missing_ids[:8])
        suffix = "…" if len(evidence_missing_ids) > 8 else ""
        missing.append(f"{len(evidence_missing_ids)} conclusive results lack both an actual result and evidence handle: {preview}{suffix}")

    exclusions = clean_strings(state.get("exclusions"))
    residual = clean_strings(state.get("residualUncertainty"))
    if exclusions:
        missing.append(f"Excluded scope remains untested: {'; '.join(exclusions[:8])}")
    if residual:
        missing.append(f"Residual uncertainty remains: {'; '.join(residual[:8])}")
    if state.get("cutLine"):
        missing.append(f"The run ended with a cut line: {str(state['cutLine'])[:1000]}")
    # This is intentionally a qualifier rather than a test failure or a gate.
    # CARBON can still assess observed evidence while making explicit the part
    # of a ship decision that requires accountable human judgment.
    if human_context["status"] != "recorded" and human_context["path"]:
        if human_context["open"]:
            missing.append(f"Human decision context remains open: {human_context['open']} question(s), including {human_context['openP0']} P0 question(s).")
        else:
            missing.append("Human decision context has not been recorded.")

    model_gaps = []
    for gap in model_coverage_gaps or []:
        if isinstance(gap, dict):
            text = " · ".join(filter(None, (str(gap.get("name") or "").strip(), str(gap.get("reason") or "").strip())))
        else:
            text = str(gap).strip()
        if text:
            model_gaps.append(text[:1000])
    if model_gaps:
        missing.append(f"{len(model_gaps)} modeled product or API entities have no mapped coverage: {'; '.join(model_gaps[:8])}")

    pages = [row for row in manifest.get("pages", []) if isinstance(row, dict)]
    missing_channels: dict[str, int] = {}
    for page in pages:
        coverage = page.get("channelCoverage") if isinstance(page.get("channelCoverage"), dict) else {}
        for channel, present in coverage.items():
            if not present:
                missing_channels[str(channel)] = missing_channels.get(str(channel), 0) + 1
    if missing_channels:
        detail = ", ".join(f"{name} on {count} page(s)" for name, count in sorted(missing_channels.items()))
        missing.append(f"Page evidence channels are missing or unavailable: {detail}")

    browser_results = [
        test for test in tests
        if str(test.get("status")) in CONCLUSIVE
        and any(token in str(test.get("adapter") or "").lower() for token in BROWSER_ADAPTER_TOKENS)
    ]
    if browser_results:
        planned_personas = {str(row.get("id")) for row in state.get("personas", []) if isinstance(row, dict) and row.get("id")}
        observed_personas = {str(row.get("personaId")) for row in manifest.get("personaFeedback", []) if isinstance(row, dict) and row.get("personaId")}
        persona_gap = sorted(planned_personas - observed_personas)
        if persona_gap:
            missing.append(f"Planned user perspectives lack observed feedback: {', '.join(persona_gap)}")
        planned_charters = {str(row.get("id")) for row in state.get("explorationCharters", []) if isinstance(row, dict) and row.get("id")}
        observed_charters = {str(row.get("charterId")) for row in manifest.get("explorationObservations", []) if isinstance(row, dict) and row.get("charterId")}
        charter_gap = sorted(planned_charters - observed_charters)
        if charter_gap:
            missing.append(f"Exploration charters lack observations: {', '.join(charter_gap)}")

    failure_reasons: list[str] = []
    if critical:
        failure_reasons.append(f"{len(critical)} critical finding(s) remain")
    if high:
        failure_reasons.append(f"{len(high)} high-severity finding(s) remain")
    if failed_high:
        failure_reasons.append(f"Failed P0/P1 tests: {', '.join(failed_high[:12])}")
    other_failures = max(0, counts.get("failed", 0) - len(failed_high))
    if other_failures:
        failure_reasons.append(f"{other_failures} additional test failure(s) remain")

    if not conclusive:
        band = "not-assessed"
        risk = "unknown"
        basis = "No passed or failed execution results are available, so quality confidence is not assessed."
    elif critical or failed_p0:
        band = "low"
        risk = "critical"
        basis = "Observed critical evidence contradicts the quality claim for the selected scope."
    elif high or failed_high or counts.get("failed"):
        band = "low"
        risk = "high"
        basis = "Observed failures or high-severity findings materially limit confidence in the selected scope."
    elif missing:
        band = "guarded"
        risk = "moderate"
        basis = "Observed checks support part of the selected scope, but material coverage or evidence gaps remain."
    elif total >= 5 and evidence_backed == conclusive == total:
        band = "high"
        risk = "low"
        basis = "All selected checks have conclusive, evidence-backed results with no recorded material gap or failure."
    else:
        band = "moderate"
        risk = "low"
        basis = "The selected checks are conclusive and no material failure is recorded, but the evidence set is still limited."

    risk_reasons = failure_reasons + missing
    if not risk_reasons:
        risk_reasons = ["No material remaining risk is recorded within the selected and executed scope."]

    next_evidence: list[str] = []
    if failure_reasons:
        next_evidence.append("Reproduce and resolve the observed failures, then rerun the original checks before adjacent regressions.")
    if unfinished:
        next_evidence.append("Execute or explicitly disposition every planned and running test.")
    if counts.get("blocked"):
        next_evidence.append("Remove the blockers or document why the blocked behavior cannot be tested safely.")
    if evidence_missing_ids:
        next_evidence.append("Attach observed actual results and reproducible evidence handles to every pass or failure.")
    if model_gaps or exclusions:
        next_evidence.append("Add decision-relevant tests for the unmapped or excluded product, API, role, state, and flow coverage.")
    if missing_channels:
        next_evidence.append("Capture the missing page evidence channels or explicitly preserve why each channel is unavailable.")
    if human_context["status"] != "recorded" and human_context["path"]:
        next_evidence.append("Use /carbon-human to record the smallest decision-changing human context; this qualifies confidence but does not block technical assessment.")
    if not next_evidence and band in {"moderate", "high"}:
        next_evidence.append("Broaden or repeat the run only if the release decision requires confidence beyond this selected scope.")
    if not next_evidence:
        next_evidence.append("Run the smallest decision-relevant executable scope and record actual results plus evidence.")

    return {
        "schema": "carbon.quality-confidence/v1",
        "confidenceInQuality": {
            "band": band,
            "basis": f"{basis} {human_context['qualifier']}",
            "scope": "Selected CARBON run and recorded evidence only",
            "isProbabilityOfCorrectness": False,
        },
        "remainingRisk": {"level": risk, "reasons": risk_reasons[:30]},
        "coverage": {
            "total": total,
            "terminal": terminal,
            "conclusive": conclusive,
            "passed": counts.get("passed", 0),
            "failed": counts.get("failed", 0),
            "blocked": counts.get("blocked", 0),
            "skipped": counts.get("skipped", 0),
            "unfinished": unfinished,
            "evidenceBacked": evidence_backed,
            "terminalPercent": round(terminal * 100 / total) if total else 0,
            "conclusivePercent": round(conclusive * 100 / total) if total else 0,
        },
        "coverageMissing": missing[:30],
        "whatWouldChangeConfidence": next_evidence[:12],
        "dimensions": dimensions,
        "dimensionOrder": list(DIMENSION_RULES),
        "humanDecisionContext": human_context,
        "interpretation": "Confidence is an evidence-qualified judgment about the tested scope, not a probability that the product is correct or defect-free.",
    }
