import {
  ActionIcon,
  Badge,
  Group,
  Modal,
  Paper,
  SegmentedControl,
  Stack,
  Tabs,
  Text,
  Tooltip,
} from "@mantine/core";
import { useViewportSize } from "@mantine/hooks";
import {
  IconChartBar,
  IconChartLine,
  IconMap,
  IconMaximize,
  IconWorld,
} from "@tabler/icons-react";
import { useMemo, useState } from "react";
import { useSessionStorageState } from "@/hooks/useSessionStorageState";
import { usePromoTheme } from "./appearance";
import { areaOf, AREAS } from "./regions";
import {
  CountryChoropleth,
  type ChoroplethRow,
} from "./charts/CountryChoropleth";
import { CountryCampaignBars } from "./charts/CountryCampaignBars";
import { CountryCampaignLines } from "./charts/CountryCampaignLines";
import styles from "./charts/charts.module.css";
import { toMapKey } from "./charts/countryNames";

interface CountryBreakdownBarChartProps {
  title: string;
  entities: Array<{ id: string; name: string }>;
  rows: Array<{ country: string; id: string; signups: number }>;
  totalBadgeLabel?: string;
  /**
   * Makes a country actionable in the map and the bars.
   *
   * Optional so a page without a country filter doesn't advertise a click that
   * does nothing. Area grouping does not call it — an area is not a country, so
   * there is nothing to filter by.
   */
  onCountryClick?: (country: string) => void;
}

const OTHER_COLOR = "#898781";
const MAX_SLOTS = 8;

/** Chart heights per size preference. Taller than the old fixed 300/340. */
const HEIGHTS = { small: 260, normal: 420, tall: 560 } as const;
type ChartSize = keyof typeof HEIGHTS;

/** Chrome to subtract from the viewport when sizing the fullscreen chart. */
const FULLSCREEN_CHROME = 210;

export function CountryBreakdownBarChart({
  title,
  entities,
  rows,
  onCountryClick,
  totalBadgeLabel,
}: CountryBreakdownBarChartProps) {
  const [hoveredRow, setHoveredRow] = useState<number | null>(null);
  type View = "map" | "bars" | "lines";
  const [view, setView] = useState<View>("map");
  const [size, setSize] = useSessionStorageState<ChartSize>(
    "campaigns.countryChart.size",
    "normal",
  );
  const [grouping, setGrouping] = useSessionStorageState<"country" | "area">(
    "campaigns.countryChart.grouping",
    "country",
  );
  const [fullscreen, setFullscreen] = useState(false);
  const isArea = grouping === "area";
  const effectiveView: View = view;
  // Fullscreen height comes from the viewport, not a constant, so the chart fills
  // whatever screen it lands on.
  const { height: viewportHeight } = useViewportSize();
  const fullscreenHeight = Math.max(320, viewportHeight - FULLSCREEN_CHROME);

  // Series colours are derived from the page accent. Outside the promo theme
  // provider (the campaign-detail card) this falls back to the default palette.
  const { series: seriesPalette, muted } = usePromoTheme();

  const barChartData = useMemo(() => {
    const byCountry: Record<string, Record<string, number>> = {};
    const totalByEntity = new Map<string, number>();
    const entityIds = new Set(entities.map((e) => e.id));
    for (const row of rows) {
      if (!entityIds.has(row.id)) continue;
      // Area mode collapses many countries into a handful of buckets. The field
      // is still called `country` so the child charts need no change; only the
      // value it carries differs.
      const bucket =
        grouping === "area"
          ? areaOf(row.country) === "EU_UK_ROW"
            ? "EU / UK / ROW"
            : areaOf(row.country)
          : row.country;
      if (!byCountry[bucket]) byCountry[bucket] = {};
      byCountry[bucket][row.id] =
        (byCountry[bucket][row.id] ?? 0) + row.signups;
      totalByEntity.set(row.id, (totalByEntity.get(row.id) ?? 0) + row.signups);
    }

    const ranked = entities
      .filter((e) => (totalByEntity.get(e.id) ?? 0) > 0)
      .sort(
        (a, b) =>
          (totalByEntity.get(b.id) ?? 0) - (totalByEntity.get(a.id) ?? 0),
      );
    const direct =
      ranked.length <= MAX_SLOTS ? ranked : ranked.slice(0, MAX_SLOTS - 1);
    const overflow =
      ranked.length <= MAX_SLOTS ? [] : ranked.slice(MAX_SLOTS - 1);

    const palette = seriesPalette(direct.length);
    const series = direct.map((e, i) => ({
      key: e.id,
      name: e.name,
      color: palette[i].from,
      colorTo: palette[i].to,
    }));
    if (overflow.length > 0) {
      // "Other" stays deliberately neutral grey — it's a bucket, not a series,
      // so it shouldn't compete with the accent-derived colours.
      series.push({
        key: "__other__",
        name: `Other (${overflow.length})`,
        color: OTHER_COLOR,
        colorTo: OTHER_COLOR,
      });
    }

    const data = Object.entries(byCountry)
      .map(([country, counts]) => {
        const row: Record<string, string | number> = { country };
        let rowTotal = 0;
        for (const s of series) {
          const v =
            s.key === "__other__"
              ? overflow.reduce((sum, e) => sum + (counts[e.id] ?? 0), 0)
              : (counts[s.key] ?? 0);
          row[s.name] = v;
          rowTotal += v;
        }
        return { row, rowTotal };
      })
      .filter(({ rowTotal }) => rowTotal > 0)
      .sort((a, b) => b.rowTotal - a.rowTotal)
      .map(({ row }) => row);

    return { data, series };
  }, [entities, rows, seriesPalette, grouping]);

  const tooltipRows = useMemo(() => {
    if (hoveredRow === null) return null;
    const row = barChartData.data[hoveredRow];
    if (!row) return null;
    const entries = barChartData.series
      .map((s) => ({
        name: s.name,
        color: s.color,
        value: Number(row[s.name] ?? 0),
      }))
      .filter((e) => e.value > 0)
      .sort((a, b) => b.value - a.value);
    if (entries.length === 0) return null;
    const total = entries.reduce((sum, e) => sum + e.value, 0);
    return { country: String(row.country ?? ""), entries, total };
  }, [hoveredRow, barChartData]);

  /*
   * Map rows.
   *
   * The map always plots *countries* — "SEAP" has no geometry — so in area mode
   * it cannot use the grouped rows. It rebuilds per-country totals from the raw
   * input and tags each with its area, letting the choropleth colour
   * categorically by area instead of by signup intensity.
   */
  const areaMapRows = useMemo<ChoroplethRow[]>(() => {
    if (!isArea) return [];
    const entityIds = new Set(entities.map((e) => e.id));
    const byCountry = new Map<string, number>();
    for (const row of rows) {
      if (!entityIds.has(row.id)) continue;
      const name = (row.country ?? "").trim();
      if (!name) continue;
      byCountry.set(name, (byCountry.get(name) ?? 0) + row.signups);
    }
    const palette = seriesPalette(AREAS.length);
    return Array.from(byCountry, ([country, value]) => {
      const area = areaOf(country);
      const index = AREAS.indexOf(area);
      // Only the no-country bucket is muted: it isn't a geography, so it shouldn't
      // take a colour from the accent fan alongside the real areas. Muted is a pale
      // tint of the accent rather than a fixed grey, so it still follows the theme.
      const neutral = area === "No Country Info";
      return {
        country,
        value,
        areaLabel: area === "EU_UK_ROW" ? "EU / UK / ROW" : area,
        areaColor: neutral ? muted.from : palette[Math.max(0, index)].from,
      };
    });
  }, [isArea, rows, entities, seriesPalette, muted]);

  // Map rows: one total per country, plus the campaign split for its tooltip.
  const countryMapRows = useMemo<ChoroplethRow[]>(
    () =>
      barChartData.data.map((row) => {
        const breakdown = barChartData.series
          .map((s) => ({
            name: s.name,
            color: s.color,
            value: Number(row[s.name] ?? 0),
          }))
          .filter((e) => e.value > 0)
          .sort((a, b) => b.value - a.value);
        return {
          country: String(row.country ?? ""),
          value: breakdown.reduce((sum, e) => sum + e.value, 0),
          breakdown,
        };
      }),
    [barChartData],
  );

  const mapRows = isArea ? areaMapRows : countryMapRows;

  /*
   * Hover the map owns directly.
   *
   * This used to be derived purely from `hoveredRow`, an index into
   * `barChartData.data` — which is capped at MAX_SLOTS and buckets the tail into
   * "Other". Hovering any country outside the top few therefore produced
   * findIndex === -1, hoveredRow stayed null, and no tooltip appeared. Area mode
   * was worse: the map plots every country while the bars are grouped, so almost
   * nothing matched.
   *
   * The map's own key is authoritative for the map, and syncing to a bar row is
   * now a best-effort extra rather than a precondition for the tooltip.
   */
  const [mapHoverKey, setMapHoverKey] = useState<string | null>(null);

  const hoveredMapKey = useMemo(() => {
    if (mapHoverKey) return mapHoverKey;
    // Hover that started on the bars still highlights the matching country.
    if (hoveredRow === null) return null;
    const name = barChartData.data[hoveredRow]?.country;
    return name ? toMapKey(String(name)) : null;
  }, [mapHoverKey, hoveredRow, barChartData]);

  const handleMapHover = (mapKey: string | null) => {
    setMapHoverKey(mapKey);
    if (mapKey === null) {
      setHoveredRow(null);
      return;
    }
    // Cross-highlight the bar for this country when it has one. A miss is fine
    // and no longer suppresses the map tooltip.
    const index = barChartData.data.findIndex(
      (row) => toMapKey(String(row.country ?? "")) === mapKey,
    );
    setHoveredRow(index >= 0 ? index : null);
  };

  /**
   * The tab group at a given height. Rendered inline *or* inside the fullscreen
   * modal — never both, so only one set of charts is ever mounted and measured.
   */
  const chartGroup = (h: number) => (
    <Tabs
      value={effectiveView}
      onChange={(v) => {
        setView((v as View) ?? "bars");
        // Hover belongs to whichever view was showing; carrying it across would
        // leave a highlight with no pointer near it.
        setHoveredRow(null);
        setMapHoverKey(null);
      }}
      variant="pills"
      radius="md"
      // Load-bearing. Mantine defaults keepMounted to true and hides the inactive
      // panel with `display: none` — an element with no box measures 0 wide, so a
      // chart sized by ParentSize renders nothing and never recovers. Mounting on
      // selection guarantees the chart measures a visible element.
      keepMounted={false}
    >
      <Tabs.List mb="sm">
        <Tabs.Tab value="map" leftSection={<IconMap size={14} />}>
          Map
        </Tabs.Tab>
        {/* Each value must match a Tabs.Panel below — a mismatch renders an
            empty panel with no error. */}
        <Tabs.Tab value="bars" leftSection={<IconChartBar size={14} />}>
          Bars
        </Tabs.Tab>
        <Tabs.Tab value="lines" leftSection={<IconChartLine size={14} />}>
          Lines
        </Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="map">
        <CountryChoropleth
          rows={mapRows}
          height={h}
          hoveredKey={hoveredMapKey}
          onHoverCountry={handleMapHover}
          onCountryClick={onCountryClick}
        />
      </Tabs.Panel>

      <Tabs.Panel value="bars">
        <CountryCampaignBars
          data={barChartData.data}
          series={barChartData.series}
          xKey="country"
          height={h}
          hoveredIndex={hoveredRow}
          onHoverIndex={setHoveredRow}
          /* Not wired in area grouping: the x label is an area there, which is
             not something the country filter can take. */
          onColumnClick={isArea ? undefined : onCountryClick}
        />
      </Tabs.Panel>

      <Tabs.Panel value="lines">
        <CountryCampaignLines
          data={barChartData.data}
          series={barChartData.series}
          xKey="country"
          height={h}
          hoveredIndex={hoveredRow}
          onHoverIndex={setHoveredRow}
        />
      </Tabs.Panel>
    </Tabs>
  );

  const legend = (
    <Group gap={10} mt="md" wrap="wrap">
      {barChartData.series.map((s) => (
        <Group key={s.name} gap={8} wrap="nowrap" className={styles.legendPill}>
          <span
            className={styles.legendPillDot}
            style={{ background: s.color }}
          />
          <Text size="sm" fw={600} lineClamp={1}>
            {s.name}
          </Text>
        </Group>
      ))}
    </Group>
  );

  return (
    // gray-0 stays light in the dark scheme, so it needs light-dark() rather
    // than a fixed token.
    <Paper
      withBorder
      radius="md"
      p="md"
      bg="light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7))"
    >
      <Group justify="space-between" mb="sm" align="center">
        <Group gap={8} align="center">
          <IconWorld size={16} stroke={1.6} />
          <Text size="sm" fw={700}>
            {isArea ? title.replace(/^Country/, "Area") : title}
          </Text>
          {barChartData.data.length > 0 && (
            <Badge variant="light" size="md" radius="sm">
              {barChartData.data.length}{" "}
              {isArea
                ? `area${barChartData.data.length === 1 ? "" : "s"}`
                : `countr${barChartData.data.length === 1 ? "y" : "ies"}`}
            </Badge>
          )}
        </Group>
        <Group gap="sm" align="center" wrap="nowrap">
          {barChartData.data.length > 0 && totalBadgeLabel && (
            <Badge variant="light" size="md" radius="sm">
              {totalBadgeLabel}
            </Badge>
          )}

          {barChartData.data.length > 0 && (
            <Group gap={6} align="center" wrap="nowrap">
              <SegmentedControl
                size="xs"
                value={grouping}
                onChange={(v) => {
                  setGrouping(v as "country" | "area");
                  setHoveredRow(null);
                }}
                data={[
                  { value: "country", label: "Country" },
                  { value: "area", label: "Area" },
                ]}
                aria-label="Group by"
              />
              <SegmentedControl
                size="xs"
                value={size}
                onChange={(v) => setSize(v as ChartSize)}
                data={[
                  { value: "small", label: "S" },
                  { value: "normal", label: "M" },
                  { value: "tall", label: "L" },
                ]}
                aria-label="Chart height"
              />
              <Tooltip label="Fullscreen" withArrow>
                <ActionIcon
                  size="sm"
                  variant="default"
                  aria-label="Open charts fullscreen"
                  onClick={() => setFullscreen(true)}
                >
                  <IconMaximize size={14} />
                </ActionIcon>
              </Tooltip>
            </Group>
          )}
        </Group>
      </Group>
      {barChartData.data.length === 0 ? (
        <Stack align="center" justify="center" py="xl" gap={6}>
          <IconWorld size={40} stroke={1.2} opacity={0.3} />
          <Text c="dimmed" size="sm">
            No country data in range.
          </Text>
        </Stack>
      ) : (
        <>
          {/* Three views of the same filtered data: the map answers "where", the
              bars and lines answer "which campaign". Each owns its popup. */}
          {/* Rendered inline only while the modal is closed — mounting both
              would measure and animate two copies of every chart. */}
          {!fullscreen && chartGroup(HEIGHTS[size])}
          {!fullscreen && legend}

          <Modal
            opened={fullscreen}
            onClose={() => setFullscreen(false)}
            fullScreen
            withCloseButton
            title={
              <Group gap={8} align="center">
                <IconWorld size={16} stroke={1.6} />
                <Text fw={700}>{title}</Text>
                <Badge variant="light" size="sm" radius="sm">
                  {barChartData.data.length} countr
                  {barChartData.data.length === 1 ? "y" : "ies"}
                </Badge>
              </Group>
            }
          >
            {fullscreen && chartGroup(fullscreenHeight)}
            {fullscreen && legend}
          </Modal>
        </>
      )}
    </Paper>
  );
}
