import { useEffect, useMemo, useRef, useState } from "react";
import worldMap from "../data/world.json";
import { formatMoney, formatNumber } from "../utils/format.js";

const DEGREE = Math.PI / 180;
const INITIAL_ROTATION = { lat: 8, lon: 65 };
const MAX_LATITUDE = 58;
const LAND_RINGS = extractRings(worldMap.features);

export function GlobeMap({ items }) {
  const canvasRef = useRef(null);
  const wrapRef = useRef(null);
  const dragRef = useRef(null);
  const [dragging, setDragging] = useState(false);
  const [hovered, setHovered] = useState(null);
  const [rotation, setRotation] = useState(INITIAL_ROTATION);
  const [size, setSize] = useState({ height: 0, width: 0 });
  const points = useMemo(() => normalizePoints(items), [items]);

  useEffect(() => {
    const element = wrapRef.current;
    if (!element) {
      return undefined;
    }

    const syncSize = () => {
      const rect = element.getBoundingClientRect();
      setSize({ height: Math.max(1, Math.round(rect.height)), width: Math.max(1, Math.round(rect.width)) });
    };

    syncSize();
    const observer = typeof ResizeObserver === "function" ? new ResizeObserver(syncSize) : null;
    observer?.observe(element);
    window.addEventListener("resize", syncSize);

    return () => {
      observer?.disconnect();
      window.removeEventListener("resize", syncSize);
    };
  }, []);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || !size.width || !size.height) {
      return;
    }
    drawGlobe(canvas, size, rotation, points);
  }, [points, rotation, size]);

  const onPointerDown = (event) => {
    event.currentTarget.setPointerCapture(event.pointerId);
    dragRef.current = {
      lat: rotation.lat,
      lon: rotation.lon,
      x: event.clientX,
      y: event.clientY
    };
    setHovered(null);
    setDragging(true);
  };

  const onPointerMove = (event) => {
    const drag = dragRef.current;
    if (!drag) {
      setHovered(findHoveredPoint(event, wrapRef.current, size, rotation, points));
      return;
    }
    const dx = event.clientX - drag.x;
    const dy = event.clientY - drag.y;
    setRotation({
      lat: clamp(drag.lat + dy * 0.24, -MAX_LATITUDE, MAX_LATITUDE),
      lon: normalizeLongitude(drag.lon - dx * 0.34)
    });
  };

  const endDrag = (event) => {
    if (dragRef.current) {
      event.currentTarget.releasePointerCapture?.(event.pointerId);
    }
    dragRef.current = null;
    setDragging(false);
    setHovered(findHoveredPoint(event, wrapRef.current, size, rotation, points));
  };

  return (
    <div
      aria-label="可旋转全球充值分布"
      className={["globe-map", dragging ? "is-dragging" : ""].filter(Boolean).join(" ")}
      onPointerCancel={endDrag}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={endDrag}
      onPointerLeave={() => setHovered(null)}
      ref={wrapRef}
      role="img"
    >
      <canvas className="globe-canvas" ref={canvasRef} />
      {hovered ? (
        <div className="globe-tooltip" style={{ left: hovered.x, top: hovered.y }}>
          <strong>{hovered.item.country}</strong>
          <span>充值 {formatMoney(hovered.item.value)}</span>
          <span>活跃 {formatNumber(hovered.item.activeUsers)}</span>
          <span>付费 {formatNumber(hovered.item.paidUsers)}</span>
        </div>
      ) : null}
    </div>
  );
}

function drawGlobe(canvas, size, rotation, points) {
  const dpr = window.devicePixelRatio || 1;
  canvas.width = Math.round(size.width * dpr);
  canvas.height = Math.round(size.height * dpr);
  canvas.style.width = `${size.width}px`;
  canvas.style.height = `${size.height}px`;

  const context = canvas.getContext("2d");
  context.setTransform(dpr, 0, 0, dpr, 0, 0);
  context.clearRect(0, 0, size.width, size.height);

  const cx = size.width / 2;
  const cy = size.height / 2;
  const radius = Math.max(1, Math.min(size.width * 0.48, size.height * 0.47));

  drawAtmosphere(context, cx, cy, radius);

  context.save();
  context.beginPath();
  context.arc(cx, cy, radius, 0, Math.PI * 2);
  context.clip();

  drawGraticule(context, rotation, cx, cy, radius);
  drawLand(context, rotation, cx, cy, radius);
  drawRoutes(context, points, rotation, cx, cy, radius);
  drawPoints(context, points, rotation, cx, cy, radius);
  drawRankMarkers(context, points, rotation, cx, cy, radius);

  context.restore();
  drawRim(context, cx, cy, radius);
}

function drawAtmosphere(context, cx, cy, radius) {
  context.save();
  context.shadowBlur = 34;
  context.shadowColor = "rgba(35, 228, 255, 0.22)";
  context.fillStyle = "rgba(5, 31, 52, 0.92)";
  context.beginPath();
  context.arc(cx, cy, radius + 1, 0, Math.PI * 2);
  context.fill();
  context.restore();

  const ocean = context.createRadialGradient(cx - radius * 0.42, cy - radius * 0.48, radius * 0.08, cx, cy, radius);
  ocean.addColorStop(0, "#144766");
  ocean.addColorStop(0.46, "#082c49");
  ocean.addColorStop(0.76, "#061f36");
  ocean.addColorStop(1, "#04182d");
  context.fillStyle = ocean;
  context.beginPath();
  context.arc(cx, cy, radius, 0, Math.PI * 2);
  context.fill();
}

function drawGraticule(context, rotation, cx, cy, radius) {
  context.save();
  context.lineWidth = 0.65;
  context.strokeStyle = "rgba(102, 184, 222, 0.16)";

  for (let lat = -60; lat <= 60; lat += 30) {
    drawProjectedLine(
      context,
      Array.from({ length: 91 }, (_, index) => [-180 + index * 4, lat]),
      rotation,
      cx,
      cy,
      radius
    );
  }

  for (let lon = -150; lon <= 180; lon += 30) {
    drawProjectedLine(
      context,
      Array.from({ length: 41 }, (_, index) => [lon, -80 + index * 4]),
      rotation,
      cx,
      cy,
      radius
    );
  }

  context.restore();
}

function drawLand(context, rotation, cx, cy, radius) {
  context.save();
  context.fillStyle = "rgba(25, 63, 91, 0.9)";
  context.strokeStyle = "rgba(75, 148, 190, 0.58)";
  context.lineWidth = 0.62;

  for (const ring of LAND_RINGS) {
    drawRing(context, ring, rotation, cx, cy, radius);
  }

  context.restore();
}

function drawRoutes(context, points, rotation, cx, cy, radius) {
  if (points.length < 2) {
    return;
  }

  const hub = points[0];
  const hubPoint = project(hub.point, rotation, cx, cy, radius);
  if (!hubPoint) {
    return;
  }

  context.save();
  context.lineWidth = 0.8;
  context.strokeStyle = "rgba(43, 230, 246, 0.22)";

  for (const target of points.slice(1, 8)) {
    const targetPoint = project(target.point, rotation, cx, cy, radius);
    if (!targetPoint) {
      continue;
    }
    const midX = (hubPoint.x + targetPoint.x) / 2;
    const midY = (hubPoint.y + targetPoint.y) / 2;
    const lift = Math.min(36, Math.hypot(targetPoint.x - hubPoint.x, targetPoint.y - hubPoint.y) * 0.22);
    context.beginPath();
    context.moveTo(hubPoint.x, hubPoint.y);
    context.quadraticCurveTo(midX, midY - lift, targetPoint.x, targetPoint.y);
    context.stroke();
  }

  context.restore();
}

function drawPoints(context, points, rotation, cx, cy, radius) {
  const max = Math.max(...points.map((item) => item.value), 1);

  for (const item of points) {
    const point = project(item.point, rotation, cx, cy, radius);
    if (!point) {
      continue;
    }
    const strength = Math.sqrt(item.value / max);
    const halo = 10 + strength * 24;
    const core = 3.2 + strength * 5.4;
    const glow = context.createRadialGradient(point.x, point.y, 1, point.x, point.y, halo);
    glow.addColorStop(0, "rgba(129, 252, 255, 0.86)");
    glow.addColorStop(0.2, "rgba(39, 228, 245, 0.42)");
    glow.addColorStop(1, "rgba(39, 228, 245, 0)");

    context.fillStyle = glow;
    context.beginPath();
    context.arc(point.x, point.y, halo, 0, Math.PI * 2);
    context.fill();

    context.strokeStyle = "rgba(126, 252, 255, 0.42)";
    context.lineWidth = 1;
    context.beginPath();
    context.arc(point.x, point.y, core * 2.2, 0, Math.PI * 2);
    context.stroke();

    context.fillStyle = "#35f0f3";
    context.shadowBlur = 16;
    context.shadowColor = "rgba(53, 240, 243, 0.78)";
    context.beginPath();
    context.arc(point.x, point.y, core, 0, Math.PI * 2);
    context.fill();
    context.shadowBlur = 0;
  }
}

function drawRankMarkers(context, points, rotation, cx, cy, radius) {
  const visible = points
    .map((item, index) => ({ index, item, point: project(item.point, rotation, cx, cy, radius) }))
    .filter((item) => item.point)
    .filter((item) => item.index < 5);

  if (!visible.length) {
    return;
  }

  context.save();
  context.textAlign = "center";
  context.textBaseline = "middle";
  context.font = "800 11px Inter, Arial, sans-serif";

  for (const marker of visible) {
    const rank = marker.index + 1;
    const markerRadius = rank <= 2 ? 10.5 : 9.5;

    context.shadowBlur = 16;
    context.shadowColor = "rgba(47, 232, 243, 0.55)";
    context.fillStyle = "rgba(4, 31, 50, 0.92)";
    context.beginPath();
    context.arc(marker.point.x, marker.point.y, markerRadius, 0, Math.PI * 2);
    context.fill();
    context.shadowBlur = 0;

    context.lineWidth = 1.5;
    context.strokeStyle = rank <= 2 ? "rgba(143, 255, 255, 0.92)" : "rgba(76, 224, 241, 0.72)";
    context.stroke();

    context.fillStyle = rank <= 2 ? "#f0fcff" : "#bdf9ff";
    context.fillText(String(rank), marker.point.x, marker.point.y + 0.5);
  }

  context.restore();
}

function drawRim(context, cx, cy, radius) {
  context.save();
  const rim = context.createRadialGradient(cx, cy, radius * 0.7, cx, cy, radius * 1.04);
  rim.addColorStop(0, "rgba(37, 228, 245, 0)");
  rim.addColorStop(0.78, "rgba(37, 228, 245, 0.02)");
  rim.addColorStop(1, "rgba(37, 228, 245, 0.34)");
  context.fillStyle = rim;
  context.beginPath();
  context.arc(cx, cy, radius * 1.04, 0, Math.PI * 2);
  context.fill();

  context.lineWidth = 1.1;
  context.strokeStyle = "rgba(86, 186, 226, 0.42)";
  context.beginPath();
  context.arc(cx, cy, radius, 0, Math.PI * 2);
  context.stroke();
  context.restore();
}

function drawRing(context, ring, rotation, cx, cy, radius) {
  context.beginPath();
  let started = false;
  let hasPoint = false;

  for (const coordinate of ring) {
    const point = project(coordinate, rotation, cx, cy, radius);
    if (!point) {
      started = false;
      continue;
    }
    if (!started) {
      context.moveTo(point.x, point.y);
      started = true;
    } else {
      context.lineTo(point.x, point.y);
    }
    hasPoint = true;
  }

  if (hasPoint) {
    context.fill();
    context.stroke();
  }
}

function drawProjectedLine(context, coordinates, rotation, cx, cy, radius) {
  context.beginPath();
  let started = false;

  for (const coordinate of coordinates) {
    const point = project(coordinate, rotation, cx, cy, radius);
    if (!point) {
      started = false;
      continue;
    }
    if (!started) {
      context.moveTo(point.x, point.y);
      started = true;
    } else {
      context.lineTo(point.x, point.y);
    }
  }

  context.stroke();
}

function project(coordinate, rotation, cx, cy, radius) {
  const lon = coordinate[0] * DEGREE;
  const lat = coordinate[1] * DEGREE;
  const centerLon = rotation.lon * DEGREE;
  const centerLat = rotation.lat * DEGREE;
  const deltaLon = lon - centerLon;
  const cosc = Math.sin(centerLat) * Math.sin(lat) + Math.cos(centerLat) * Math.cos(lat) * Math.cos(deltaLon);

  if (cosc <= 0.02) {
    return null;
  }

  return {
    x: cx + radius * Math.cos(lat) * Math.sin(deltaLon),
    y: cy - radius * (Math.cos(centerLat) * Math.sin(lat) - Math.sin(centerLat) * Math.cos(lat) * Math.cos(deltaLon))
  };
}

function normalizePoints(items = []) {
  return items
    .filter((item) => Array.isArray(item.geo_point) && item.geo_point.length === 2)
    .map((item) => ({
      activeUsers: Number(item.active_users) || 0,
      country: item.country,
      paidUsers: Number(item.paid_users) || 0,
      point: item.geo_point,
      value: Number(item.recharge_usd_minor) || 0
    }))
    .sort((left, right) => right.value - left.value);
}

function findHoveredPoint(event, element, size, rotation, points) {
  if (!element || !size.width || !size.height || !points.length) {
    return null;
  }

  const rect = element.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;
  const cx = size.width / 2;
  const cy = size.height / 2;
  const radius = Math.max(1, Math.min(size.width * 0.48, size.height * 0.47));
  let nearest = null;

  for (const item of points) {
    const projected = project(item.point, rotation, cx, cy, radius);
    if (!projected) {
      continue;
    }
    const distance = Math.hypot(projected.x - x, projected.y - y);
    if (distance <= 18 && (!nearest || distance < nearest.distance)) {
      nearest = { distance, item, x: projected.x, y: projected.y };
    }
  }

  if (!nearest) {
    return null;
  }

  return {
    item: nearest.item,
    x: clamp(nearest.x + 14, 8, size.width - 136),
    y: clamp(nearest.y - 76, 8, size.height - 96)
  };
}


function extractRings(features = []) {
  const rings = [];
  for (const feature of features) {
    const geometry = feature.geometry;
    if (!geometry) {
      continue;
    }
    if (geometry.type === "Polygon") {
      rings.push(...geometry.coordinates);
    }
    if (geometry.type === "MultiPolygon") {
      for (const polygon of geometry.coordinates) {
        rings.push(...polygon);
      }
    }
  }
  return rings;
}

function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max);
}

function normalizeLongitude(value) {
  return ((((value + 180) % 360) + 360) % 360) - 180;
}
