
const ARCH_NODES = [
  {
    id: "ecu", group: "car", x: 60, y: 160,
    label: "ECU / CAN Bus", sub: "Vehicle systems",
    color: "#D6AB39",
    details: {
      title: "ECU & CAN Bus",
      body: "All vehicle subsystems publish data on the 500 kbps CAN bus. Messages are defined in a private DBC file that maps CAN IDs to physical signals like SOC, motor temperature, and throttle position.",
      specs: ["500 kbps bitrate", "DBC-defined signals", "Category tags: VCU, BMS, INV, etc."],
      links: [
        { label: "Custom BMS '25", url: "https://github.com/Western-Formula-Racing/Custom-BMS_25" },
        { label: "ECU '25", url: "https://github.com/Western-Formula-Racing/ECU_25" },
      ],
    },
  },
  {
    id: "hat", group: "car", x: 210, y: 160,
    label: "CAN HAT", sub: "MCP2517FD",
    color: "#a855f7",
    details: {
      title: "MCP2517FD CAN HAT",
      body: "An SPI-connected CAN FD controller with a 20 MHz crystal oscillator and MCP2562FDT-HSN transceiver. Mounted on the car Raspberry Pi, it feeds raw CAN frames to SocketCAN (can0).",
      specs: ["MCP2517FD controller", "20 MHz crystal", "MCP2562FDT-HSN transceiver", "SPI interface to RPi"],
    },
  },
  {
    id: "rpi_car", group: "car", x: 360, y: 160,
    label: "Car Raspberry Pi", sub: "UTS — car mode",
    color: "#a855f7",
    details: {
      title: "Car Raspberry Pi",
      body: "Runs the Universal Telemetry Software in car mode, using systemd. Reads CAN frames, batches 20 messages per 50 ms burst, and transmits via UDP. A 60-second ring buffer enables TCP retransmission recovery for any dropped packets.",
      specs: ["Raspberry Pi 4/5", "Ubuntu + SocketCAN", "UDP batch: 20 msg / 50 ms", "TCP ring buffer: 60 sec", "Docker containerised"],
    },
  },
  {
    id: "radio", group: "link", x: 510, y: 160,
    label: "Ubiquiti Radio", sub: "5 GHz link",
    color: "#22d3ee",
    details: {
      title: "Ubiquiti 5 GHz Radio Link",
      body: "Point-to-point 5 GHz RF link between the car and the base station. Carries both CAN telemetry (UDP/TCP) and live H.264 video (RTSP) over the same link. The UDP + TCP dual-protocol design tolerates intermittent packet loss gracefully.",
      specs: ["5 GHz band", "UDP :5005 (CAN telemetry)", "TCP :5006 (packet recovery)", "RTSP :8554 (onboard video input)", "WebRTC WHEP :8889 (base station side video consumption)"],
    },
  },
  {
    id: "rpi_base", group: "base", x: 660, y: 160,
    label: "Base Station", sub: "UTS — base mode",
    color: "#a855f7",
    details: {
      title: "Base Station Laptop or Raspberry Pi",
      body: "Runs UTS in base mode (no can0 detected). Receives UDP CAN frames, requests TCP retransmission for any gaps, decodes and publishes batches to Redis. Also writes directly to server TimescaleDB over the network and serves a status HTTP page.",
      specs: ["Auto role detection", "Redis PUBLISH can_messages", "TimescaleDB direct write", "Status page :8080", "WebSocket bridge :9080"],
    },
  },
  {
    id: "redis", group: "base", x: 810, y: 100,
    label: "Redis", sub: "Pub/Sub broker",
    color: "#ef4444",
    details: {
      title: "Redis Pub/Sub",
      body: "Acts as the internal message bus on the base station. UTS publishes CAN message batches to the can_messages channel; the WebSocket bridge subscribes and fans out to all connected PECAN clients in real time.",
      specs: ["Channel: can_messages", "Channel: system_stats", "In-memory, low-latency", "Runs in Docker"],
    },
  },
  {
    id: "ws", group: "base", x: 810, y: 220,
    label: "WebSocket Bridge", sub: "websocket_bridge.py",
    color: "#a855f7",
    details: {
      title: "WebSocket Bridge",
      body: "Subscribes to Redis and broadcasts JSON-encoded CAN message batches over WebSocket (:9080 / :9443). PECAN auto-connects based on deployment context — cloud demo, localhost dev, or car hotspot (192.168.x.x).",
      specs: ["Port :9080 (ws) / :9443 (wss)", "5× reconnect with linear backoff", "JSON array batches", "Auto-protocol detection"],
    },
  },
  {
    id: "pecan", group: "browser", x: 990, y: 100,
    label: "PECAN Dashboard", sub: "React · TypeScript",
    color: "#D6AB39",
    details: {
      title: "PECAN Dashboard",
      body: "A React 19 + TypeScript SPA hosted on GitHub Pages. Receives live CAN frames via WebSocket, decodes them with the candied DBC library, and writes to a hot/cold data pipeline. Features a timeline scrubber, Plotly charts, custom monitor builder, and accumulator monitor.",
      specs: ["React 19 + Vite + Tailwind CSS v4", "candied DBC decoder", "Hot buffer: last 5 min", "Cold store: up to 1 hr (OPFS)", "Plotly.js charts", "GitHub Pages hosted"],
    },
  },
  {
    id: "timescale", group: "server", x: 990, y: 225,
    label: "TimescaleDB", sub: "Time-series database",
    color: "#22d3ee",
    details: {
      title: "TimescaleDB",
      body: "PostgreSQL extension optimised for time-series data. The base station UTS writes decoded CAN frames directly over the network. Enables long-term historical analysis and powers Grafana dashboards for post-session review.",
      specs: ["PostgreSQL + TimescaleDB", "Direct write from base UTS", "Hypertable partitioning", "VPS-hosted server stack"],
    },
  },
  {
    id: "grafana", group: "server", x: 1140, y: 225,
    label: "Grafana", sub: "Analytics",
    color: "#f97316",
    details: {
      title: "Grafana Analytics",
      body: "Visualises historical data from TimescaleDB. Pre-built dashboards cover lap-by-lap comparisons, long-run battery degradation, and drivetrain health. Cloudflare Workers proxy provides public access without exposing internal ports.",
      specs: ["TimescaleDB data source", "Pre-built racing dashboards", "Cloudflare Workers proxy", "Port :8087"],
    },
  },
  {
    id: "flight", group: "car", x: 360, y: 300,
    label: "Flight Recorder", sub: "ESP32 / RPi PWA",
    color: "#f97316",
    details: {
      title: "Flight Recorder",
      body: "A Progressive Web App deployed on Cloudflare Pages. A lightweight CAN to WebSocket relay runs directly on an ESP32 or RPi on the car. A phone connected to the car's hotspot will store CAN messages to IndexedDB during a run, then batch-uploads to the server TimescaleDB over internet post-session. No SD card pull required.",
      specs: ["PWA on Cloudflare Pages", "IndexedDB storage", "ESP32 or RPi compatible", "POST /api/can-frames/batch", "PECAN pipeline without major modifications"],
    },
  },
  {
    id: "downloader", group: "server", x: 1140, y: 100,
    label: "Data Downloader", sub: "FastAPI",
    color: "#22d3ee",
    details: {
      title: "Data Downloader API",
      body: "FastAPI service that accepts batch CAN frame uploads from the Flight Recorder and serves data-query endpoints. Decodes frames, inserts into TimescaleDB, and supports filtered exports for offline analysis.",
      specs: ["FastAPI (Python)", "POST /api/can-frames/batch", "Query builder", "VPS server stack"],
    },
  },
];

const ARCH_EDGES = [
  { from: "ecu", to: "hat", color: "#D6AB39" },
  { from: "hat", to: "rpi_car", color: "#a855f7" },
  { from: "rpi_car", to: "radio", color: "#a855f7", dashed: true },
  { from: "radio", to: "rpi_base", color: "#22d3ee", dashed: true },
  { from: "rpi_base", to: "redis", color: "#a855f7" },
  { from: "rpi_base", to: "ws", color: "#a855f7" },
  { from: "rpi_base", to: "timescale", color: "#22d3ee" },
  { from: "redis", to: "ws", color: "#ef4444" },
  { from: "ws", to: "pecan", color: "#D6AB39" },
  { from: "timescale", to: "grafana", color: "#22d3ee" },
  { from: "timescale", to: "downloader", color: "#22d3ee" },
  { from: "flight", to: "downloader", color: "#f97316", dashed: true },
  { from: "rpi_car", to: "flight", color: "#f97316" },
];

function getNodeById(id) { return ARCH_NODES.find(n => n.id === id); }

function ArchEdges({ nodes, highlight }) {
  const W = 1260, H = 420;
  return (
    <svg viewBox={`0 0 ${W} ${H}`} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none", overflow: "visible" }}>
      {ARCH_EDGES.map((e, i) => {
        const a = getNodeById(e.from), b = getNodeById(e.to);
        if (!a || !b) return null;
        const isHl = highlight === e.from || highlight === e.to;
        return (
          <line key={i}
            x1={a.x} y1={a.y} x2={b.x} y2={b.y}
            stroke={e.color} strokeWidth={isHl ? 2 : 1}
            opacity={isHl ? 0.8 : 0.22}
            strokeDasharray={e.dashed ? "6 4" : "none"}
            style={{ transition: "opacity 0.2s, stroke-width 0.2s" }}
          />
        );
      })}
    </svg>
  );
}

const GROUP_LABELS = {
  car: { label: "CAR SIDE", color: "#D6AB39" },
  link: { label: "RF LINK", color: "#22d3ee" },
  base: { label: "BASE STATION", color: "#a855f7" },
  server: { label: "SERVER / BROWSER", color: "#f97316" },
  browser: { label: "BROWSER", color: "#D6AB39" },
};

function Architecture() {
  const [selected, setSelected] = React.useState(null);
  const node = selected ? ARCH_NODES.find(n => n.id === selected) : null;

  return (
    <section id="architecture" style={{ padding: "100px 2rem", background: "rgba(0,0,0,0.2)" }}>
      <div style={{ maxWidth: 1280, margin: "0 auto" }}>
        <SectionHeader tag="02 / Architecture" title="System Architecture" subtitle="Click any node to explore its role in the data pipeline — from raw CAN frames on the car to Grafana dashboards on the server." />

        <div style={{ position: "relative", marginTop: 48, overflowX: "auto" }}>
          <div style={{ position: "relative", minWidth: 900 }}>
            {/* SVG edges */}
            <div style={{ position: "absolute", inset: 0, width: "100%", height: 380 }}>
              <ArchEdges nodes={ARCH_NODES} highlight={selected} />
            </div>

            {/* Nodes */}
            <div style={{ position: "relative", height: 380 }}>
              {ARCH_NODES.map(n => {
                const isSelected = selected === n.id;
                const pct = x => `${(x / 1260) * 100}%`;
                const pctY = y => `${(y / 420) * 100}%`;
                return (
                  <button
                    key={n.id}
                    onClick={() => setSelected(selected === n.id ? null : n.id)}
                    style={{
                      position: "absolute",
                      left: `${(n.x / 1260) * 100}%`,
                      top: `${(n.y / 420) * 100}%`,
                      transform: "translate(-50%, -50%)",
                      background: isSelected
                        ? `linear-gradient(135deg, ${n.color}30, ${n.color}10)`
                        : "rgba(32,32,47,0.9)",
                      border: `1.5px solid ${isSelected ? n.color : n.color + "55"}`,
                      borderRadius: 10, padding: "10px 14px",
                      cursor: "pointer", textAlign: "center", minWidth: 110,
                      boxShadow: isSelected ? `0 0 20px ${n.color}40` : "none",
                      transition: "all 0.2s",
                      zIndex: isSelected ? 10 : 1,
                    }}
                    onMouseEnter={e => { if (!isSelected) e.currentTarget.style.borderColor = n.color; }}
                    onMouseLeave={e => { if (!isSelected) e.currentTarget.style.borderColor = n.color + "55"; }}
                  >
                    <div style={{ fontFamily: "'Orbitron', sans-serif", fontSize: 10, fontWeight: 700, color: "#fff", letterSpacing: 0.5, lineHeight: 1.2 }}>{n.label}</div>
                    <div style={{ fontFamily: "'Space Mono', monospace", fontSize: 9, color: n.color, marginTop: 3, opacity: 0.9 }}>{n.sub}</div>
                  </button>
                );
              })}
            </div>

            {/* Group labels */}
            <div style={{ display: "flex", gap: 16, marginTop: 8, paddingLeft: 4, flexWrap: "wrap" }}>
              {Object.entries(GROUP_LABELS).map(([k, v]) => (
                <div key={k} style={{ display: "flex", alignItems: "center", gap: 6 }}>
                  <div style={{ width: 8, height: 8, borderRadius: 2, background: v.color, opacity: 0.7 }} />
                  <span style={{ fontFamily: "'Space Mono', monospace", fontSize: 9, color: v.color, opacity: 0.7, letterSpacing: 2, textTransform: "uppercase" }}>{v.label}</span>
                </div>
              ))}
            </div>
          </div>
        </div>

        {/* Detail panel */}
        {node && (
          <div style={{
            marginTop: 32, padding: "28px 32px",
            background: `linear-gradient(135deg, ${node.color}10 0%, rgba(32,32,47,0.6) 100%)`,
            border: `1px solid ${node.color}40`, borderRadius: 16,
            display: "grid", gridTemplateColumns: "1fr 1fr", gap: 32, alignItems: "start",
          }}>
            <div>
              <div style={{ fontFamily: "'Space Mono', monospace", fontSize: 10, color: node.color, letterSpacing: 2, textTransform: "uppercase", marginBottom: 8 }}>Selected Node</div>
              <h3 style={{ fontFamily: "'Orbitron', sans-serif", fontWeight: 700, fontSize: 20, color: "#fff", marginBottom: 12 }}>{node.details.title}</h3>
              <p style={{ color: "rgba(255,255,255,0.65)", lineHeight: 1.7, fontSize: 15 }}>{node.details.body}</p>
            </div>
            <div>
              <div style={{ fontFamily: "'Space Mono', monospace", fontSize: 10, color: "rgba(255,255,255,0.4)", letterSpacing: 2, textTransform: "uppercase", marginBottom: 12 }}>Specs</div>
              <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                {node.details.specs.map((s, i) => (
                  <div key={i} style={{ display: "flex", alignItems: "flex-start", gap: 10 }}>
                    <div style={{ width: 5, height: 5, borderRadius: "50%", background: node.color, marginTop: 7, flexShrink: 0 }} />
                    <span style={{ fontFamily: "'Space Mono', monospace", fontSize: 12, color: "rgba(255,255,255,0.7)", lineHeight: 1.5 }}>{s}</span>
                  </div>
                ))}
              </div>
              {node.details.links && (
                <div style={{ marginTop: 16, display: "flex", gap: 10, flexWrap: "wrap" }}>
                  {node.details.links.map(l => (
                    <a key={l.url} href={l.url} target="_blank" rel="noopener noreferrer" style={{
                      fontFamily: "'Space Mono', monospace", fontSize: 10, color: node.color,
                      background: node.color + "15", border: `1px solid ${node.color}40`,
                      borderRadius: 6, padding: "5px 12px", textDecoration: "none",
                      letterSpacing: 0.5,
                    }}>{l.label} ↗</a>
                  ))}
                </div>
              )}
            </div>
          </div>
        )}
        {!node && (
          <p style={{ textAlign: "center", color: "rgba(255,255,255,0.25)", fontFamily: "'Space Mono', monospace", fontSize: 12, marginTop: 24, letterSpacing: 1 }}>
            ← Click any node to see details
          </p>
        )}
      </div>
    </section>
  );
}

function SectionHeader({ tag, title, subtitle }) {
  return (
    <div style={{ marginBottom: 8 }}>
      <div style={{ fontFamily: "'Space Mono', monospace", fontSize: 10, color: "#D6AB39", letterSpacing: 3, textTransform: "uppercase", marginBottom: 10, opacity: 0.8 }}>{tag}</div>
      <h2 style={{ fontFamily: "'Orbitron', sans-serif", fontWeight: 900, fontSize: "clamp(1.8rem, 4vw, 2.8rem)", color: "#fff", marginBottom: subtitle ? 14 : 0, lineHeight: 1.1 }}>{title}</h2>
      {subtitle && <p style={{ color: "rgba(255,255,255,0.5)", fontSize: 16, maxWidth: 640, lineHeight: 1.6 }}>{subtitle}</p>}
    </div>
  );
}

Object.assign(window, { Architecture, SectionHeader });
