"use client";

/**
 * Country × Campaign stacked bar chart.
 *
 * Vertical columns, one per country, stacked by campaign. Scales and ticks are
 * computed here rather than pulled from @visx/scale + @visx/axis — the maths is
 * a few lines, it avoids two dependencies, and it lets the gloss be built into
 * the geometry instead of fought against a generic axis renderer.
 *
 * Gloss is four layers per column: a vertical gradient fill, a specular band
 * across the top of each segment, a left-edge sheen down the whole stack, and a
 * soft ground shadow under the baseline.
 */

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 BarSeries {
  key: string;
  name: string;
  color: string;
  colorTo?: string;
}

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

interface CountryCampaignBarsProps {
  data: BarRow[];
  series: BarSeries[];
  /** 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;
  /** Columns drawn before the rest are summarised as a count. */
  maxColumns?: number;
  /** Makes columns clickable; receives the x-axis label. */
  onColumnClick?: (label: string) => void;
}

const MARGIN = { top: 14, right: 8, bottom: 54, left: 46 };
const CORNER = 5;
/** Below this column pitch the labels overlap, so they tilt. */
const TILT_BELOW = 64;

/**
 * Round, human-friendly axis ticks covering 0..max.
 *
 * Step is clamped to whole numbers: these are signup counts, so a "0.5" tick
 * label would be meaningless (which is what an unclamped step produces once the
 * maximum drops to 1–3).
 */
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;
}

/** Rect with only its top corners rounded — the silhouette of a stacked column. */
function topRoundedPath(x: number, y: number, w: number, h: number, r: number) {
  const rr = Math.max(0, Math.min(r, w / 2, h));
  return [
    `M${x},${y + h}`,
    `L${x},${y + rr}`,
    `Q${x},${y} ${x + rr},${y}`,
    `L${x + w - rr},${y}`,
    `Q${x + w},${y} ${x + w},${y + rr}`,
    `L${x + w},${y + h}`,
    "Z",
  ].join(" ");
}

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

function BarsInner({
  data,
  series,
  xKey,
  width,
  height,
  hoveredIndex,
  onHoverIndex,
  maxColumns = 14,
  onColumnClick,
}: CountryCampaignBarsProps & { width: number; height: number }) {
  const reduce = useReducedMotion();
  const { chartDepth } = usePromoTheme();
  const glossy = chartDepth === "glossy";
  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 columns = data
      .map((row, rowIndex) => {
        const segments = series
          .map((s) => ({
            name: s.name,
            color: s.color,
            colorTo: s.colorTo ?? s.color,
            value: Number(row[s.name] ?? 0),
          }))
          .filter((seg) => seg.value > 0);
        return {
          label: String(row[xKey] ?? ""),
          rowIndex,
          segments,
          total: segments.reduce((sum, seg) => sum + seg.value, 0),
        };
      })
      .filter((c) => c.total > 0)
      .sort((a, b) => b.total - a.total);

    const shown = columns.slice(0, maxColumns);
    return { shown, hidden: columns.length - shown.length };
  }, [data, series, xKey, maxColumns]);

  if (!model.shown.length) {
    return (
      <Stack align="center" justify="center" h={height} gap={4}>
        <Text size="sm" c="dimmed">
          No signups to chart.
        </Text>
      </Stack>
    );
  }

  const maxTotal = Math.max(...model.shown.map((c) => c.total));
  const ticks = niceTicks(maxTotal);
  const domainMax = ticks[ticks.length - 1] || 1;
  const yOf = (value: number) => baseline - (value / domainMax) * plotH;

  const step = plotW / model.shown.length;
  const colW = Math.min(46, Math.max(10, step * 0.62));
  const tilt = step < TILT_BELOW;

  const active =
    hoveredIndex !== null
      ? model.shown.find((c) => c.rowIndex === hoveredIndex)
      : null;

  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>
          {/*
            userSpaceOnUse spanning the plot, so a short segment still samples the
            same ramp as a tall one — with objectBoundingBox each segment would
            get its own full gradient and the stack would look striped.
          */}
          {series.map((s, i) => (
            <linearGradient
              key={`${uid}-fill-${i}`}
              id={`${uid}-fill-${i}`}
              gradientUnits="userSpaceOnUse"
              x1={0}
              y1={MARGIN.top}
              x2={0}
              y2={baseline}
            >
              <stop offset="0%" stopColor={s.color} />
              <stop offset="100%" stopColor={s.colorTo ?? s.color} />
            </linearGradient>
          ))}

          {glossy && (
            <>
              {/* Specular band across the top of each segment. */}
              <linearGradient id={`${uid}-sheen`} x1="0" y1="0" x2="0" y2="1">
                <stop offset="0%" stopColor="#fff" stopOpacity={0.42} />
                <stop offset="100%" stopColor="#fff" stopOpacity={0} />
              </linearGradient>
              {/* Left-edge highlight running the height of the stack. */}
              <linearGradient id={`${uid}-edge`} x1="0" y1="0" x2="1" y2="0">
                <stop offset="0%" stopColor="#fff" stopOpacity={0.3} />
                <stop offset="55%" stopColor="#fff" stopOpacity={0} />
              </linearGradient>
              <filter
                id={`${uid}-ground`}
                x="-60%"
                y="-60%"
                width="220%"
                height="220%"
              >
                <feGaussianBlur stdDeviation={3} />
              </filter>
            </>
          )}
        </defs>

        {/* Gridlines and 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>
        ))}

        {model.shown.map((column, ci) => {
          const cx = MARGIN.left + ci * step + (step - colW) / 2;
          const stackTop = yOf(column.total);
          const stackH = baseline - stackTop;
          const isActive = hoveredIndex === column.rowIndex;
          const dimmed = hoveredIndex !== null && !isActive;
          const clipId = `${uid}-clip-${ci}`;

          // Segment offsets, largest at the base.
          let cursorY = baseline;
          const laid = column.segments.map((seg) => {
            const h = (seg.value / domainMax) * plotH;
            cursorY -= h;
            // Resolve the gradient here, not inline: findIndex returning -1 would
            // build `url(#…-fill--1)`, an invalid reference that paints nothing.
            // A solid fill is visibly wrong; an invisible bar is not.
            const idx = series.findIndex((s) => s.name === seg.name);
            return {
              ...seg,
              y: cursorY,
              h,
              paint: idx >= 0 ? `url(#${uid}-fill-${idx})` : seg.color,
            };
          });

          return (
            <g
              key={`col-${column.label}-${ci}`}
              onMouseEnter={() => onHoverIndex(column.rowIndex)}
              onClick={
                onColumnClick ? () => onColumnClick(column.label) : undefined
              }
              /* Without a handler the pointer would promise a drill-down that
                 doesn't exist. */
              style={{ cursor: onColumnClick ? "pointer" : "default" }}
            >
              <clipPath id={clipId}>
                <path d={topRoundedPath(cx, stackTop, colW, stackH, CORNER)} />
              </clipPath>

              {/* Ground shadow sits outside the clip so it can spread. */}
              {glossy && (
                <ellipse
                  cx={cx + colW / 2}
                  cy={baseline + 3}
                  rx={colW * 0.52}
                  ry={3}
                  fill="#0f172a"
                  opacity={dimmed ? 0.08 : 0.22}
                  filter={`url(#${uid}-ground)`}
                />
              )}

              {/* One transform per column grows the whole silhouette from the
                  baseline — cheaper and smoother than animating each segment,
                  and the rounded cap scales with it. */}
              <motion.g
                initial={reduce ? false : { scaleY: 0 }}
                animate={{ scaleY: 1, opacity: dimmed ? 0.42 : 1 }}
                transition={{
                  scaleY: {
                    duration: 0.6,
                    delay: reduce ? 0 : Math.min(ci * 0.045, 0.5),
                    ease: EASE_OUT,
                  },
                  opacity: { duration: 0.16 },
                }}
                // Explicit user-space origin at the column's base. Avoids relying
                // on transform-box, which behaves inconsistently on SVG groups.
                style={{ transformOrigin: `${cx + colW / 2}px ${baseline}px` }}
              >
                <g clipPath={`url(#${clipId})`}>
                  {laid.map((seg, si) => (
                    <g key={`seg-${seg.name}-${si}`}>
                      <rect
                        x={cx}
                        y={seg.y}
                        width={colW}
                        height={seg.h}
                        fill={seg.paint}
                      />
                      {glossy && seg.h > 3 && (
                        <rect
                          x={cx}
                          y={seg.y}
                          width={colW}
                          height={Math.min(10, seg.h * 0.5)}
                          fill={`url(#${uid}-sheen)`}
                        />
                      )}
                      {/* Hairline between stacked campaigns. */}
                      {si < laid.length - 1 && (
                        <line
                          x1={cx}
                          x2={cx + colW}
                          y1={seg.y}
                          y2={seg.y}
                          className={styles.barDivider}
                        />
                      )}
                    </g>
                  ))}

                  {glossy && (
                    <rect
                      x={cx}
                      y={stackTop}
                      width={colW}
                      height={stackH}
                      fill={`url(#${uid}-edge)`}
                    />
                  )}
                </g>

                {/* Outline on the hovered column, drawn over the fill. */}
                {isActive && (
                  <path
                    d={topRoundedPath(cx, stackTop, colW, stackH, CORNER)}
                    fill="none"
                    stroke="var(--promo-accent, var(--mantine-color-indigo-6))"
                    strokeWidth={1.6}
                  />
                )}
              </motion.g>

              {/* Total above the column. */}
              <text
                x={cx + colW / 2}
                y={stackTop - 6}
                textAnchor="middle"
                className={styles.barTotal}
                opacity={dimmed ? 0.35 : 1}
              >
                {column.total.toLocaleString()}
              </text>

              {/* Country label, tilted only when the pitch is too tight. */}
              <text
                x={tilt ? cx + colW / 2 : cx + colW / 2}
                y={baseline + (tilt ? 12 : 16)}
                textAnchor={tilt ? "end" : "middle"}
                transform={
                  tilt
                    ? `rotate(-35, ${cx + colW / 2}, ${baseline + 12})`
                    : undefined
                }
                className={styles.barAxisLabel}
                fontWeight={isActive ? 700 : 400}
                fill={
                  isActive
                    ? "var(--promo-accent, var(--mantine-color-indigo-6))"
                    : undefined
                }
              >
                {column.label}
              </text>
            </g>
          );
        })}

        {/* Baseline. */}
        <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}>
          Showing top {model.shown.length} countries · {model.hidden} more not
          charted
        </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}>
              {active.segments
                .slice()
                .sort((a, b) => b.value - a.value)
                .map((seg) => (
                  <div
                    key={seg.name}
                    style={{ display: "flex", alignItems: "center", gap: 6 }}
                  >
                    <span
                      className={styles.tooltipSwatch}
                      style={{ background: seg.color }}
                    />
                    <span style={{ fontWeight: 700 }}>
                      {seg.value.toLocaleString()}
                    </span>
                    <span>{seg.name}</span>
                  </div>
                ))}
            </Stack>
            {active.segments.length > 1 && (
              <div className={styles.tooltipTotal}>
                {active.total.toLocaleString()} total
              </div>
            )}
          </div>
        </Portal>
      )}
    </div>
  );
}
