"""Human-first error-analysis view model."""
from __future__ import annotations

import re

from ..common import evidence_index
from ..models import HumanReportView, VisualEdge, VisualNode
from ..visualizations import cause_graph_figure


_LEAD_CLAUSE = re.compile(r"\s[—–-]\s|(?<=[.。!?])\s")
_MAX_LABEL_CHARS = 44


def _short_label(statement: str) -> str:
    """The candidate's name, taken from the opening clause of its statement.

    `causeCandidates` carries no title field: `statement` is the full claim,
    routinely several sentences long. Passed through whole it filled the
    figure's name cell with prose and ran the node text out of the drawing, so
    the figure takes the lead clause and leaves the claim to the cards below.
    """
    head = _LEAD_CLAUSE.split(statement.strip(), maxsplit=1)[0].strip()
    if len(head) <= _MAX_LABEL_CHARS:
        return head
    return head[:_MAX_LABEL_CHARS].rstrip() + "…"


def _cause_node(row: dict, leading_cause_id: str) -> VisualNode:
    # The figure names its nodes and nothing more. Both the failure sentence
    # and each candidate's disproof already have a section of their own, and
    # repeating them here printed every one of them twice.
    leading = row["id"] == leading_cause_id
    return VisualNode(
        row["id"],
        _short_label(row["statement"]),
        "candidate",
        "leading" if leading else row["confidence"],
        "",
        note="Leading cause" if leading else f'Confidence {row["confidence"]}',
    )


def _cause_edges(rows: list[dict], symptom_id: str) -> tuple[VisualEdge, ...]:
    """The chain between candidates, then the symptom each chain ends at.

    Candidates are not always competing guesses for one spot. A propagation
    chain — the extraction breaks, the error is swallowed, the empty result is
    stamped a success — needs every link to hold for the symptom to appear, and
    `downstreamOf` is where the diagnosis says so. Drawing every candidate
    straight at the symptom instead claimed they were alternatives.

    Only a candidate nothing else is downstream of reaches the symptom: an
    upstream link would otherwise be drawn as its own explanation of the
    symptom as well as a step on the way there.
    """
    known = {row["id"] for row in rows}
    upstream = {
        row["id"]: [step for step in row.get("downstreamOf", []) if step in known]
        for row in rows
    }
    has_downstream = {step for steps in upstream.values() for step in steps}
    chain = tuple(
        VisualEdge(step, row["id"], "then", "chain")
        for row in rows
        for step in upstream[row["id"]]
    )
    return chain + tuple(
        VisualEdge(row["id"], symptom_id, "may cause", "hypothesis")
        for row in rows
        if row["id"] not in has_downstream
    )


def _cause_figure(error: dict):
    symptom = VisualNode("symptom", "Observed failure", "effect", "risk", "", note="Symptom")
    rows = error.get("causeCandidates", [])
    leading_cause_id = (error.get("routing") or {}).get("leadingCauseId") or ""
    causes = tuple(_cause_node(row, leading_cause_id) for row in rows)
    return cause_graph_figure(
        nodes=(symptom, *causes),
        edges=_cause_edges(rows, symptom.id),
        title="Cause hypotheses and observed symptom",
    )


def build_error_analysis_view(data: dict) -> HumanReportView:
    error = data["errorAnalysis"]
    figure = _cause_figure(error)
    context = {
        "humanSummary": data["humanSummary"],
        "error": error,
        "narrative": error["userNarrative"],
        "causeFigure": figure,
        "evidenceIndex": evidence_index(data),
    }
    return HumanReportView(
        "error-analysis",
        "html/tasks/error-analysis.template.html",
        context,
        (figure,),
    )
