/**
 * WorldMap — SVG world map showing per-country ban counts.
 *
 * Uses a local TopoJSON bundle and d3-geo for projection, path generation,
 * and native SVG pan/zoom behaviour.
 */

import { createPortal } from "react-dom";
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { Button, makeStyles, tokens } from "@fluentui/react-components";
import { geoMercator, geoPath } from "d3-geo";
import { feature } from "topojson-client";
import type { FeatureCollection } from "geojson";
import type {
  GeometryCollection as TopoGeometryCollection,
  Topology,
} from "topojson-specification";
import worldData from "world-atlas/countries-110m.json";
import { useCardStyles } from "../components/commonStyles";
import { ISO_NUMERIC_TO_ALPHA2 } from "../utils/isoNumericToAlpha2";
import { getBanCountColor } from "../utils/mapColors";

const MAP_WIDTH = 800;
const MAP_HEIGHT = 400;
const MIN_ZOOM = 1;
const MAX_ZOOM = 8;
const ZOOM_STEP = 0.5;
const PAN_THRESHOLD = 3;
const MAP_COUNTRY_STROKE_WIDTH = 0.75;

const useStyles = makeStyles({
  mapWrapper: {
    width: "100%",
    position: "relative",
    overflow: "hidden",
  },
  svg: {
    width: "100%",
    height: "auto",
    touchAction: "none",
  },
  country: {
    transition: "fill 150ms ease, stroke 150ms ease",
    stroke: tokens.colorNeutralStroke2,
    strokeWidth: MAP_COUNTRY_STROKE_WIDTH,
    fill: "var(--country-fill)",
    outline: "none",
    cursor: "pointer",
  },
  countryHovered: {
    fill: "var(--country-hover-fill)",
  },
  countrySelected: {
    fill: "var(--country-selected-fill)",
  },
  countLabel: {
    fontSize: tokens.fontSizeBase200,
    fontWeight: "600",
    fill: tokens.colorNeutralForeground1,
    pointerEvents: "none",
    userSelect: "none",
  },
  zoomControls: {
    position: "absolute",
    top: tokens.spacingVerticalM,
    right: tokens.spacingHorizontalM,
    display: "flex",
    flexDirection: "column",
    gap: tokens.spacingVerticalXS,
    zIndex: 10,
  },
  tooltip: {
    position: "fixed",
    zIndex: 9999,
    pointerEvents: "none",
    backgroundColor: tokens.colorNeutralBackground1,
    border: `1px solid ${tokens.colorNeutralStroke2}`,
    borderRadius: tokens.borderRadiusSmall,
    padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalS}`,
    display: "flex",
    flexDirection: "column",
    gap: tokens.spacingVerticalXXS,
    boxShadow: tokens.shadow4,
  },
  tooltipCountry: {
    fontSize: tokens.fontSizeBase200,
    fontWeight: tokens.fontWeightSemibold,
    color: tokens.colorNeutralForeground1,
  },
  tooltipCount: {
    fontSize: tokens.fontSizeBase200,
    color: tokens.colorNeutralForeground2,
  },
});

type TopoJsonTopology = Topology & {
  objects: {
    countries: TopoGeometryCollection;
  };
};

type TooltipState = {
  cc: string;
  count: number;
  name: string;
  x: number;
  y: number;
} | null;

interface WorldMapProps {
  /** ISO alpha-2 country code → ban count. */
  countries: Record<string, number>;
  /** Optional mapping from country code to display name. */
  countryNames?: Record<string, string>;
  /** Currently selected country filter (null means no filter). */
  selectedCountry: string | null;
  /** Called when the user clicks a country or deselects. */
  onSelectCountry: (cc: string | null) => void;
  /** Ban count threshold for green coloring (default: 20). */
  thresholdLow?: number;
  /** Ban count threshold for yellow coloring (default: 50). */
  thresholdMedium?: number;
  /** Ban count threshold for red coloring (default: 100). */
  thresholdHigh?: number;
}

export function WorldMap({
  countries,
  countryNames,
  selectedCountry,
  onSelectCountry,
  thresholdLow = 20,
  thresholdMedium = 50,
  thresholdHigh = 100,
}: WorldMapProps): React.JSX.Element {
  const styles = useStyles();
  const cardStyles = useCardStyles();
  const [zoom, setZoom] = useState<number>(MIN_ZOOM);
  const [center, setCenter] = useState<[number, number]>([0, 0]);
  const [hoveredCountry, setHoveredCountry] = useState<string | null>(null);
  const [tooltip, setTooltip] = useState<TooltipState>(null);

  const zoomRef = useRef<number>(zoom);
  const centerRef = useRef<[number, number]>(center);
  const dragStateRef = useRef<{
    active: boolean;
    startX: number;
    startY: number;
    startCenter: [number, number];
    moved: boolean;
  } | null>(null);
  const clickSuppressedRef = useRef<boolean>(false);

  useEffect(() => {
    zoomRef.current = zoom;
  }, [zoom]);

  useEffect(() => {
    centerRef.current = center;
  }, [center]);

  const topology = useMemo(() => worldData as unknown as TopoJsonTopology, []);

  const geoJson = useMemo(
    () =>
      feature(topology, topology.objects.countries) as FeatureCollection,
    [topology],
  );

  const projection = useMemo(
    () => geoMercator().fitSize([MAP_WIDTH, MAP_HEIGHT], geoJson),
    [geoJson],
  );

  const pathGenerator = useMemo(
    () => geoPath().projection(projection),
    [projection],
  );

  const countryFeatures = useMemo(
    () => geoJson.features,
    [geoJson.features],
  );

  const clampZoom = useCallback((value: number) => Math.min(Math.max(value, MIN_ZOOM), MAX_ZOOM), []);

  const handleCountrySelect = useCallback(
    (cc: string | null): void => {
      if (clickSuppressedRef.current) {
        return;
      }

      onSelectCountry(selectedCountry === cc ? null : cc);
    },
    [onSelectCountry, selectedCountry],
  );

  /** SVG-level click handler — paths never receive click when pointer capture
   *  is active on the SVG, so we resolve the target via the data-cc attribute. */
  const handleSvgClick = useCallback((event: React.MouseEvent<SVGSVGElement>) => {
    const target = (event.target as Element).closest('[data-cc]');
    const cc = target?.getAttribute('data-cc') ?? null;
    if (cc) handleCountrySelect(cc);
  }, [handleCountrySelect]);

  const handlePointerDown = useCallback((event: React.PointerEvent<SVGSVGElement>) => {
    if (event.button !== 0) return;
    dragStateRef.current = {
      active: true,
      startX: event.clientX,
      startY: event.clientY,
      startCenter: centerRef.current,
      moved: false,
    };
    clickSuppressedRef.current = false;
  }, []);

  const handlePointerMove = useCallback((event: React.PointerEvent<SVGSVGElement>) => {
    const drag = dragStateRef.current;
    if (!drag?.active) return;

    const dx = event.clientX - drag.startX;
    const dy = event.clientY - drag.startY;
    if (!drag.moved && Math.hypot(dx, dy) > PAN_THRESHOLD) {
      drag.moved = true;
      clickSuppressedRef.current = true;
      event.currentTarget.setPointerCapture(event.pointerId);
    }

    setCenter([drag.startCenter[0] + dx, drag.startCenter[1] + dy]);
  }, []);

  const handlePointerUp = useCallback((event: React.PointerEvent<SVGSVGElement>) => {
    const drag = dragStateRef.current;
    if (!drag) return;

    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }

    dragStateRef.current = null;
    window.setTimeout(() => {
      clickSuppressedRef.current = false;
    }, 0);
  }, []);

  const handleWheel = useCallback((event: React.WheelEvent<SVGSVGElement>) => {
    event.preventDefault();

    const currentZoom = zoomRef.current;
    const desiredZoom = clampZoom(currentZoom + (event.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP));
    if (desiredZoom === currentZoom) {
      return;
    }

    const rect = event.currentTarget.getBoundingClientRect();
    const svgX = (event.clientX - rect.left - centerRef.current[0]) / currentZoom;
    const svgY = (event.clientY - rect.top - centerRef.current[1]) / currentZoom;

    setZoom(desiredZoom);
    setCenter([
      centerRef.current[0] - svgX * (desiredZoom - currentZoom),
      centerRef.current[1] - svgY * (desiredZoom - currentZoom),
    ]);
  }, [clampZoom]);

  const handleZoomIn = useCallback(() => {
    setZoom((value) => clampZoom(value + ZOOM_STEP));
  }, [clampZoom]);

  const handleZoomOut = useCallback(() => {
    setZoom((value) => clampZoom(value - ZOOM_STEP));
  }, [clampZoom]);

  const handleResetView = useCallback(() => {
    setZoom(MIN_ZOOM);
    setCenter([0, 0]);
  }, []);

  return (
    <div
      className={`${cardStyles.card} ${styles.mapWrapper}`}
      role="img"
      aria-label="World map showing banned IP counts by country. Click a country to filter the table below."
    >
      <div className={styles.zoomControls}>
        <Button
          appearance="secondary"
          size="small"
          onClick={handleZoomIn}
          disabled={zoom >= MAX_ZOOM}
          title="Zoom in"
          aria-label="Zoom in"
        >
          +
        </Button>
        <Button
          appearance="secondary"
          size="small"
          onClick={handleZoomOut}
          disabled={zoom <= MIN_ZOOM}
          title="Zoom out"
          aria-label="Zoom out"
        >
          −
        </Button>
        <Button
          appearance="secondary"
          size="small"
          onClick={handleResetView}
          disabled={zoom === MIN_ZOOM && center[0] === 0 && center[1] === 0}
          title="Reset view"
          aria-label="Reset view"
        >
          ⟲
        </Button>
      </div>

      <svg
        className={styles.svg}
        viewBox={`0 0 ${String(MAP_WIDTH)} ${String(MAP_HEIGHT)}`}
        role="img"
        aria-label="World map showing banned IP counts by country."
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
        onPointerLeave={handlePointerUp}
        onWheel={handleWheel}
        onClick={handleSvgClick}
      >
        <g transform={`translate(${String(center[0])} ${String(center[1])}) scale(${String(zoom)})`}>
          {countryFeatures.map((featureItem) => {
            const rawId = featureItem.id;
            const numericId = String(Number(rawId));
            const cc = ISO_NUMERIC_TO_ALPHA2[numericId] ?? null;
            const count = cc !== null ? countries[cc] ?? 0 : 0;
            const isSelected = cc !== null && selectedCountry === cc;
            const fillColor = getBanCountColor(count, thresholdLow, thresholdMedium, thresholdHigh);
            const pathString = pathGenerator(featureItem) ?? "";
            if (!pathString) {
              return null;
            }

            return (
              <g key={String(rawId)}>
                <path
                  d={pathString}
                  data-cc={cc ?? undefined}
                  role={cc ? "button" : undefined}
                  tabIndex={cc ? 0 : undefined}
                  aria-label={
                    cc
                      ? `${cc}: ${String(count)} ban${count !== 1 ? "s" : ""}${
                          isSelected ? " (selected)" : ""
                        }`
                      : undefined
                  }
                  aria-pressed={isSelected || undefined}
                  className={`${styles.country} ${
                    isSelected ? styles.countrySelected : ""
                  } ${hoveredCountry === cc ? styles.countryHovered : ""}`}
                  style={
                    {
                      ["--country-fill" as string]: fillColor,
                      ["--country-hover-fill" as string]: isSelected
                        ? tokens.colorBrandBackgroundHover
                        : tokens.colorBrandBackground2,
                      ["--country-selected-fill" as string]: tokens.colorBrandBackground,
                    } as React.CSSProperties
                  }
                  onClick={(): void => {
                    if (cc) handleCountrySelect(cc);
                  }}
                  onKeyDown={(event): void => {
                    if (cc && (event.key === "Enter" || event.key === " ")) {
                      event.preventDefault();
                      handleCountrySelect(cc);
                    }
                  }}
                  onMouseEnter={(event): void => {
                    if (!cc) return;
                    setHoveredCountry(cc);
                    setTooltip({
                      cc,
                      count,
                      name: countryNames?.[cc] ?? cc,
                      x: event.clientX,
                      y: event.clientY,
                    });
                  }}
                  onMouseMove={(event): void => {
                    setTooltip((current) =>
                      current
                        ? { ...current, x: event.clientX, y: event.clientY }
                        : current,
                    );
                  }}
                  onMouseLeave={(): void => {
                    setHoveredCountry(null);
                    setTooltip(null);
                  }}
                />
              </g>
            );
          })}
        </g>
      </svg>

      {tooltip &&
        createPortal(
          <div
            className={styles.tooltip}
            style={{ left: tooltip.x + 12, top: tooltip.y + 12 }}
            role="tooltip"
            aria-live="polite"
          >
            <span className={styles.tooltipCountry}>{tooltip.name}</span>
            <span className={styles.tooltipCount}>
              {tooltip.count.toLocaleString()} ban{tooltip.count !== 1 ? "s" : ""}
            </span>
          </div>,
          document.body,
        )}
    </div>
  );
}
