"use client";

/**
 * Country × Campaign line chart.
 *
 * One line per campaign, plotted across countries ordered by total signups
 * (highest first). That ordering is what makes a line legitimate here: the x
 * axis isn't time, it's a ranked axis, so each line reads as how far a campaign
 * reaches down the market list — a steep line is concentrated in a few markets,
 * a flat one is spread evenly.
 *
 * Everything colour-related comes from the page theme, so it follows whichever
 * accent is chosen. Curves are smoothed with a hand-rolled Catmull-Rom → cubic
 * conversion rather than pulling in d3-shape.
 */

import { Portal, Skeleton, Stack, Text } from "@mantine/core";
import { ParentSize } from "@visx/responsive";
import { useId, useMemo, useState } from "react";
import { usePromoTheme } from "../appearance";
import { EASE_OUT, motion, useReducedMotion } from "../motion";
import styles from "./charts.module.css";

export interface LineSeries {
  key: string;
  name: string;
  color: string;
  colorTo?: string;
}

/** Row shape: the country label plus one numeric entry per series *name*. */
export type LineRow = Record<string, string | number>;

interface CountryCampaignLinesProps {
  data: LineRow[];
  series: LineSeries[];
  /** Key in each row holding the country label. */
  xKey: string;
  height?: number;
  /** Index into `data` of the highlighted country, or null. */
  hoveredIndex: number | null;
  onHoverIndex: (index: number | null) => void;
  /** Countries plotted before the tail is dropped. */
  maxPoints?: number;
  /**
   * Keep `data` in the order given instead of ranking by total descending.
   *
   * Ranking is right for a chart of countries — the biggest contributors should
   * lead. It is wrong for anything with a meaningful sequence: a per-day series
   * ranked by volume has no time axis left, it just draws the same points in a
   * misleading order. Set this for ordered data, and the tail is then dropped
   * from the *end* of the sequence rather than from the smallest values.
   */
  preserveOrder?: boolean;
  /** Plural noun for the "top N …" caption. */
  pointNoun?: string;
}

const MARGIN = { top: 16, right: 14, bottom: 52, left: 46 };
const TILT_BELOW = 64;

/** Round, whole-number axis ticks covering 0..max. */
function niceTicks(max: number, target = 4): number[] {
  if (!Number.isFinite(max) || max <= 0) return [0, 1];
  const rough = max / target;
  const mag = 10 ** Math.floor(Math.log10(rough));
  const norm = rough / mag;
  const step = Math.max(
    1,
    (norm >= 5 ? 10 : norm >= 2 ? 5 : norm >= 1 ? 2 : 1) * mag,
  );
  const ticks: number[] = [];
  for (let v = 0; v <= max + step * 1e-6; v += step) ticks.push(v);
  if (ticks[ticks.length - 1] < max) ticks.push(ticks.length * step);
  return ticks;
}

/**
 * Smooth path through points via Catmull-Rom, converted to cubic beziers.
 *
 * `tension` 0 is a straight polyline, 1 is fully round. Kept mild so the curve
 * never bulges past a data point and invents a value that isn't there.
 */
function smoothPath(points: Array<{ x: number; y: number }>, tension = 0.5) {
  if (points.length === 0) return "";
  if (points.length === 1) return `M${points[0].x},${points[0].y}`;
  const t = tension / 6;
  let d = `M${points[0].x},${points[0].y}`;
  for (let i = 0; i < points.length - 1; i++) {
    const p0 = points[i - 1] ?? points[i];
    const p1 = points[i];
    const p2 = points[i + 1];
    const p3 = points[i + 2] ?? p2;
    const c1x = p1.x + (p2.x - p0.x) * t;
    const c1y = p1.y + (p2.y - p0.y) * t;
    const c2x = p2.x - (p3.x - p1.x) * t;
    const c2y = p2.y - (p3.y - p1.y) * t;
    d += ` C${c1x},${c1y} ${c2x},${c2y} ${p2.x},${p2.y}`;
  }
  return d;
}

export function CountryCampaignLines(props: CountryCampaignLinesProps) {
  const height = props.height ?? 340;
  return (
    <div style={{ height }}>
      <ParentSize debounceTime={30}>
        {({ width }) =>
          width < 10 ? (
            <Skeleton height={height} radius="md" />
          ) : (
            <LinesInner {...props} width={width} height={height} />
          )
        }
      </ParentSize>
    </div>
  );
}

function LinesInner({
  data,
  series,
  xKey,
  width,
  height,
  hoveredIndex,
  onHoverIndex,
  maxPoints = 16,
  preserveOrder = false,
  pointNoun = "countries",
}: CountryCampaignLinesProps & { width: number; height: number }) {
  const reduce = useReducedMotion();
  const { chartDepth } = usePromoTheme();
  const glossy = chartDepth !== "flat";
  const is3d = chartDepth === "3d";
  const uid = useId().replace(/:/g, "");
  const [cursor, setCursor] = useState({ x: 0, y: 0 });

  const plotW = Math.max(0, width - MARGIN.left - MARGIN.right);
  const plotH = Math.max(0, height - MARGIN.top - MARGIN.bottom);
  const baseline = MARGIN.top + plotH;

  const model = useMemo(() => {
    const rows = data
      .map((row, rowIndex) => {
        const values = series.map((s) => Number(row[s.name] ?? 0));
        return {
          label: String(row[xKey] ?? ""),
          rowIndex,
          values,
          total: values.reduce((a, b) => a + b, 0),
        };
      })
      .filter((r) => r.total > 0);

    if (!preserveOrder) rows.sort((a, b) => b.total - a.total);

    // For ordered data the most recent points matter most, so the head is
    // dropped rather than the tail.
    const shown = preserveOrder
      ? rows.slice(Math.max(0, rows.length - maxPoints))
      : rows.slice(0, maxPoints);
    return { shown, hidden: rows.length - shown.length };
  }, [data, series, xKey, maxPoints, preserveOrder]);

  if (model.shown.length < 2) {
    return (
      <Stack align="center" justify="center" h={height} gap={4}>
        <Text size="sm" c="dimmed">
          {model.shown.length === 0
            ? "No signups to chart."
            : "At least two countries are needed for a trend line."}
        </Text>
      </Stack>
    );
  }

  const maxValue = Math.max(...model.shown.flatMap((r) => r.values), 1);
  const ticks = niceTicks(maxValue);
  const domainMax = ticks[ticks.length - 1] || 1;

  const step = plotW / Math.max(1, model.shown.length - 1);
  const xOf = (i: number) => MARGIN.left + i * step;
  const yOf = (v: number) => baseline - (v / domainMax) * plotH;
  const tilt = step < TILT_BELOW;

  const active =
    hoveredIndex !== null
      ? model.shown.find((r) => r.rowIndex === hoveredIndex)
      : null;
  const activePos = active ? model.shown.indexOf(active) : -1;

  return (
    <div
      className={styles.chartWrap}
      style={{ height }}
      onMouseMove={(e) => setCursor({ x: e.clientX, y: e.clientY })}
      onMouseLeave={() => onHoverIndex(null)}
    >
      <svg
        width={width}
        height={height}
        role="img"
        aria-label="Signups by country and campaign"
      >
        <defs>
          {series.map((s, i) => (
            <linearGradient
              key={`${uid}-area-${i}`}
              id={`${uid}-area-${i}`}
              gradientUnits="userSpaceOnUse"
              x1={0}
              y1={MARGIN.top}
              x2={0}
              y2={baseline}
            >
              <stop offset="0%" stopColor={s.color} stopOpacity={0.3} />
              <stop offset="100%" stopColor={s.color} stopOpacity={0} />
            </linearGradient>
          ))}
          {glossy && (
            <filter
              id={`${uid}-glow`}
              x="-20%"
              y="-20%"
              width="140%"
              height="140%"
            >
              <feDropShadow
                dx="0"
                dy={is3d ? 3 : 1.5}
                stdDeviation={is3d ? 3 : 1.6}
                floodColor="#0f172a"
                floodOpacity={is3d ? 0.4 : 0.25}
              />
            </filter>
          )}
        </defs>

        {/* Gridlines + value axis. */}
        {ticks.map((tick) => (
          <g key={`tick-${tick}`}>
            <line
              x1={MARGIN.left}
              x2={MARGIN.left + plotW}
              y1={yOf(tick)}
              y2={yOf(tick)}
              className={styles.barGrid}
            />
            <text
              x={MARGIN.left - 7}
              y={yOf(tick)}
              textAnchor="end"
              dominantBaseline="middle"
              className={styles.barAxisLabel}
            >
              {tick.toLocaleString()}
            </text>
          </g>
        ))}

        {/* Crosshair on the hovered country, behind the lines. */}
        {activePos >= 0 && (
          <line
            x1={xOf(activePos)}
            x2={xOf(activePos)}
            y1={MARGIN.top}
            y2={baseline}
            className={styles.lineCrosshair}
          />
        )}

        {series.map((s, si) => {
          const points = model.shown.map((row, i) => ({
            x: xOf(i),
            y: yOf(row.values[si] ?? 0),
          }));
          const linePath = smoothPath(points);
          const areaPath = `${linePath} L${points[points.length - 1].x},${baseline} L${points[0].x},${baseline} Z`;

          // Note: no dimming of other series on hover here. The crosshair plus
          // the tooltip already identify the country, and fading the other lines
          // would hide the cross-campaign comparison the chart exists for.
          return (
            <g key={`series-${s.key}-${si}`}>
              <motion.path
                d={areaPath}
                fill={`url(#${uid}-area-${si})`}
                initial={reduce ? false : { opacity: 0 }}
                animate={{ opacity: 1 }}
                transition={{
                  duration: 0.5,
                  delay: reduce ? 0 : 0.25 + si * 0.08,
                  ease: EASE_OUT,
                }}
              />
              <motion.path
                d={linePath}
                fill="none"
                stroke={s.color}
                strokeWidth={is3d ? 3.4 : 2.4}
                strokeLinecap="round"
                strokeLinejoin="round"
                filter={glossy ? `url(#${uid}-glow)` : undefined}
                // pathLength draws the line on rather than fading it in.
                initial={reduce ? false : { pathLength: 0 }}
                animate={{ pathLength: 1 }}
                transition={{
                  duration: 0.9,
                  delay: reduce ? 0 : si * 0.12,
                  ease: EASE_OUT,
                }}
              />
              {points.map((p, i) => {
                const isOn = i === activePos;
                return (
                  <motion.circle
                    key={`pt-${i}`}
                    cx={p.x}
                    cy={p.y}
                    r={isOn ? 5 : 3}
                    fill="var(--mantine-color-body)"
                    stroke={s.color}
                    strokeWidth={isOn ? 3 : 2}
                    initial={reduce ? false : { opacity: 0, scale: 0.4 }}
                    animate={{ opacity: 1, scale: 1 }}
                    transition={{
                      duration: 0.3,
                      delay: reduce ? 0 : 0.5 + si * 0.08 + i * 0.02,
                      ease: EASE_OUT,
                    }}
                  />
                );
              })}
            </g>
          );
        })}

        {/* Country labels + full-height hit strips. */}
        {model.shown.map((row, i) => (
          <g key={`x-${row.label}-${i}`}>
            <rect
              x={xOf(i) - step / 2}
              y={MARGIN.top}
              width={step}
              height={plotH}
              fill="transparent"
              style={{ cursor: "pointer" }}
              onMouseEnter={() => onHoverIndex(row.rowIndex)}
            />
            <text
              x={xOf(i)}
              y={baseline + (tilt ? 12 : 16)}
              textAnchor={tilt ? "end" : "middle"}
              transform={
                tilt ? `rotate(-35, ${xOf(i)}, ${baseline + 12})` : undefined
              }
              className={styles.barAxisLabel}
              fontWeight={i === activePos ? 700 : 400}
              fill={
                i === activePos
                  ? "var(--promo-accent, var(--mantine-color-indigo-6))"
                  : undefined
              }
              pointerEvents="none"
            >
              {row.label}
            </text>
          </g>
        ))}

        <line
          x1={MARGIN.left}
          x2={MARGIN.left + plotW}
          y1={baseline}
          y2={baseline}
          className={styles.barBaseline}
        />
      </svg>

      {model.hidden > 0 && (
        <Text size="xs" c="dimmed" mt={-4}>
          {preserveOrder
            ? `Latest ${model.shown.length} ${pointNoun}`
            : `Top ${model.shown.length} ${pointNoun} by signups`}{" "}
          · {model.hidden} more not plotted
        </Text>
      )}

      {/*
       * Portalled to <body>: `.tooltip` is position: fixed, but `PageSection`
       * animates `filter: blur()` and framer-motion leaves `filter: blur(0px)`
       * behind, which permanently makes that div the containing block for fixed
       * descendants. Without the portal the tooltip anchors to it and drifts far
       * from the cursor on a scrolled page.
       */}
      {active && (
        <Portal>
          <div
            className={styles.tooltip}
            style={{ left: cursor.x + 14, top: cursor.y - 20 }}
          >
            <div className={styles.tooltipTitle}>{active.label}</div>
            <Stack gap={1} mt={2}>
              {series
                .map((s, si) => ({ s, value: active.values[si] ?? 0 }))
                .filter((e) => e.value > 0)
                .sort((a, b) => b.value - a.value)
                .map(({ s, value }) => (
                  <div
                    key={s.key}
                    style={{ display: "flex", alignItems: "center", gap: 6 }}
                  >
                    <span
                      className={styles.tooltipSwatch}
                      style={{ background: s.color }}
                    />
                    <span style={{ fontWeight: 700 }}>
                      {value.toLocaleString()}
                    </span>
                    <span>{s.name}</span>
                  </div>
                ))}
            </Stack>
            <div className={styles.tooltipTotal}>
              {active.total.toLocaleString()} total
            </div>
          </div>
        </Portal>
      )}
    </div>
  );
}
