"""Create a NetworkX graph from scan artifacts."""

from __future__ import annotations

from collections import Counter

import networkx as nx
from networkx.readwrite import json_graph

from sentra.models.artifacts import (
    CertificateFinding,
    ComponentEvidence,
    ConfigHint,
    CorrelationArtifact,
    EvidenceEdge,
    EvidenceNode,
    FirmwareEvidence,
    NetworkScanArtifact,
    ServiceHint,
)


def build_evidence_graph(
    network_scan: NetworkScanArtifact | None,
    firmware_scan: FirmwareEvidence | None,
) -> CorrelationArtifact:
    graph = nx.MultiDiGraph()

    if network_scan:
        _add_network_scan(graph, network_scan)

    if firmware_scan:
        _add_firmware_scan(graph, firmware_scan, network_scan)

    data = json_graph.node_link_data(graph)
    nodes = [
        EvidenceNode(
            id=node_data["id"],
            kind=node_data.get("kind", "unknown"),
            label=node_data.get("label", node_data["id"]),
            attributes={key: value for key, value in node_data.items() if key not in {"id", "kind", "label"}},
        )
        for node_data in data["nodes"]
    ]
    edge_records = data.get("links", data.get("edges", []))
    edges = [
        EvidenceEdge(
            source=edge_data["source"],
            target=edge_data["target"],
            relation=edge_data.get("relation", "related_to"),
            attributes={key: value for key, value in edge_data.items() if key not in {"source", "target", "relation"}},
        )
        for edge_data in edge_records
    ]
    return CorrelationArtifact(
        nodes=nodes,
        edges=edges,
        summary=summarize_graph(graph),
        insights=_build_graph_insights(graph),
    )


def summarize_graph(graph: nx.MultiDiGraph) -> dict[str, int]:
    summary = {
        "nodes": graph.number_of_nodes(),
        "edges": graph.number_of_edges(),
        "devices": 0,
        "services": 0,
        "protocols": 0,
        "findings": 0,
        "firmware": 0,
        "certificates": 0,
        "endpoints": 0,
        "software_components": 0,
        "fingerprint_inferences": 0,
        "evidence_items": 0,
        "config_hints": 0,
        "service_hints": 0,
        "extracted_artifacts": 0,
        "vulnerability_references": 0,
        "component_evidence": 0,
        "firmware_inferences": 0,
    }
    kind_map = {
        "device": "devices",
        "service": "services",
        "protocol": "protocols",
        "finding": "findings",
        "firmware": "firmware",
        "certificate": "certificates",
        "endpoint": "endpoints",
        "software_component": "software_components",
        "fingerprint_inference": "fingerprint_inferences",
        "evidence_item": "evidence_items",
        "config_hint": "config_hints",
        "service_hint": "service_hints",
        "extracted_artifact": "extracted_artifacts",
        "vulnerability_reference": "vulnerability_references",
        "component_evidence": "component_evidence",
        "firmware_inference": "firmware_inferences",
    }
    for _, attrs in graph.nodes(data=True):
        key = kind_map.get(str(attrs.get("kind", "")))
        if key:
            summary[key] += 1
    return summary


def _add_network_scan(graph: nx.MultiDiGraph, network_scan: NetworkScanArtifact) -> None:
    for device in network_scan.devices:
        device_id = f"device:{device.ip_address}"
        graph.add_node(
            device_id,
            kind="device",
            label=device.host,
            ip_address=device.ip_address,
            vendor=device.vendor,
            model=device.model,
        )
        for service in device.services:
            service_id = f"service:{device.ip_address}:{service.protocol}:{service.port}"
            graph.add_node(
                service_id,
                kind="service",
                label=f"{service.service} ({service.port}/{service.protocol})",
                service=service.service,
                protocol=service.protocol,
                port=service.port,
                banner=service.banner,
                product=service.product,
                version=service.version,
                confidence=service.confidence,
            )
            graph.add_edge(device_id, service_id, relation="hosts")
            graph.add_edge(device_id, service_id, relation="exposes")

            protocol_id = f"protocol:{service.protocol.lower()}"
            graph.add_node(protocol_id, kind="protocol", label=service.protocol.lower())
            graph.add_edge(service_id, protocol_id, relation="uses_protocol")

            for fact in service.observed_facts:
                evidence_id = f"evidence:{device.ip_address}:{service.port}:{fact.field}:{fact.value}"
                graph.add_node(
                    evidence_id,
                    kind="evidence_item",
                    label=fact.field,
                    value=fact.value,
                    confidence=fact.confidence,
                )
                graph.add_edge(service_id, evidence_id, relation="observed_in")

        for protocol in device.protocols:
            protocol_id = f"protocol:{protocol}"
            graph.add_node(protocol_id, kind="protocol", label=protocol)
            graph.add_edge(device_id, protocol_id, relation="speaks")
        for endpoint in device.endpoints:
            endpoint_id = f"endpoint:{device.ip_address}:{endpoint.url}"
            graph.add_node(endpoint_id, kind="endpoint", label=endpoint.url, confidence=endpoint.confidence)
            graph.add_edge(device_id, endpoint_id, relation="exposes")
        for certificate in device.certificates:
            certificate_id = f"certificate:{device.ip_address}:{certificate.common_name or 'unknown'}"
            graph.add_node(
                certificate_id,
                kind="certificate",
                label=certificate.common_name or certificate.subject or "certificate",
                subject=certificate.subject,
                issuer=certificate.issuer,
                confidence=certificate.confidence,
            )
            graph.add_edge(device_id, certificate_id, relation="observed_in")
        for component in device.software_components:
            component_id = f"software:{device.ip_address}:{component.normalized_name}"
            graph.add_node(
                component_id,
                kind="software_component",
                label=component.name,
                normalized_name=component.normalized_name,
                version=component.version,
                confidence=component.confidence,
            )
            graph.add_edge(device_id, component_id, relation="depends_on")
        for match in device.vulnerability_matches:
            vuln_id = f"vuln:{device.ip_address}:{match.cve_id}"
            graph.add_node(
                vuln_id,
                kind="vulnerability_reference",
                label=match.cve_id,
                severity=match.severity,
                confidence=match.confidence,
                category=match.category,
                rationale=match.rationale,
            )
            graph.add_edge(device_id, vuln_id, relation="affects")
        for fingerprint in device.fingerprint_matches:
            fingerprint_id = f"fingerprint:{device.ip_address}:{fingerprint}"
            graph.add_node(
                fingerprint_id,
                kind="fingerprint_inference",
                label=fingerprint,
                confidence=device.fingerprint_confidence,
            )
            graph.add_edge(device_id, fingerprint_id, relation="inferred_from")
        for finding in device.findings:
            finding_id = f"finding:{device.ip_address}:{finding.title}"
            graph.add_node(finding_id, kind="finding", label=finding.title, severity=finding.severity)
            graph.add_edge(device_id, finding_id, relation="has_finding")


def _add_firmware_scan(
    graph: nx.MultiDiGraph,
    firmware_scan: FirmwareEvidence,
    network_scan: NetworkScanArtifact | None,
) -> None:
    firmware_id = f"firmware:{firmware_scan.sha256[:12]}"
    graph.add_node(
        firmware_id,
        kind="firmware",
        label=firmware_scan.path,
        sha256=firmware_scan.sha256,
        extraction_provider=firmware_scan.extraction_metadata.provider,
        extracted_files=len(firmware_scan.extracted_artifacts),
    )

    for artifact in firmware_scan.extracted_artifacts:
        artifact_id = f"extracted:{firmware_scan.sha256[:12]}:{artifact.relative_path}"
        graph.add_node(
            artifact_id,
            kind="extracted_artifact",
            label=artifact.relative_path,
            size_bytes=artifact.size_bytes,
            source=artifact.source,
            media_type=artifact.media_type,
            depth=artifact.depth,
        )
        graph.add_edge(firmware_id, artifact_id, relation="contains")
    for component in firmware_scan.software_components:
        component_id = f"software:firmware:{component.normalized_name}"
        graph.add_node(
            component_id,
            kind="software_component",
            label=component.name,
            normalized_name=component.normalized_name,
            version=component.version,
            confidence=component.confidence,
        )
        graph.add_edge(firmware_id, component_id, relation="depends_on")
    for evidence in firmware_scan.component_evidence:
        _add_component_evidence(graph, firmware_id, evidence)
    for certificate in firmware_scan.certificates:
        certificate_id = f"certificate:firmware:{certificate.common_name or certificate.subject or 'unknown'}"
        graph.add_node(
            certificate_id,
            kind="certificate",
            label=certificate.common_name or certificate.subject or "certificate",
            subject=certificate.subject,
            issuer=certificate.issuer,
            confidence=certificate.confidence,
        )
        graph.add_edge(firmware_id, certificate_id, relation="observed_in")
    for finding in firmware_scan.certificate_findings:
        _add_certificate_finding(graph, firmware_id, finding)
    for hint in firmware_scan.config_hints:
        _add_config_hint(graph, firmware_id, hint)
    for hint in firmware_scan.service_hints:
        _add_service_hint(graph, firmware_id, hint)
    for fact in firmware_scan.observed_facts:
        evidence_id = f"evidence:firmware:{fact.field}:{fact.value}"
        graph.add_node(evidence_id, kind="evidence_item", label=fact.field, value=fact.value, confidence=fact.confidence)
        graph.add_edge(firmware_id, evidence_id, relation="observed_in")
    for hint in firmware_scan.inference_hints:
        inference_id = f"inference:firmware:{hint.field}:{hint.value}"
        graph.add_node(
            inference_id,
            kind="fingerprint_inference" if hint.field in {"vendor", "product"} else "firmware_inference",
            label=hint.value,
            confidence=hint.confidence,
            rationale=hint.rationale,
        )
        graph.add_edge(firmware_id, inference_id, relation="inferred_from")
    for match in firmware_scan.vulnerability_matches:
        vuln_id = f"vuln:firmware:{match.cve_id}"
        graph.add_node(
            vuln_id,
            kind="vulnerability_reference",
            label=match.cve_id,
            severity=match.severity,
            confidence=match.confidence,
            category=match.category,
            rationale=match.rationale,
        )
        graph.add_edge(firmware_id, vuln_id, relation="affects")
    for finding in firmware_scan.findings:
        finding_id = f"finding:firmware:{finding.title}"
        graph.add_node(finding_id, kind="finding", label=finding.title, severity=finding.severity)
        graph.add_edge(firmware_id, finding_id, relation="has_finding")

    if network_scan and firmware_scan.suspected_vendors:
        for device in network_scan.devices:
            if device.vendor and device.vendor in firmware_scan.suspected_vendors:
                graph.add_edge(f"device:{device.ip_address}", firmware_id, relation="likely_runs")
            _correlate_device_with_firmware(graph, device.ip_address, device.services, firmware_scan)


def _add_component_evidence(graph: nx.MultiDiGraph, firmware_id: str, evidence: ComponentEvidence) -> None:
    component_id = f"software:firmware:{evidence.component.normalized_name}"
    evidence_id = f"evidence:firmware:component:{evidence.component.normalized_name}:{evidence.source_path}"
    graph.add_node(
        evidence_id,
        kind="component_evidence",
        label=evidence.evidence_type,
        value=evidence.detail,
        confidence=evidence.confidence,
    )
    graph.add_edge(component_id, evidence_id, relation="observed_in")
    graph.add_edge(firmware_id, evidence_id, relation="related_to")
    extracted_id = f"extracted:{firmware_id.split(':', 1)[1]}:{evidence.source_path}"
    if graph.has_node(extracted_id):
        graph.add_edge(extracted_id, evidence_id, relation="contains_evidence")


def _add_config_hint(graph: nx.MultiDiGraph, firmware_id: str, hint: ConfigHint) -> None:
    hint_id = f"config:{hint.path}:{hint.key}"
    graph.add_node(
        hint_id,
        kind="config_hint",
        label=f"{hint.key} ({hint.path})",
        category=hint.category,
        confidence=hint.confidence,
    )
    graph.add_edge(firmware_id, hint_id, relation="observed_in")
    extracted_id = f"extracted:{firmware_id.split(':', 1)[1]}:{hint.path}"
    if graph.has_node(extracted_id):
        graph.add_edge(extracted_id, hint_id, relation="contains_evidence")


def _add_service_hint(graph: nx.MultiDiGraph, firmware_id: str, hint: ServiceHint) -> None:
    hint_id = f"service-hint:{hint.service}:{hint.source_path}"
    graph.add_node(
        hint_id,
        kind="service_hint",
        label=f"{hint.service} ({hint.source_path})",
        confidence=hint.confidence,
        rationale=hint.rationale,
        source_kind=hint.source_kind,
    )
    graph.add_edge(firmware_id, hint_id, relation="likely_runs")
    extracted_id = f"extracted:{firmware_id.split(':', 1)[1]}:{hint.source_path}"
    if graph.has_node(extracted_id):
        graph.add_edge(extracted_id, hint_id, relation="contains_evidence")


def _add_certificate_finding(graph: nx.MultiDiGraph, firmware_id: str, finding: CertificateFinding) -> None:
    finding_id = f"certificate-finding:{finding.path}:{finding.common_name or finding.subject or 'unknown'}"
    graph.add_node(
        finding_id,
        kind="certificate",
        label=finding.common_name or finding.subject or "certificate",
        confidence=finding.confidence,
        issuer=finding.issuer,
        path=finding.path,
    )
    graph.add_edge(firmware_id, finding_id, relation="observed_in")
    extracted_id = f"extracted:{firmware_id.split(':', 1)[1]}:{finding.path}"
    if graph.has_node(extracted_id):
        graph.add_edge(extracted_id, finding_id, relation="contains_evidence")


def _correlate_device_with_firmware(
    graph: nx.MultiDiGraph,
    device_ip: str,
    services,
    firmware_scan: FirmwareEvidence,
) -> None:  # type: ignore[no-untyped-def]
    device_id = f"device:{device_ip}"
    service_names = {service.service.lower() for service in services}
    service_products = {
        (service.normalized_product or service.product or "").lower()
        for service in services
        if service.normalized_product or service.product
    }
    for hint in firmware_scan.service_hints:
        if hint.service.lower() in service_names:
            hint_id = f"service-hint:{hint.service}:{hint.source_path}"
            if graph.has_node(hint_id):
                graph.add_edge(device_id, hint_id, relation="matches_firmware_signal")
    for component in firmware_scan.software_components:
        component_id = f"software:firmware:{component.normalized_name}"
        if component.normalized_name.lower() in service_products and graph.has_node(component_id):
            graph.add_edge(device_id, component_id, relation="matches_component")


def _build_graph_insights(graph: nx.MultiDiGraph) -> dict[str, object]:
    node_counts = Counter(str(attrs.get("kind", "unknown")) for _, attrs in graph.nodes(data=True))
    relation_counts = Counter(str(attrs.get("relation", "related_to")) for _, _, attrs in graph.edges(data=True))
    protocol_counts = Counter(
        str(attrs.get("label", "unknown"))
        for _, attrs in graph.nodes(data=True)
        if str(attrs.get("kind", "")) == "protocol"
    )
    visual_group_counts = Counter(
        {
            "device": "device",
            "service": "service",
            "endpoint": "service",
            "protocol": "protocol",
            "firmware": "firmware",
            "extracted_artifact": "firmware",
            "software_component": "firmware",
            "component_evidence": "firmware",
        }.get(str(attrs.get("kind", "")), "intelligence")
        for _, attrs in graph.nodes(data=True)
    )
    risky_kinds = {
        kind: count
        for kind, count in sorted(node_counts.items(), key=lambda item: (-item[1], item[0]))
        if kind in {"finding", "vulnerability_reference", "service_hint", "config_hint", "certificate", "component_evidence"}
    }
    weak_paths = _rank_weak_paths(graph)
    return {
        "densest_node_kinds": [
            {"kind": kind, "count": count}
            for kind, count in sorted(node_counts.items(), key=lambda item: (-item[1], item[0]))[:5]
        ],
        "top_relations": [
            {"relation": relation, "count": count}
            for relation, count in sorted(relation_counts.items(), key=lambda item: (-item[1], item[0]))[:6]
        ],
        "top_protocols": [
            {"protocol": protocol, "count": count}
            for protocol, count in sorted(protocol_counts.items(), key=lambda item: (-item[1], item[0]))[:5]
        ],
        "visualization_groups": [
            {"group": group, "count": count}
            for group, count in sorted(visual_group_counts.items(), key=lambda item: (-item[1], item[0]))
        ],
        "risk_signal_kinds": risky_kinds,
        "vulnerability_categories": dict(
            sorted(
                Counter(
                    str(attrs.get("category", "unknown"))
                    for _, attrs in graph.nodes(data=True)
                    if str(attrs.get("kind", "")) == "vulnerability_reference"
                ).items()
            )
        ),
        "fingerprint_confidence_max": max(
            (
                float(attrs.get("confidence", 0.0))
                for _, attrs in graph.nodes(data=True)
                if str(attrs.get("kind", "")) == "fingerprint_inference"
            ),
            default=0.0,
        ),
        "correlated_firmware_signals": sum(
            count
            for kind, count in node_counts.items()
            if kind in {"config_hint", "service_hint", "certificate", "extracted_artifact", "component_evidence", "firmware_inference"}
        ),
        "ranked_weak_paths": weak_paths,
        "top_weak_path": weak_paths[0] if weak_paths else {},
        "highlighted_path_targets": [str(item.get("terminal_finding", "")) for item in weak_paths[:5]],
    }


def _rank_weak_paths(graph: nx.MultiDiGraph) -> list[dict[str, object]]:
    artifact = CorrelationArtifact(
        nodes=[
            EvidenceNode(id=node_id, kind=str(attrs.get("kind", "unknown")), label=str(attrs.get("label", node_id)), attributes={key: value for key, value in attrs.items() if key not in {"kind", "label"}})
            for node_id, attrs in graph.nodes(data=True)
        ],
        edges=[
            EvidenceEdge(source=source, target=target, relation=str(attrs.get("relation", "related_to")), attributes={key: value for key, value in attrs.items() if key != "relation"})
            for source, target, attrs in graph.edges(data=True)
        ],
    )
    from sentra.reporting.renderer import _build_weak_paths  # local import avoids a top-level cycle

    return _build_weak_paths(artifact)

