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

from ..common import evidence_index
from ..models import HumanReportView, VisualEdge, VisualNode
from ..visualizations import dependency_figure, infrastructure_figure, workflow_figure


def _component_nodes(project: dict) -> tuple[VisualNode, ...]:
    return tuple(
        VisualNode(
            id=row["id"],
            label=row["name"],
            group=(row.get("paths") or ["project"])[0].split("/", 1)[0],
            status="stable",
            detail=row["responsibility"],
            paths=tuple(row.get("paths") or ()),
        )
        for row in project.get("components", [])
    )


def _dependency_edges(project: dict) -> tuple[VisualEdge, ...]:
    return tuple(
        VisualEdge(
            source=row["fromComponentId"],
            target=row["toComponentId"],
            label=row.get("direction", "dependency"),
            kind="dependency",
        )
        for row in project.get("dependencies", [])
    )


def _infrastructure_figure(project: dict):
    """The components that hold a boundary, and what sits on the far side.

    The dependency graph answers "what calls what inside the code". Where the
    data lives and which outside service the code depends on is a different
    question with different nodes, and reading it off a component graph means
    reconstructing it from adapter paths.
    """
    components = {row["id"]: row for row in project.get("components", [])}
    nodes: list[VisualNode] = []
    edges: list[VisualEdge] = []
    seen: set[str] = set()

    def owner_node(component_id: str) -> str | None:
        row = components.get(component_id)
        if row is None:
            return None
        if row["id"] not in seen:
            seen.add(row["id"])
            nodes.append(
                VisualNode(row["id"], row["name"], "component", "stable", row["responsibility"], note="Component")
            )
        return row["id"]

    for index, row in enumerate(project.get("dataStores", []), start=1):
        store_id = f"store-{index}"
        nodes.append(
            VisualNode(store_id, row["name"], "store", "store", row.get("readBoundary", ""), note="Data store")
        )
        source = owner_node(row.get("ownerComponentId", ""))
        if source:
            edges.append(VisualEdge(source, store_id, "reads · writes", "storage"))
    for index, row in enumerate(project.get("externalSystems", []), start=1):
        system_id = f"external-{index}"
        nodes.append(
            VisualNode(system_id, row["name"], "external", "external", row.get("direction", ""), note="External system")
        )
        edges.append(VisualEdge(row.get("adapter", "adapter"), system_id, row.get("direction", ""), "external"))
        if row.get("adapter") and row["adapter"] not in seen:
            seen.add(row["adapter"])
            nodes.append(
                VisualNode(row["adapter"], row["adapter"], "adapter", "stable", "Adapter", note="Adapter")
            )
    return infrastructure_figure(
        nodes=tuple(nodes), edges=tuple(edges), title="Infrastructure and boundaries"
    )


def _workflow_figure(project: dict):
    """Each workflow's steps, as the path a request takes between components."""
    components = {row["id"]: row for row in project.get("components", [])}
    nodes: list[VisualNode] = []
    edges: list[VisualEdge] = []
    seen: set[str] = set()
    for flow in project.get("workflows", []):
        steps = sorted(flow.get("steps", []), key=lambda step: step.get("order", 0))
        for step in steps:
            component_id = step.get("componentId", "")
            if component_id and component_id not in seen:
                seen.add(component_id)
                row = components.get(component_id, {})
                nodes.append(
                    VisualNode(
                        component_id,
                        row.get("name", component_id),
                        "component",
                        "stable",
                        step.get("action", ""),
                    )
                )
        for first, second in zip(steps, steps[1:]):
            edges.append(
                VisualEdge(
                    first.get("componentId", ""),
                    second.get("componentId", ""),
                    second.get("action", "next"),
                    "workflow",
                )
            )
    return workflow_figure(nodes=tuple(nodes), edges=tuple(edges), title="Workflows")


def build_project_analysis_view(data: dict) -> HumanReportView:
    project = data["projectAnalysis"]
    architecture = dependency_figure(
        nodes=_component_nodes(project),
        edges=_dependency_edges(project),
        title="Project architecture and dependencies",
    )
    infrastructure = _infrastructure_figure(project)
    workflows = _workflow_figure(project) if project.get("workflows") else None
    context = {
        "humanSummary": data["humanSummary"],
        "project": project,
        "infrastructureFigure": infrastructure,
        "workflowFigure": workflows,
        "narrative": project["userNarrative"],
        "architectureFigure": architecture,
        "evidenceIndex": evidence_index(data),
    }
    return HumanReportView(
        task_type="project-analysis",
        template_name="html/tasks/project-analysis.template.html",
        context=context,
        figures=tuple(f for f in (architecture, infrastructure, workflows) if f),
    )
