"""Deterministic scan diff helpers."""

from __future__ import annotations

import json
from html import escape
from pathlib import Path

from sentra.models import CorrelationArtifact, FirmwareEvidence, NetworkScanArtifact


def build_scan_diff(
    previous: NetworkScanArtifact | None,
    current: NetworkScanArtifact | None,
    output_format: str,
    *,
    previous_firmware: FirmwareEvidence | None = None,
    current_firmware: FirmwareEvidence | None = None,
    previous_correlation: CorrelationArtifact | None = None,
    current_correlation: CorrelationArtifact | None = None,
) -> str:
    previous_hosts = _device_index(previous)
    current_hosts = _device_index(current)

    added = sorted(set(current_hosts) - set(previous_hosts))
    removed = sorted(set(previous_hosts) - set(current_hosts))
    changed = sorted(
        host
        for host in set(current_hosts).intersection(previous_hosts)
        if current_hosts[host] != previous_hosts[host]
    )
    changed_details = [
        {
            "host": host,
            "before_services": list(previous_hosts[host][0]),
            "after_services": list(current_hosts[host][0]),
            "before_findings": list(previous_hosts[host][1]),
            "after_findings": list(current_hosts[host][1]),
            "new_findings": sorted(set(current_hosts[host][1]) - set(previous_hosts[host][1])),
            "removed_findings": sorted(set(previous_hosts[host][1]) - set(current_hosts[host][1])),
        }
        for host in changed
    ]

    firmware_changes = _firmware_diff(previous_firmware, current_firmware)
    correlation_changes = _correlation_diff(previous_correlation, current_correlation)

    payload = {
        "added_hosts": added,
        "removed_hosts": removed,
        "changed_hosts": changed,
        "changed_details": changed_details,
        "firmware_changes": firmware_changes,
        "correlation_changes": correlation_changes,
        "before_count": len(previous_hosts),
        "after_count": len(current_hosts),
    }

    if output_format == "json":
        return json.dumps(payload, indent=2, sort_keys=True)
    if output_format == "markdown":
        lines = [
            "# Sentra Scan Diff",
            "",
            "## Summary",
            "",
            f"- Before host count: {payload['before_count']}",
            f"- After host count: {payload['after_count']}",
            f"- Added hosts: {len(added)}",
            f"- Removed hosts: {len(removed)}",
            f"- Changed hosts: {len(changed)}",
            "",
        ]
        for key in ("added_hosts", "removed_hosts", "changed_hosts"):
            lines.append(f"## {key.replace('_', ' ').title()}")
            values = payload[key]
            if values:
                lines.extend(f"- {value}" for value in values)
            else:
                lines.append("- None")
            lines.append("")
        lines.extend(["## Changed Host Details", ""])
        if changed_details:
            for item in changed_details:
                lines.append(f"### {item['host']}")
                lines.append(f"- Before services: {', '.join(item['before_services']) or 'None'}")
                lines.append(f"- After services: {', '.join(item['after_services']) or 'None'}")
                lines.append(f"- Before findings: {', '.join(item['before_findings']) or 'None'}")
                lines.append(f"- After findings: {', '.join(item['after_findings']) or 'None'}")
                lines.append(f"- New findings: {', '.join(item['new_findings']) or 'None'}")
                lines.append(f"- Removed findings: {', '.join(item['removed_findings']) or 'None'}")
                lines.append("")
        else:
            lines.append("- None")
            lines.append("")
        lines.extend(["## Firmware Analysis Changes", ""])
        for label, values in firmware_changes.items():
            if isinstance(values, list):
                lines.append(f"- {label.replace('_', ' ').title()}: {', '.join(values) or 'None'}")
            else:
                lines.append(f"- {label.replace('_', ' ').title()}: {values}")
        lines.extend(["", "## Correlated Evidence Changes", ""])
        for label, values in correlation_changes.items():
            if isinstance(values, list):
                lines.append(f"- {label.replace('_', ' ').title()}: {', '.join(values) or 'None'}")
            else:
                lines.append(f"- {label.replace('_', ' ').title()}: {values}")
        lines.append("")
        return "\n".join(lines).rstrip() + "\n"
    if output_format == "html":
        return _to_html(payload)
    return (
        "Sentra Scan Diff\n"
        f"before_count: {payload['before_count']}\n"
        f"after_count: {payload['after_count']}\n"
        f"added_hosts: {len(added)} ({', '.join(added) or 'None'})\n"
        f"removed_hosts: {len(removed)} ({', '.join(removed) or 'None'})\n"
        f"changed_hosts: {len(changed)} ({', '.join(changed) or 'None'})\n"
        f"firmware_signal_changes: {firmware_changes['signal_delta_count']}\n"
        f"path_ranking_changes: {correlation_changes['changed_path_rankings']}\n"
    )


def write_diff_report(destination: Path, content: str) -> None:
    destination.parent.mkdir(parents=True, exist_ok=True)
    destination.write_text(content, encoding="utf-8")


def _to_html(payload: dict[str, object]) -> str:
    def list_items(values: list[str]) -> str:
        if not values:
            return "<li>None</li>"
        return "".join(f"<li>{escape(value)}</li>" for value in values)

    changed_rows = "".join(
        "<tr>"
        f"<td>{escape(item['host'])}</td>"
        f"<td>{escape(', '.join(item['before_services']) or 'None')}</td>"
        f"<td>{escape(', '.join(item['after_services']) or 'None')}</td>"
        f"<td>{escape(', '.join(item['new_findings']) or 'None')}</td>"
        f"<td>{escape(', '.join(item['removed_findings']) or 'None')}</td>"
        "</tr>"
        for item in payload["changed_details"]
    ) or "<tr><td colspan='5'>No changed hosts.</td></tr>"
    firmware_rows = "".join(
        f"<tr><td>{escape(label.replace('_', ' ').title())}</td><td>{escape(', '.join(values) if isinstance(values, list) else str(values))}</td></tr>"
        for label, values in payload["firmware_changes"].items()
    )
    correlation_rows = "".join(
        f"<tr><td>{escape(label.replace('_', ' ').title())}</td><td>{escape(', '.join(values) if isinstance(values, list) else str(values))}</td></tr>"
        for label, values in payload["correlation_changes"].items()
    )
    return f"""<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Sentra Scan Diff</title>
  <style>
    :root {{ --bg:#f5f3ee; --panel:#fffdfa; --ink:#1d241f; --muted:#5f6b63; --line:#d4d0c7; --accent:#174c3c; }}
    * {{ box-sizing:border-box; }} body {{ margin:0; background:linear-gradient(180deg,#faf7f1 0%,var(--bg) 100%); color:var(--ink); font-family:"Segoe UI","Aptos",Arial,sans-serif; }}
    main {{ max-width:1080px; margin:0 auto; padding:2rem 1.25rem 4rem; }} section {{ background:var(--panel); border:1px solid var(--line); border-radius:18px; padding:1.25rem; margin-bottom:1rem; }}
    .metric-grid {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:.75rem; }} .metric-card {{ border:1px solid var(--line); border-radius:14px; padding:.9rem; background:linear-gradient(180deg,#fff,#f7f6f2); }}
    .metric-label {{ color:var(--muted); font-size:.82rem; }} .metric-value {{ color:var(--accent); font-size:1.45rem; font-weight:700; }}
    .three-col {{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem; }} ul {{ margin:.2rem 0 0; padding-left:1rem; }}
    table {{ width:100%; border-collapse:collapse; }} th,td {{ border-bottom:1px solid var(--line); padding:.65rem .45rem; text-align:left; vertical-align:top; }} th {{ color:var(--muted); text-transform:uppercase; font-size:.82rem; }}
    @media (max-width:900px) {{ .three-col {{ grid-template-columns:1fr; }} }}
  </style>
</head>
<body>
  <main>
    <section>
      <h1>Sentra Scan Diff</h1>
      <div class="metric-grid">
        <div class="metric-card"><div class="metric-label">Before Hosts</div><div class="metric-value">{payload['before_count']}</div></div>
        <div class="metric-card"><div class="metric-label">After Hosts</div><div class="metric-value">{payload['after_count']}</div></div>
        <div class="metric-card"><div class="metric-label">Added Hosts</div><div class="metric-value">{len(payload['added_hosts'])}</div></div>
        <div class="metric-card"><div class="metric-label">Removed Hosts</div><div class="metric-value">{len(payload['removed_hosts'])}</div></div>
        <div class="metric-card"><div class="metric-label">Changed Hosts</div><div class="metric-value">{len(payload['changed_hosts'])}</div></div>
      </div>
    </section>
    <section class="three-col">
      <div><h2>Added Hosts</h2><ul>{list_items(payload['added_hosts'])}</ul></div>
      <div><h2>Removed Hosts</h2><ul>{list_items(payload['removed_hosts'])}</ul></div>
      <div><h2>Changed Hosts</h2><ul>{list_items(payload['changed_hosts'])}</ul></div>
    </section>
    <section><h2>Changed Host Details</h2><table><thead><tr><th>Host</th><th>Before Services</th><th>After Services</th><th>New Findings</th><th>Removed Findings</th></tr></thead><tbody>{changed_rows}</tbody></table></section>
    <section><h2>Firmware Analysis Changes</h2><table><thead><tr><th>Signal</th><th>Change</th></tr></thead><tbody>{firmware_rows}</tbody></table></section>
    <section><h2>Correlated Evidence Changes</h2><table><thead><tr><th>Signal</th><th>Change</th></tr></thead><tbody>{correlation_rows}</tbody></table></section>
  </main>
</body>
</html>
"""


def _device_index(artifact: NetworkScanArtifact | None) -> dict[str, tuple[tuple[str, ...], tuple[str, ...]]]:
    if artifact is None:
        return {}
    index: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {}
    for device in artifact.devices:
        services = tuple(sorted(f"{service.service}:{service.port}/{service.protocol}" for service in device.services))
        findings = tuple(sorted(finding.title for finding in device.findings))
        index[device.ip_address] = (services, findings)
    return index


def _firmware_diff(previous: FirmwareEvidence | None, current: FirmwareEvidence | None) -> dict[str, object]:
    before_findings = {finding.title for finding in previous.findings} if previous else set()
    after_findings = {finding.title for finding in current.findings} if current else set()
    before_config = {f"{hint.key}:{hint.path}" for hint in previous.config_hints} if previous else set()
    after_config = {f"{hint.key}:{hint.path}" for hint in current.config_hints} if current else set()
    before_service_names = {hint.service for hint in previous.service_hints} if previous else set()
    after_service_names = {hint.service for hint in current.service_hints} if current else set()
    before_services = {f"{hint.service}:{hint.source_path}" for hint in previous.service_hints} if previous else set()
    after_services = {f"{hint.service}:{hint.source_path}" for hint in current.service_hints} if current else set()
    before_components = {_component_signature(component) for component in previous.software_components} if previous else set()
    after_components = {_component_signature(component) for component in current.software_components} if current else set()
    before_certs = {item.path for item in previous.certificate_findings} if previous else set()
    after_certs = {item.path for item in current.certificate_findings} if current else set()
    before_artifacts = {item.relative_path for item in previous.extracted_artifacts} if previous else set()
    after_artifacts = {item.relative_path for item in current.extracted_artifacts} if current else set()
    before_provider = previous.extraction_metadata.provider if previous else ""
    after_provider = current.extraction_metadata.provider if current else ""
    changed_provider = f"{before_provider or 'none'} -> {after_provider or 'none'}" if before_provider != after_provider else "unchanged"
    signal_delta_count = sum(
        len(values)
        for values in [
            sorted(after_config - before_config),
            sorted(before_config - after_config),
            sorted(after_services - before_services),
            sorted(before_services - after_services),
            sorted(after_components - before_components),
            sorted(before_components - after_components),
            sorted(after_certs - before_certs),
            sorted(before_certs - after_certs),
            sorted(after_artifacts - before_artifacts),
            sorted(before_artifacts - after_artifacts),
        ]
    )
    return {
        "new_findings": sorted(after_findings - before_findings),
        "removed_findings": sorted(before_findings - after_findings),
        "new_config_hints": sorted(after_config - before_config),
        "removed_config_hints": sorted(before_config - after_config),
        "new_service_hints": sorted(after_service_names - before_service_names),
        "removed_service_hints": sorted(before_service_names - after_service_names),
        "new_service_hint_signals": sorted(after_services - before_services),
        "removed_service_hint_signals": sorted(before_services - after_services),
        "new_components": sorted(after_components - before_components),
        "removed_components": sorted(before_components - after_components),
        "new_certificate_findings": sorted(after_certs - before_certs),
        "removed_certificate_findings": sorted(before_certs - after_certs),
        "new_extracted_paths": sorted(after_artifacts - before_artifacts),
        "removed_extracted_paths": sorted(before_artifacts - after_artifacts),
        "changed_extraction_provider": changed_provider,
        "signal_delta_count": signal_delta_count,
    }


def _correlation_diff(
    previous: CorrelationArtifact | None,
    current: CorrelationArtifact | None,
) -> dict[str, object]:
    before_nodes = {node.id for node in previous.nodes} if previous else set()
    after_nodes = {node.id for node in current.nodes} if current else set()
    before_paths = _weak_path_titles(previous)
    after_paths = _weak_path_titles(current)
    return {
        "new_correlated_evidence": sorted(after_nodes - before_nodes)[:10],
        "removed_correlated_evidence": sorted(before_nodes - after_nodes)[:10],
        "changed_path_rankings": int(before_paths != after_paths),
        "before_path_ranking": before_paths,
        "after_path_ranking": after_paths,
        "new_path_entries": sorted(set(after_paths) - set(before_paths)),
        "removed_path_entries": sorted(set(before_paths) - set(after_paths)),
    }


def _weak_path_titles(correlation: CorrelationArtifact | None) -> list[str]:
    if correlation is None:
        return []
    ranked = correlation.insights.get("ranked_weak_paths", []) if correlation.insights else []
    if ranked:
        return [str(item.get("path", "")) for item in ranked[:8]]
    fallback = [
        node.label
        for node in correlation.nodes
        if node.kind in {"finding", "vulnerability_reference", "config_hint", "service_hint", "certificate"}
    ]
    return sorted(fallback)[:8]


def _component_signature(component) -> str:  # type: ignore[no-untyped-def]
    return component.normalized_name if not component.version else f"{component.normalized_name}:{component.version}"

