"use client";

import { useEffect, useRef, useState } from "react";

type Mode = "cloud" | "self";

type Palette = {
    ink: string;
    ink3: string;
    rule: string;
    panel: string;
    accent: string;
    up: string;
    pend: string;
    mono: string;
    sans: string;
};

type Node = {
    label: string;
    sub: string;
    x: number;
    y: number;
    w: number;
    h: number;
    state: "up" | "pend";
    flash: number;
};

type Wire = {
    node: number;
    ax: number;
    ay: number;
    bx: number;
    by: number;
    c1x: number;
    c1y: number;
    c2x: number;
    c2y: number;
};

type Packet = {
    wire: number;
    t: number;
    speed: number;
    /** true when travelling control plane to agent, which only happens down a wire the agent opened. */
    inbound: boolean;
};

const AGENTS: { label: string; sub: string; state: "up" | "pend" }[] = [
    { label: "proxmox-node-1", sub: "4 keys", state: "up" },
    { label: "db-primary", sub: "3 keys", state: "up" },
    { label: "edge-gw-02", sub: "2 keys", state: "up" },
    { label: "win-build-01", sub: "windows", state: "pend" },
];

const MODES: { id: Mode; label: string }[] = [
    { id: "cloud", label: "Hosted by us" },
    { id: "self", label: "Hosted by you" },
];

const NOTES: Record<Mode, string> = {
    cloud: "We run Vantage. Your servers reach out to it, so nothing needs to be open to the internet on your side.",
    self: "You run Vantage too, on your own hardware. Nothing leaves your network at all, and there is nothing to phone home to.",
};

function readPalette(el: HTMLElement): Palette {
    const s = getComputedStyle(el);
    const v = (name: string, fallback: string) => s.getPropertyValue(name).trim() || fallback;
    return {
        ink: v("--ink", "#0a1b33"),
        ink3: v("--ink-3", "#6c7f96"),
        rule: v("--rule", "#cdd6e2"),
        panel: v("--panel", "#ffffff"),
        accent: v("--accent", "#0b2a58"),
        up: v("--up", "#2f8a60"),
        pend: v("--pend", "#b0801f"),
        mono: `11px ${v("--mono", "monospace")}`,
        sans: `600 15px ${v("--sans", "sans-serif")}`,
    };
}

function pointAt(w: Wire, t: number) {
    const u = 1 - t;
    const a = u * u * u;
    const b = 3 * u * u * t;
    const c = 3 * u * t * t;
    const d = t * t * t;
    return {
        x: a * w.ax + b * w.c1x + c * w.c2x + d * w.bx,
        y: a * w.ay + b * w.c1y + c * w.c2y + d * w.by,
    };
}

function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.arcTo(x + w, y, x + w, y + h, r);
    ctx.arcTo(x + w, y + h, x, y + h, r);
    ctx.arcTo(x, y + h, x, y, r);
    ctx.arcTo(x, y, x + w, y, r);
    ctx.closePath();
}

export function TopologyDiagram() {
    const [mode, setMode] = useState<Mode>("cloud");
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const modeRef = useRef<Mode>(mode);

    modeRef.current = mode;

    useEffect(() => {
        const canvas = canvasRef.current;
        if (!canvas) return;
        const ctx = canvas.getContext("2d");
        if (!ctx) return;

        let palette = readPalette(document.documentElement);
        let width = 0;
        let height = 0;
        let nodes: Node[] = [];
        let wires: Wire[] = [];
        let plane = { x: 0, y: 0, w: 0, h: 0 };
        let boundary: { x: number; y: number; w: number; h: number; label: string; split: number | null } | null = null;
        let stacked = false;

        const packets: Packet[] = [];
        let planePulse = 0;
        let sinceSpawn = 0;
        let sinceCommand = 2.2;
        let raf = 0;
        let last = 0;

        const reduced = window.matchMedia("(prefers-reduced-motion: reduce)");

        const layout = () => {
            const rect = canvas.getBoundingClientRect();
            width = rect.width;
            height = rect.height;
            stacked = width < 660;

            const dpr = Math.min(window.devicePixelRatio || 1, 2);
            canvas.width = Math.round(width * dpr);
            canvas.height = Math.round(height * dpr);
            ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

            const pad = stacked ? 14 : 22;
            const nodeW = stacked ? Math.min(150, (width - pad * 2 - 16) / 2) : 168;
            const nodeH = 46;

            if (stacked) {
                plane = { x: width / 2 - 100, y: pad + 30, w: 200, h: 78 };
                const cols = 2;
                const gapX = 14;
                const gapY = 14;
                const gridW = cols * nodeW + gapX;
                const startX = width / 2 - gridW / 2;
                const startY = plane.y + plane.h + (mode === "cloud" ? 84 : 66);
                nodes = AGENTS.map((a, i) => ({
                    label: a.label,
                    sub: a.sub,
                    state: a.state,
                    flash: 0,
                    w: nodeW,
                    h: nodeH,
                    x: startX + (i % cols) * (nodeW + gapX),
                    y: startY + Math.floor(i / cols) * (nodeH + gapY),
                }));
            } else {
                const colX = pad + 8;
                plane = { x: width - pad - 214, y: height / 2 - 46, w: 206, h: 92 };
                const total = AGENTS.length * nodeH + (AGENTS.length - 1) * 16;
                const startY = height / 2 - total / 2;
                nodes = AGENTS.map((a, i) => ({
                    label: a.label,
                    sub: a.sub,
                    state: a.state,
                    flash: 0,
                    w: nodeW,
                    h: nodeH,
                    x: colX,
                    y: startY + i * (nodeH + 16),
                }));
            }

            wires = nodes.map((n, i) => {
                if (stacked) {
                    const ax = n.x + n.w / 2;
                    const ay = n.y;
                    const bx = plane.x + plane.w * (0.2 + 0.2 * i);
                    const by = plane.y + plane.h;
                    const mid = (ay + by) / 2;
                    return { node: i, ax, ay, bx, by, c1x: ax, c1y: mid, c2x: bx, c2y: mid };
                }
                const ax = n.x + n.w;
                const ay = n.y + n.h / 2;
                const bx = plane.x;
                const by = plane.y + plane.h * (0.24 + 0.17 * i);
                const mid = ax + (bx - ax) * 0.5;
                return { node: i, ax, ay, bx, by, c1x: mid, c1y: ay, c2x: mid, c2y: by };
            });

            if (modeRef.current === "self") {
                boundary = { x: pad - 8, y: pad - 8, w: width - (pad - 8) * 2, h: height - (pad - 8) * 2, label: "YOUR NETWORK", split: null };
            } else if (stacked) {
                const y = plane.y + plane.h + 34;
                boundary = { x: pad - 8, y, w: width - (pad - 8) * 2, h: height - y - (pad - 8), label: "YOUR NETWORK", split: y };
            } else {
                const split = plane.x - 46;
                boundary = { x: pad - 8, y: pad - 8, w: split - (pad - 8), h: height - (pad - 8) * 2, label: "YOUR NETWORK", split };
            }
        };

        const spawn = () => {
            if (packets.length > 60) return;
            const wire = Math.floor(Math.random() * wires.length);
            packets.push({ wire, t: 0, speed: 0.34 + Math.random() * 0.16, inbound: false });
        };

        const drawWire = (w: Wire) => {
            ctx.beginPath();
            ctx.moveTo(w.ax, w.ay);
            ctx.bezierCurveTo(w.c1x, w.c1y, w.c2x, w.c2y, w.bx, w.by);
            ctx.stroke();
        };

        const draw = (dt: number) => {
            ctx.clearRect(0, 0, width, height);
            const mono = palette.mono;

            // Boundary of what you own.
            if (boundary) {
                ctx.save();
                ctx.setLineDash([5, 5]);
                ctx.lineWidth = 1;
                ctx.strokeStyle = palette.rule;
                roundRect(ctx, boundary.x, boundary.y, boundary.w, boundary.h, 10);
                ctx.stroke();
                ctx.restore();

                ctx.font = mono;
                ctx.fillStyle = palette.ink3;
                ctx.textBaseline = "middle";
                ctx.textAlign = "left";
                ctx.fillText(boundary.label, boundary.x + 12, boundary.y + 13);

                if (modeRef.current === "cloud") {
                    ctx.textAlign = stacked ? "left" : "right";
                    const x = stacked ? boundary.x + 12 : width - 22;
                    const y = stacked ? 18 : boundary.y + 13;
                    ctx.fillText("HOSTED BY US", x, y);
                }
            }

            // Wires. Drawn under everything, because they are infrastructure.
            ctx.lineWidth = 1.25;
            ctx.strokeStyle = palette.rule;
            wires.forEach(drawWire);

            // Packets travelling the wires.
            for (let i = packets.length - 1; i >= 0; i--) {
                const p = packets[i];
                p.t += p.speed * dt;
                if (p.t >= 1) {
                    if (p.inbound) nodes[wires[p.wire].node].flash = 1;
                    else planePulse = 1;
                    packets.splice(i, 1);
                    continue;
                }
                const w = wires[p.wire];
                const pos = pointAt(w, p.inbound ? 1 - p.t : p.t);
                const fade = Math.min(1, Math.sin(p.t * Math.PI) * 2.2);

                ctx.globalAlpha = fade;
                ctx.fillStyle = p.inbound ? palette.accent : palette.up;
                ctx.beginPath();
                ctx.arc(pos.x, pos.y, p.inbound ? 3.4 : 2.4, 0, Math.PI * 2);
                ctx.fill();

                if (p.inbound) {
                    ctx.globalAlpha = fade * 0.22;
                    ctx.beginPath();
                    ctx.arc(pos.x, pos.y, 8, 0, Math.PI * 2);
                    ctx.fill();
                }
                ctx.globalAlpha = 1;
            }

            // Agents.
            for (const n of nodes) {
                if (n.flash > 0) {
                    ctx.globalAlpha = n.flash * 0.5;
                    ctx.strokeStyle = palette.accent;
                    ctx.lineWidth = 2;
                    roundRect(ctx, n.x - 2, n.y - 2, n.w + 4, n.h + 4, 8);
                    ctx.stroke();
                    ctx.globalAlpha = 1;
                    n.flash = Math.max(0, n.flash - dt * 1.6);
                }

                ctx.fillStyle = palette.panel;
                ctx.strokeStyle = palette.rule;
                ctx.lineWidth = 1;
                roundRect(ctx, n.x, n.y, n.w, n.h, 6);
                ctx.fill();
                ctx.stroke();

                ctx.fillStyle = n.state === "up" ? palette.up : palette.pend;
                ctx.beginPath();
                ctx.arc(n.x + 14, n.y + n.h / 2, 3.5, 0, Math.PI * 2);
                ctx.fill();

                ctx.textAlign = "left";
                ctx.textBaseline = "middle";
                ctx.font = mono;
                ctx.fillStyle = palette.ink;
                ctx.fillText(n.label, n.x + 26, n.y + n.h / 2 - 6);
                ctx.fillStyle = palette.ink3;
                ctx.fillText(n.sub, n.x + 26, n.y + n.h / 2 + 8);
            }

            // Control plane.
            if (planePulse > 0) {
                ctx.globalAlpha = planePulse * 0.35;
                ctx.strokeStyle = palette.accent;
                ctx.lineWidth = 2;
                roundRect(ctx, plane.x - 3 - planePulse * 4, plane.y - 3 - planePulse * 4, plane.w + 6 + planePulse * 8, plane.h + 6 + planePulse * 8, 10);
                ctx.stroke();
                ctx.globalAlpha = 1;
                planePulse = Math.max(0, planePulse - dt * 1.5);
            }

            ctx.fillStyle = palette.accent;
            roundRect(ctx, plane.x, plane.y, plane.w, plane.h, 8);
            ctx.fill();

            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            const cx = plane.x + plane.w / 2;
            ctx.fillStyle = palette.panel;
            ctx.font = palette.sans;
            ctx.fillText("Vantage", cx, plane.y + plane.h / 2 - 11);
            ctx.font = mono;
            ctx.globalAlpha = 0.78;
            ctx.fillText(modeRef.current === "cloud" ? "vantage.hostxtra.co.uk" : "your own hardware", cx, plane.y + plane.h / 2 + 8);
            ctx.globalAlpha = 1;

            // Direction of travel, stated once rather than on every wire.
            if (!stacked) {
                ctx.font = mono;
                ctx.fillStyle = palette.ink3;
                ctx.textAlign = "center";
                const midX = (nodes[0].x + nodes[0].w + plane.x) / 2;
                ctx.fillText("your servers dial out", midX, height - 26);
            }
        };

        const frame = (now: number) => {
            const dt = Math.min((now - last) / 1000, 0.05);
            last = now;

            sinceSpawn += dt;
            if (sinceSpawn > 0.42) {
                sinceSpawn = 0;
                spawn();
            }

            sinceCommand += dt;
            if (sinceCommand > 4.4) {
                sinceCommand = 0;
                const wire = Math.floor(Math.random() * wires.length);
                packets.push({ wire, t: 0, speed: 0.62, inbound: true });
            }

            draw(dt);
            raf = requestAnimationFrame(frame);
        };

        const start = () => {
            cancelAnimationFrame(raf);
            layout();
            if (reduced.matches) {
                packets.length = 0;
                wires.forEach((_, i) => packets.push({ wire: i, t: 0.35 + i * 0.12, speed: 0, inbound: false }));
                draw(0);
                return;
            }
            last = performance.now();
            raf = requestAnimationFrame(frame);
        };

        const onResize = () => start();
        const observer = new ResizeObserver(onResize);
        observer.observe(canvas);

        const repaint = () => {
            palette = readPalette(document.documentElement);
            if (reduced.matches) draw(0);
        };

        const themeObserver = new MutationObserver(repaint);
        themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });

        const scheme = window.matchMedia("(prefers-color-scheme: dark)");
        const onScheme = repaint;
        scheme.addEventListener("change", onScheme);
        reduced.addEventListener("change", start);

        start();

        return () => {
            cancelAnimationFrame(raf);
            observer.disconnect();
            themeObserver.disconnect();
            scheme.removeEventListener("change", onScheme);
            reduced.removeEventListener("change", start);
        };
    }, [mode]);

    return (
        <figure className="topo">
            <div className="topo__head">
                <div className="topo__seg" role="group" aria-label="Where Vantage runs">
                    {MODES.map((m) => (
                        <button key={m.id} type="button" className={m.id === mode ? "topo__btn is-on" : "topo__btn"} aria-pressed={m.id === mode} onClick={() => setMode(m.id)}>
                            {m.label}
                        </button>
                    ))}
                </div>
                <p className="topo__key">
                    <span className="topo__dot topo__dot--out" /> your server checking in
                    <span className="topo__dot topo__dot--in" /> something you asked for
                </p>
            </div>

            <canvas
                ref={canvasRef}
                className="topo__canvas"
                role="img"
                aria-label={
                    mode === "cloud"
                        ? "Four of your servers, inside your own network, each opening a connection out to Vantage running on our hardware. Nothing connects inward."
                        : "Four of your servers and Vantage itself, all inside your own network. The servers connect to Vantage, and nothing leaves your network."
                }
            />

            <figcaption className="topo__note">{NOTES[mode]}</figcaption>
        </figure>
    );
}
