/* global React */
const { useRef, useEffect } = React;

// Problem-section motion graphic — broken connections to generic AI.
//
// Left two-thirds of the canvas: the firm's data — six labeled FSP
// source nodes spread across the frame, structurally specific to
// regulated finance (Client Records, Regulatory Filings, Compliance
// Docs, Portfolio Data, Risk Profiles, Audit Trail). Right third: a
// single external entity (Generic AI), visually distinct from the firm
// nodes — larger, hollow, dashed outer ring, smaller inner ring.
//
// Across the gap: a continuous flux of failed connection attempts.
// Each attempt extends from a source toward the target but never
// reaches — drawing partway across, holding briefly, then fading out.
// Single failure mode (partial reach) for legibility. The previous
// three-mode version (partial / phantom / fragment) read as busy
// rather than meaningful; one consistent mode communicates more.
//
// Compliance Docs is the only accent-colored node, and it radiates a
// continuous distress pulse — the governance/audit alarm at the heart
// of the firm. It is the visual anchor.
//
// Visual language calibrated to match the hero: node sizes, stroke
// widths, build-in stagger, ambient breathing. Hovering any node or
// its label expands both together smoothly.

const PG_VBW = 1200;
const PG_VBH = 540;

const PG_INK = "#111111";
const PG_ACCENT = "rgb(224, 95, 49)";
const PG_BG = "#f5f5f5";

// Node sizes — calibrated against hero's hub/peripheral radii
const PG_R_FIRM = 13;
const PG_R_COMPLIANCE = 16;
const PG_AI_R_OUT = 29;
const PG_AI_R_IN = 17;

// Stroke widths — matching hero
const PG_SW_LINE = 2.6;
const PG_SW_OUTLINE = 2.0;
const PG_SW_AI_DASH = 1.8;
const PG_SW_AI_INNER = 1.4;

// Label typography
const PG_LABEL_SIZE = 15;

// Hover
const PG_HOVER_SCALE = 1.25;
const PG_LABEL_HOVER_SCALE = 1.28;
const PG_HOVER_RATE = 11; // smoothing rate; higher = snappier

// Build-in stagger
const PG_FADE = 0.55;
const PG_STAGGER = 0.10;

// Failure cycle: at any moment, ~2 attempts are visible. Each attempt
// takes PG_DUR seconds; new attempts start every 1.5s within an
// PG_LOOP-second cycle.
const PG_LOOP = 12;
const PG_DUR = 3.0;

// Compliance distress system cadence (slow, breath-like)
const PG_DISTRESS_DUR = 3.6;

// Node definitions. Positions are deliberate — Compliance Docs sits
// roughly at the firm cluster's centre so its distress rings read as
// the firm's heartbeat. The other firm nodes spread asymmetrically
// around it. Generic AI sits well to the right with its own breathing
// room. Label positions chosen to clear the failure-attempt sweeps.
const PG_NODES = [
  { id: 'client',     label: 'Client Records',     x: 180, y: 130, kind: 'fill',   labelPos: 'left'  },
  { id: 'reg',        label: 'Regulatory Filings', x: 410, y:  85, kind: 'hollow', labelPos: 'above' },
  { id: 'compliance', label: 'Compliance Docs',    x: 300, y: 280, kind: 'warn',   labelPos: 'left'  },
  { id: 'portfolio',  label: 'Portfolio Data',     x: 540, y: 215, kind: 'fill',   labelPos: 'above' },
  { id: 'risk',       label: 'Risk Profiles',      x: 200, y: 410, kind: 'hollow', labelPos: 'left'  },
  { id: 'audit',      label: 'Audit Trail',        x: 490, y: 460, kind: 'fill',   labelPos: 'below' },
  { id: 'ai',         label: 'Generic AI',         x: 970, y: 270, kind: 'ai',     labelPos: 'right' },
];

// 8 attempts, mixed outbound and inbound, staggered every 1.5s within
// a 12s loop. At any time roughly 2 are active.
const PG_ATTEMPTS = [
  { s: 'client',     t: 'ai',         f: 0.65, start: 0.0  },
  { s: 'reg',        t: 'ai',         f: 0.62, start: 1.5  },
  { s: 'portfolio',  t: 'ai',         f: 0.70, start: 3.0  },
  { s: 'ai',         t: 'compliance', f: 0.58, start: 4.5  },
  { s: 'risk',       t: 'ai',         f: 0.66, start: 6.0  },
  { s: 'ai',         t: 'audit',      f: 0.62, start: 7.5  },
  { s: 'audit',      t: 'ai',         f: 0.68, start: 9.0  },
  { s: 'ai',         t: 'reg',        f: 0.60, start: 10.5 },
];

// Per-node breathing — gentle radius oscillation matching the hero's
// idle motion. Deterministic per node so the cadence is stable.
const PG_BREATH = PG_NODES.map((n, i) => ({
  bw: 0.55 + (i % 5) * 0.09,
  bp: (i * 1.73) % (Math.PI * 2),
}));

// Label placement per node
function getLabelProps(n) {
  const offset = (n.kind === 'ai' ? PG_AI_R_OUT : (n.kind === 'warn' ? PG_R_COMPLIANCE : PG_R_FIRM)) + 14;
  switch (n.labelPos) {
    case 'above':
      return { x: n.x, y: n.y - offset -4, anchor: 'middle', baseline: 'auto' };
    case 'below':
      return { x: n.x, y: n.y + offset + 10, anchor: 'middle', baseline: 'auto' };
    case 'left':
      return { x: n.x - offset -24, y: n.y, anchor: 'end', baseline: 'middle' };
    case 'right':
      return { x: n.x + offset +8, y: n.y, anchor: 'start', baseline: 'middle' };
    default:
      return { x: n.x, y: n.y + offset, anchor: 'middle', baseline: 'auto' };
  }
}

// Approximate label bounding rect for the hover hit target. Tracking +
// font metrics combine to make this slightly generous, which is what
// we want (hits should be forgiving).
function getLabelHitRect(n) {
  const lp = getLabelProps(n);
  const charWidth = PG_LABEL_SIZE * 0.62; // tracked uppercase, approx
  const width = n.label.length * charWidth + 8;
  const height = PG_LABEL_SIZE + 8;

  let x = lp.x;
  if (lp.anchor === 'middle') x -= width / 2;
  else if (lp.anchor === 'end') x -= width;

  let y = lp.y;
  if (lp.baseline === 'auto') y -= height * 0.82;
  else if (lp.baseline === 'middle') y -= height / 2;

  return { x, y, width, height };
}

function ProblemGraph() {
  const lineRefs = useRef([]);
  const nodeRefs = useRef({});
  const labelRefs = useRef({});
  const aiOuterRef = useRef(null);
  const aiInnerRef = useRef(null);
  const hoverInts = useRef({});
  const hoveredId = useRef(null);

  const byId = Object.fromEntries(PG_NODES.map((n) => [n.id, n]));

  // Initialize hover intensities once
  if (Object.keys(hoverInts.current).length === 0) {
    PG_NODES.forEach((n) => { hoverInts.current[n.id] = 0; });
  }

  useEffect(() => {
    let raf;
    let lastT = null;
    const t0 = performance.now();

    const frame = (now) => {
      const tAbs = (now - t0) / 1000;
      const dt = lastT !== null ? Math.min(0.1, tAbs - lastT) : 1 / 60;
      lastT = tAbs;

      // ---- Nodes + labels: build-in fade, hover scale, ambient breath ----
      PG_NODES.forEach((n, i) => {
        const appear = Math.max(0, Math.min(1, (tAbs - i * PG_STAGGER) / PG_FADE));
        const eased = 1 - Math.pow(1 - appear, 3);

        // Smooth-approach hover intensity toward target
        const target = hoveredId.current === n.id ? 1 : 0;
        const current = hoverInts.current[n.id];
        const next = current + (target - current) * (1 - Math.exp(-PG_HOVER_RATE * dt));
        hoverInts.current[n.id] = next;

        // Ambient breath: subtle 4% radius oscillation
        const breath = 1 + 0.04 * Math.sin(tAbs * PG_BREATH[i].bw + PG_BREATH[i].bp);

        // Node
        const nodeEl = nodeRefs.current[n.id];
        if (nodeEl) {
          const nodeScale = (1 + next * (PG_HOVER_SCALE - 1)) * breath;
          nodeEl.setAttribute('transform', `scale(${nodeScale.toFixed(3)})`);
          nodeEl.style.opacity = eased.toFixed(3);
        }

        // Label
        const labelEl = labelRefs.current[n.id];
        if (labelEl) {
          const lblScale = 1 + next * (PG_LABEL_HOVER_SCALE - 1);
          labelEl.setAttribute('transform', `scale(${lblScale.toFixed(3)})`);
          const labelOp = eased * (0.6 + next * 0.35);
          labelEl.style.opacity = labelOp.toFixed(3);
        }
      });

      // ---- AI node opacity matches its node's build-in ----
      const aiIdx = PG_NODES.findIndex((n) => n.id === 'ai');
      const aiAppear = Math.max(0, Math.min(1, (tAbs - aiIdx * PG_STAGGER) / PG_FADE));
      const aiEased = 1 - Math.pow(1 - aiAppear, 3);
      if (aiOuterRef.current) aiOuterRef.current.style.opacity = (aiEased * 0.55).toFixed(3);
      if (aiInnerRef.current) aiInnerRef.current.style.opacity = (aiEased * 0.40).toFixed(3);

      // ---- Failed attempts ----
      // Wait for the build-in to finish before any attempts begin, so the
      // graphic settles before the failure cycle starts.
      const startDelay = PG_NODES.length * PG_STAGGER + 0.4;

      PG_ATTEMPTS.forEach((a, i) => {
        const line = lineRefs.current[i];
        if (!line) return;

        if (tAbs < startDelay) {
          line.style.opacity = '0';
          return;
        }

        const tEff = (tAbs - startDelay) % PG_LOOP;
        const e = (((tEff - a.start) % PG_LOOP) + PG_LOOP) % PG_LOOP;

        if (e >= PG_DUR) {
          line.style.opacity = '0';
          return;
        }

        const u = e / PG_DUR;
        const A = byId[a.s];
        const B = byId[a.t];
        const dx = B.x - A.x;
        const dy = B.y - A.y;

        // Reach grows from 0 toward a.f, eased, holds, then fades out
        const drawP = u <= 0.55 ? u / 0.55 : 1;
        const easedReach = 1 - Math.pow(1 - drawP, 2);
        const reach = a.f * easedReach;
        const x2 = A.x + dx * reach;
        const y2 = A.y + dy * reach;

        line.setAttribute('x2', x2.toFixed(1));
        line.setAttribute('y2', y2.toFixed(1));

        // Opacity envelope: ease in, hold, ease out
        let op;
        if (u < 0.10) op = (u / 0.10) * 0.42;
        else if (u < 0.65) op = 0.42;
        else op = 0.42 * (1 - (u - 0.65) / 0.35);
        line.style.opacity = op.toFixed(3);
      });

      raf = requestAnimationFrame(frame);
    };

    raf = requestAnimationFrame(frame);
    return () => cancelAnimationFrame(raf);
  }, []);

  const cn = byId.compliance;

  const setHover = (id) => () => { hoveredId.current = id; };
  const clearHover = (id) => () => {
    if (hoveredId.current === id) hoveredId.current = null;
  };

  return (
    <div className="pg-wrap">
      <svg
        className="pg"
        viewBox={`0 0 ${PG_VBW} ${PG_VBH}`}
        preserveAspectRatio="xMidYMid meet"
        role="img"
        aria-label="A knowledge graph showing a financial firm's data sources on the left — Client Records, Regulatory Filings, Compliance Docs, Portfolio Data, Risk Profiles, and Audit Trail — and an external Generic AI on the right. Repeated attempts to connect between them extend partway across the gap but never reach. A distress ring pulses around the Compliance Docs node."
        style={{ width: '100%', height: 'auto', display: 'block', overflow: 'visible' }}
      >
        <style>{`
          .pg-line {
            stroke: ${PG_INK};
            stroke-width: ${PG_SW_LINE};
            stroke-linecap: round;
            fill: none;
            opacity: 0;
            pointer-events: none;
          }
          .pg-node {
            transform-box: fill-box;
            transform-origin: center;
            pointer-events: none;
          }
          .pg-node--fill   { fill: ${PG_INK}; stroke: none; }
          .pg-node--hollow { fill: ${PG_BG}; stroke: ${PG_INK}; stroke-width: ${PG_SW_OUTLINE}; }
          .pg-node--warn   { fill: ${PG_ACCENT}; stroke: none; }
          .pg-ai-outer {
            fill: none;
            stroke: ${PG_INK};
            stroke-width: ${PG_SW_AI_DASH};
            stroke-dasharray: 6 6;
            pointer-events: none;
          }
          .pg-ai-inner {
            fill: none;
            stroke: ${PG_INK};
            stroke-width: ${PG_SW_AI_INNER};
            pointer-events: none;
          }
          .pg-label {
            fill: ${PG_INK};
            font-family: 'Inter', system-ui, sans-serif;
            font-size: ${PG_LABEL_SIZE}px;
            font-weight: 500;
            letter-spacing: 0.14em;
            text-transform: uppercase;
            transform-box: fill-box;
            transform-origin: center;
            pointer-events: none;
          }
          .pg-hit {
            fill: transparent;
            cursor: pointer;
          }
          .pg-warn-core {
            fill: ${PG_ACCENT};
            opacity: 0.16;
            transform-box: fill-box;
            transform-origin: center;
            animation: pg-core ${PG_DISTRESS_DUR}s ease-in-out infinite;
            pointer-events: none;
          }
          .pg-warn-ring {
            fill: none;
            stroke: ${PG_ACCENT};
            stroke-width: 1.4;
            transform-box: fill-box;
            transform-origin: center;
            opacity: 0;
            pointer-events: none;
          }
          .pg-warn-ring--a { animation: pg-radiate ${PG_DISTRESS_DUR}s ease-out infinite; }
          .pg-warn-ring--b { animation: pg-radiate ${PG_DISTRESS_DUR}s ease-out infinite; animation-delay: ${(PG_DISTRESS_DUR / 3).toFixed(2)}s; }
          .pg-warn-ring--c { animation: pg-radiate ${PG_DISTRESS_DUR}s ease-out infinite; animation-delay: ${((PG_DISTRESS_DUR / 3) * 2).toFixed(2)}s; }
          @keyframes pg-radiate {
            0%   { transform: scale(1);   opacity: 0.45; }
            70%  { transform: scale(3.2); opacity: 0;    }
            100% { transform: scale(3.2); opacity: 0;    }
          }
          @keyframes pg-core {
            0%, 100% { transform: scale(1);    opacity: 0.10; }
            50%      { transform: scale(1.30); opacity: 0.22; }
          }
        `}</style>

        {/* 1. AI node structure — drawn first so attempt lines visibly
              enter its territory */}
        <circle
          ref={aiOuterRef}
          className="pg-ai-outer"
          cx={byId.ai.x}
          cy={byId.ai.y}
          r={PG_AI_R_OUT}
          style={{ opacity: 0 }}
        />
        <circle
          ref={aiInnerRef}
          className="pg-ai-inner"
          cx={byId.ai.x}
          cy={byId.ai.y}
          r={PG_AI_R_IN}
          style={{ opacity: 0 }}
        />

        {/* 2. Failed attempt lines */}
        <g>
          {PG_ATTEMPTS.map((a, i) => {
            const A = byId[a.s];
            return (
              <line
                key={`attempt-${i}`}
                ref={(el) => { lineRefs.current[i] = el; }}
                x1={A.x}
                y1={A.y}
                x2={A.x}
                y2={A.y}
                className="pg-line"
              />
            );
          })}
        </g>

        {/* 3. Compliance Docs distress system — the visual anchor */}
        <circle className="pg-warn-core" cx={cn.x} cy={cn.y} r="17" />
        <circle className="pg-warn-ring pg-warn-ring--a" cx={cn.x} cy={cn.y} r="12" />
        <circle className="pg-warn-ring pg-warn-ring--b" cx={cn.x} cy={cn.y} r="12" />
        <circle className="pg-warn-ring pg-warn-ring--c" cx={cn.x} cy={cn.y} r="12" />

        {/* 4. Firm-side nodes */}
        <g>
          {PG_NODES.filter((n) => n.kind !== 'ai').map((n) => {
            const r = n.kind === 'warn' ? PG_R_COMPLIANCE : PG_R_FIRM;
            return (
              <circle
                key={n.id}
                ref={(el) => { nodeRefs.current[n.id] = el; }}
                cx={n.x}
                cy={n.y}
                r={r}
                className={`pg-node pg-node--${n.kind}`}
                style={{ opacity: 0 }}
              />
            );
          })}
        </g>

        {/* 5. Labels */}
        <g>
          {PG_NODES.map((n) => {
            const lp = getLabelProps(n);
            return (
              <text
                key={`lbl-${n.id}`}
                ref={(el) => { labelRefs.current[n.id] = el; }}
                x={lp.x}
                y={lp.y}
                textAnchor={lp.anchor}
                dominantBaseline={lp.baseline}
                className="pg-label"
                style={{ opacity: 0 }}
              >
                {n.label}
              </text>
            );
          })}
        </g>

        {/* 6. Hover hit targets — on top of everything. Two per node:
              a circle around the node and a rect around the label.
              Both trigger the same hover state. */}
        <g>
          {PG_NODES.map((n) => {
            const nodeR = n.kind === 'ai' ? PG_AI_R_OUT : (n.kind === 'warn' ? PG_R_COMPLIANCE : PG_R_FIRM);
            const hitR = nodeR + 8;
            const lblRect = getLabelHitRect(n);

            return (
              <React.Fragment key={`hit-${n.id}`}>
                <circle
                  className="pg-hit"
                  cx={n.x}
                  cy={n.y}
                  r={hitR}
                  onMouseEnter={setHover(n.id)}
                  onMouseLeave={clearHover(n.id)}
                />
                <rect
                  className="pg-hit"
                  x={lblRect.x}
                  y={lblRect.y}
                  width={lblRect.width}
                  height={lblRect.height}
                  onMouseEnter={setHover(n.id)}
                  onMouseLeave={clearHover(n.id)}
                />
              </React.Fragment>
            );
          })}
        </g>
      </svg>
    </div>
  );
}

window.ProblemGraph = ProblemGraph;