import { useState, useRef, useEffect, useCallback } from "react";
import type {
  QuestArc,
  QuestNode,
  ClaimedRewardResponse,
} from "../../types/quest";
import { useQuestStore } from "../../stores/useQuestStore";
import { useAuthStore } from "../../stores/authStore";
import { api } from "../../utils/api"; // adjust path to your API client
import { QuestNodeModal } from "../../components/QuestNodeModal";
import { ChestOpenModal } from "../../components/ChestOpenModal";
import { InfoHeader } from "../../components/InfoHeader";

// ─── Map geometry (all in SVG user-units, viewBox width = 390) ───────────────
const VW = 390;
const ROW_GAP = 265;
const TOP_PAD = 80;

const COL_X = [
  Math.round(VW * 0.22), // 86
  Math.round(VW * 0.5), // 195
  Math.round(VW * 0.78), // 304
];

const ARC_COL_SEQS: Record<string, number[]> = {
  east_blue: [0, 1, 2, 0, 1, 2],
  alabasta: [2, 0, 2, 1, 0, 2],
  skypiea: [1, 2, 0, 2, 0, 1],
};
const COL_SEQ_DEFAULT = [0, 1, 2, 0, 1, 2];

const CARD_W = 130;
const CARD_H = 170; // base height (locked / completed / active)
const CARD_H_CLAIMABLE = 235; // extra room for progress section + claim button

const islandCX = (i: number, arcId: string) => {
  const seq = ARC_COL_SEQS[arcId] ?? COL_SEQ_DEFAULT;
  return COL_X[seq[i % seq.length]];
};
const islandCY = (i: number) => TOP_PAD + i * ROW_GAP;
const svgHeight = (n: number) =>
  TOP_PAD + (n - 1) * ROW_GAP + TOP_PAD + CARD_H_CLAIMABLE;

// ─── Island shapes ────────────────────────────────────────────────────────────
const SHAPES = [
  `<ellipse cx="0" cy="0" rx="57" ry="33"/>`,
  `<polygon points="0,-38 28,-14 48,10 40,33 22,38 -22,38 -40,33 -48,10 -28,-14"/>`,
  `<ellipse cx="0" cy="5" rx="62" ry="26"/>`,
  `<polygon points="0,-38 20,-14 50,-8 32,12 42,36 16,24 0,38 -16,24 -42,36 -32,12 -50,-8 -20,-14"/>`,
  `<path d="M-50,0 C-50,-34 -20,-38 0,-36 C22,-34 48,-18 50,4 C52,24 36,30 18,24 C6,20 4,10 10,4 C16,-4 26,-4 28,4 C30,12 22,18 12,16 C-4,10 -10,-8 0,-20 C12,-32 -30,-28 -50,0 Z"/>`,
  `<path d="M0,-38 C18,-38 44,-18 44,8 C44,28 26,38 0,38 C-26,38 -44,28 -44,8 C-44,-18 -18,-38 0,-38 Z"/>`,
];

// ─── Styles ───────────────────────────────────────────────────────────────────
const STYLES = `
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&family=Cinzel:wght@700;900&display=swap');
  @import url('https://fonts.googleapis.com/css2?family=Sorts+Mill+Goudy:ital@0;1&display=swap');

  * { box-sizing: border-box; }

  .qm-screen {
    height: 100vh; font-family: 'Nunito', sans-serif;
    position: relative; display: flex; flex-direction: column;
    overflow: hidden; background: #060e1f;
  }

  /* ══ HEADER ══ */
  .qm-header {
    position: relative; z-index: 30; flex-shrink: 0;
    background: rgba(4,10,24,0.94);
    backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
    border-bottom: 1px solid rgba(251,191,36,0.12);
    padding: 1.25rem 1.25rem 0;
  }

  /* On desktop, account for sidebar */
  @media (min-width: 768px) {
    .qm-header {
      padding-left: calc(17rem + 1.25rem);
    }
  }
  .qm-page-title {
    font-family: 'Sorts Mill Goudy', serif;
    font-size: 1.3rem; font-weight: 900; letter-spacing: 0.05em;
    color: #fbbf24;
    text-shadow: 0 0 24px rgba(251,191,36,0.5), 0 0 60px rgba(251,191,36,0.15);
    margin-bottom: 0.15rem;
  }
  .qm-page-sub {
    font-family: 'Nunito Sans', sans-serif; font-size: 0.7rem; font-weight: 600;
    color: rgba(255,255,255,0.35); margin-bottom: 0.85rem;
  }
  .qm-stats-strip {
    display: flex; gap: 0.4rem; overflow-x: auto;
    scrollbar-width: none; padding-bottom: 0.85rem;
  }
  .qm-stats-strip::-webkit-scrollbar { display:none; }
  .qm-stat-chip {
    display: flex; align-items: center; gap: 0.3rem;
    padding: 0.28rem 0.7rem; flex-shrink: 0;
    background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08);
    border-radius: 100px;
  }
  .qm-stat-val   { font-size:0.76rem; font-weight:900; color:#fbbf24; }
  .qm-stat-label { font-family:'Nunito Sans',sans-serif; font-size:0.62rem; font-weight:600; color:rgba(255,255,255,0.35); }
  .qm-arc-tabs {
    display: flex; gap:0; overflow-x:auto; scrollbar-width:none;
    border-top: 1px solid rgba(255,255,255,0.06);
  }
  .qm-arc-tabs::-webkit-scrollbar { display:none; }
  .qm-arc-tab {
    flex-shrink:0; display:flex; align-items:center; gap:0.4rem;
    padding: 0.6rem 1rem; border:none; background:transparent; cursor:pointer;
    font-family:'Nunito',sans-serif; font-weight:800; font-size:0.78rem;
    color: rgba(255,255,255,0.3); border-bottom: 3px solid transparent;
    transition: all 0.2s ease; white-space:nowrap;
  }
  .qm-arc-tab:hover { color:rgba(255,255,255,0.6); }
  .qm-arc-tab.active { color:var(--arc-accent); border-bottom-color:var(--arc-accent); }
  .qm-tab-dot {
    width:7px; height:7px; border-radius:50%;
    background:#ef4444; box-shadow:0 0 8px #ef4444;
    animation: qmDotBlink 1.4s ease-in-out infinite;
  }
  @keyframes qmDotBlink { 0%,100%{ opacity:1; } 50%{ opacity:0.4; } }

  /* ══ SEA ══ */
  .qm-sea-scroll {
    flex:1; overflow-y:auto; overflow-x:hidden;
    position:relative; scrollbar-width:none; -webkit-overflow-scrolling:touch;
  }
  .qm-sea-scroll::-webkit-scrollbar { display:none; }
  .qm-sea {
    position:relative; min-height:100%;
    padding: 1.25rem 1.25rem 8rem;
    background:
      radial-gradient(ellipse 80% 40% at 20% 15%, rgba(6,80,160,0.45) 0%, transparent 60%),
      radial-gradient(ellipse 60% 50% at 80% 60%, rgba(4,50,110,0.35) 0%, transparent 55%),
      radial-gradient(ellipse 70% 40% at 50% 90%, rgba(8,120,180,0.2) 0%, transparent 50%),
      linear-gradient(180deg, #071530 0%, #04101e 40%, #020a14 100%);
    overflow:hidden;
  }

  /* On desktop, account for sidebar (16rem + 1rem margins = 17rem) */
  @media (min-width: 768px) {
    .qm-sea {
      padding-left: calc(17rem + 1.25rem);
      margin-left: 0;
    }
  }

  .qm-sea-shimmer {
    position:absolute; inset:0; pointer-events:none; z-index:0;
    background:
      repeating-linear-gradient(105deg, transparent 0%, transparent 55%, rgba(56,189,248,0.018) 56%, transparent 57%),
      repeating-linear-gradient(75deg,  transparent 0%, transparent 70%, rgba(56,189,248,0.012) 71%, transparent 72%);
    background-size: 400% 400%, 300% 300%;
    animation: qmSeaMove 14s ease-in-out infinite alternate;
  }
  @keyframes qmSeaMove {
    0%   { background-position: 0% 0%, 100% 0%; }
    100% { background-position: 100% 100%, 0% 100%; }
  }
  .qm-bubble {
    position:absolute; border-radius:50%; pointer-events:none; z-index:1;
    background: rgba(255,255,255,0.045);
    animation: qmBobble var(--bdur) ease-in-out infinite;
    animation-delay: var(--bdelay);
  }
  @keyframes qmBobble {
    0%,100%{ transform:translateY(0) scale(1); opacity:0.5; }
    50%    { transform:translateY(-10px) scale(1.1); opacity:0.9; }
  }

  /* ── Arc banner ── */
  .qm-arc-banner {
    position:relative; z-index:5;
    border-radius:22px; padding: 1.1rem 1.25rem; margin-bottom: 1.5rem;
    border: 1px solid rgba(255,255,255,0.1);
    box-shadow: 0 12px 40px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.08);
    overflow:hidden;
  }
  .qm-arc-banner::before {
    content:''; position:absolute; inset:0;
    background: repeating-linear-gradient(45deg, transparent, transparent 15px, rgba(255,255,255,0.015) 15px, rgba(255,255,255,0.015) 16px);
  }
  .qm-arc-banner-bg-emoji {
    position:absolute; right:0.5rem; top:50%; transform:translateY(-50%);
    font-size:5rem; opacity:0.09; filter:blur(2px); pointer-events:none; z-index:0;
  }
  .qm-arc-banner-name {
    font-family:'Sorts Mill Goudy',serif; font-size:1.25rem; font-weight:900; color:white;
    letter-spacing:0.06em; text-shadow: 0 2px 16px rgba(0,0,0,0.6); position:relative; z-index:1;
  }
  .qm-arc-banner-sub {
    font-family:'Nunito Sans',sans-serif; font-size:0.7rem; font-weight:600;
    color:rgba(255,255,255,0.5); margin-top:0.2rem; position:relative; z-index:1;
  }
  .qm-arc-banner-prog {
    display:flex; align-items:center; gap:0.65rem; margin-top:0.8rem; position:relative; z-index:1;
  }
  .qm-arc-banner-track { flex:1; height:5px; border-radius:100px; background:rgba(255,255,255,0.12); overflow:hidden; }
  .qm-arc-banner-fill {
    height:100%; border-radius:100px; background:rgba(255,255,255,0.8);
    box-shadow:0 0 8px rgba(255,255,255,0.5); transition:width 0.8s cubic-bezier(0.34,1.56,0.64,1);
  }
  .qm-arc-banner-count {
    font-family:'Nunito',sans-serif; font-size:0.68rem; font-weight:900;
    color:rgba(255,255,255,0.65); white-space:nowrap;
  }

  /* ══ MAP SVG CANVAS ══ */
  .qm-map-svg { display:block; width:100%; overflow:visible; position:relative; z-index:5; }

  /* ── Info card (foreignObject inside SVG) ── */
  .qm-info-card {
    background: rgba(255,255,255,0.055); border:1px solid rgba(255,255,255,0.09);
    border-radius:16px; padding:0.7rem 0.85rem;
    backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
    transition: background 0.15s ease, border-color 0.15s ease;
    overflow:hidden;
  }
  .qm-info-card.is-claimable { border-color:rgba(251,191,36,0.45); background:rgba(251,191,36,0.07); }
  .qm-info-card.is-active    { border-color:rgba(255,255,255,0.14); }
  .qm-info-card.is-locked    { opacity:0.42; }
  .qm-info-row1 { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.4rem; gap:0.4rem; }
  .qm-info-title { font-family:'Sorts Mill Goudy',serif; font-size:0.78rem; font-weight:700; color:white; line-height:1.25; }
  .qm-info-loc   { font-size:0.52rem; font-weight:800; letter-spacing:0.14em; text-transform:uppercase; color:var(--arc-accent); margin-bottom:0.12rem; }
  .qm-xp-badge {
    display:flex; align-items:center; gap:0.18rem; padding:0.18rem 0.45rem;
    background:rgba(251,191,36,0.13); border:1px solid rgba(251,191,36,0.3);
    border-radius:100px; flex-shrink:0;
  }
  .qm-xp-badge-val { font-size:0.62rem; font-weight:900; color:#fbbf24; }
  .qm-prog-track { height:5px; background:rgba(255,255,255,0.08); border-radius:100px; overflow:hidden; margin-bottom:0.22rem; }
  .qm-prog-fill {
    height:100%; border-radius:100px;
    background:linear-gradient(90deg, var(--arc-accent), color-mix(in srgb,var(--arc-accent) 65%,white));
    box-shadow:0 0 8px color-mix(in srgb,var(--arc-accent) 55%,transparent);
    transition:width 0.7s cubic-bezier(0.34,1.56,0.64,1);
  }
  .qm-prog-label { font-family:'Nunito Sans',sans-serif; font-size:0.55rem; font-weight:700; color:rgba(255,255,255,0.38); }
  .qm-claim-btn {
    width:100%; margin-top:0.5rem; padding:0.48rem;
    background:linear-gradient(135deg,#fbbf24,#f59e0b); border:none; border-radius:10px; cursor:pointer;
    font-family:'Sorts Mill Goudy',serif; font-size:0.72rem; font-weight:700;
    color:#1a0e00; letter-spacing:0.04em;
    box-shadow:0 3px 0 #d97706, 0 5px 14px rgba(251,191,36,0.3); transition:all 0.12s ease;
  }
  .qm-claim-btn:hover  { transform:translateY(-1px); box-shadow:0 5px 0 #d97706; }
  .qm-claim-btn:active { transform:translateY(1px);  box-shadow:0 1px 0 #d97706; }

  /* ══ ARC COMPLETE ══ */
  .qm-arc-done {
    position:relative; z-index:5; margin-top:1.5rem; padding:1.25rem; text-align:center;
    background:linear-gradient(135deg,rgba(251,191,36,0.12),rgba(251,191,36,0.04));
    border:1px solid rgba(251,191,36,0.3); border-radius:20px;
    box-shadow:0 0 40px rgba(251,191,36,0.06);
  }
  .qm-arc-done-title {
    font-family:'Sorts Mill Goudy',serif; font-size:1rem; font-weight:900; color:#fbbf24;
    text-shadow:0 0 20px rgba(251,191,36,0.6); margin-bottom:0.2rem;
  }
  .qm-arc-done-sub { font-family:'Nunito Sans',sans-serif; font-size:0.7rem; font-weight:600; color:rgba(251,191,36,0.55); }

  /* ══ FAB ══ */
  .qm-fab {
    position:fixed; bottom:calc(1.25rem + 80px + env(safe-area-inset-bottom)); right:1.25rem; z-index:25;
    width:52px; height:52px; border-radius:50%;
    background:linear-gradient(135deg,#1a0e45,#3730a3); border:2px solid rgba(251,191,36,0.45);
    display:flex; align-items:center; justify-content:center; font-size:1.5rem; cursor:pointer;
    box-shadow:0 6px 24px rgba(0,0,0,0.55), 0 0 0 1px rgba(251,191,36,0.15);
    animation:qmFabFloat 4s ease-in-out infinite;
    transition:transform 0.2s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.2s;
  }
  .qm-fab:hover  { transform:scale(1.1) rotate(8deg); }
  .qm-fab:active { transform:scale(0.92); }
  @keyframes qmFabFloat { 0%,100%{ transform:translateY(0) rotate(-4deg); } 50%{ transform:translateY(-7px) rotate(4deg); } }

  /* ══ NODE ENTRANCE ══ */
  @keyframes qmIslandIn { from{ opacity:0; transform:scale(0.82) translateY(22px); } to{ opacity:1; transform:scale(1) translateY(0); } }
  .qm-island-in { animation: qmIslandIn 0.5s cubic-bezier(0.34,1.56,0.64,1) both; }
`;

// ─── Arc theme generator ──────────────────────────────────────────────────────
// Deterministic pseudo-random theme derived from arc.id string so the same arc
// always gets the same colours across renders/sessions — no server field needed.

export interface ArcTheme {
  accent: string; // bright highlight colour
  accentDark: string; // darker variant for shadows/gradients
  bgFrom: string; // banner gradient start
  bgTo: string; // banner gradient end
  emoji: string; // banner / tab icon
  terrain: { l: string; m: string; d: string; s: string }; // island fill colours
  decos: [string, string, string]; // SVG decoration emojis
}

/** Cheap seeded PRNG — Mulberry32. Returns a function that yields [0,1) floats. */
const mkRng = (seed: number) => {
  let s = seed >>> 0;
  return () => {
    s += 0x6d2b79f5;
    let t = Math.imul(s ^ (s >>> 15), 1 | s);
    t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
};

/** Turn an arc.id string into a stable 32-bit seed via djb2. */
const strToSeed = (str: string): number => {
  let h = 5381;
  for (let i = 0; i < str.length; i++)
    h = (Math.imul(h, 33) ^ str.charCodeAt(i)) >>> 0;
  return h;
};

/** Convert HSL values (all 0-1) to a CSS hex colour. */
const hslToHex = (h: number, s: number, l: number): string => {
  const a = s * Math.min(l, 1 - l);
  const f = (n: number) => {
    const k = (n + h * 12) % 12;
    const c = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
    return Math.round(255 * c)
      .toString(16)
      .padStart(2, "0");
  };
  return `#${f(0)}${f(8)}${f(4)}`;
};

const ARC_EMOJIS = [
  "⚓",
  "🏴‍☠️",
  "🗺️",
  "⚔️",
  "🌊",
  "🔱",
  "☠️",
  "🧭",
  "💎",
  "🏝️",
  "⛵",
  "🌋",
];
const DECO_SETS: [string, string, string][] = [
  ["🌴", "🌿", "🌴"],
  ["🌵", "🏺", "🌵"],
  ["☁️", "✨", "☁️"],
  ["🪨", "🌾", "🪨"],
  ["🍄", "🌸", "🍄"],
  ["🔥", "💀", "🔥"],
  ["❄️", "🌨️", "❄️"],
  ["🌺", "🦜", "🌺"],
];

/**
 * Generates a fully deterministic colour theme from arc.id.
 * The same id always produces the same theme — no randomness at render time.
 */
export const generateArcTheme = (arc: QuestArc): ArcTheme => {
  const rng = mkRng(strToSeed(arc.id));

  // Pick a hue, then build a coherent palette around it
  const hue = rng(); // 0-1  (= 0°-360°)
  const hueShift = 0.05 + rng() * 0.1; // slight shift for dark variant
  const satHigh = 0.55 + rng() * 0.35; // 0.55-0.90
  const satLow = satHigh * (0.5 + rng() * 0.3); // darker bg is less saturated

  const accent = hslToHex(hue, satHigh, 0.72); // bright, light
  const accentDark = hslToHex(hue, satHigh, 0.3); // same hue, deep
  const bgFrom = hslToHex(hue, satLow, 0.14); // very dark, rich
  const bgTo = hslToHex(hue + hueShift, satLow, 0.22); // slightly lighter

  // Terrain: light highlight, mid tone, dark shadow, shadow rgba
  const tL = hslToHex(hue, satHigh, 0.68);
  const tM = hslToHex(hue, satHigh, 0.42);
  const tD = hslToHex(hue, satHigh * 0.85, 0.22);
  // Shadow as rgba — parse the dark hex back to rgb values
  const sd = parseInt(tD.slice(1, 3), 16);
  const sg = parseInt(tD.slice(3, 5), 16);
  const sb = parseInt(tD.slice(5, 7), 16);
  const terrain = { l: tL, m: tM, d: tD, s: `rgba(${sd},${sg},${sb},0.6)` };

  // Pick emoji + deco set deterministically
  const emojiIdx = Math.floor(rng() * ARC_EMOJIS.length);
  const decoIdx = Math.floor(rng() * DECO_SETS.length);
  const emoji = ARC_EMOJIS[emojiIdx];
  const decos = DECO_SETS[decoIdx];

  return { accent, accentDark, bgFrom, bgTo, emoji, terrain, decos };
};

/** Cache so we never regenerate a theme for the same arc within a session. */
const themeCache = new Map<string, ArcTheme>();
const getArcTheme = (arc: QuestArc): ArcTheme => {
  if (!themeCache.has(arc.id)) themeCache.set(arc.id, generateArcTheme(arc));
  return themeCache.get(arc.id)!;
};

// ─── Requirement helpers ───────────────────────────────────────────────────────
// req_type → display icon (unchanged from original REQ_ICON map)
const REQ_ICON: Record<string, string> = {
  questions: "❓",
  accuracy: "🎯",
  streak: "🔥",
  sessions: "📚",
  topics: "🗺️",
  xp: "⚡",
  leaderboard: "🏆",
};

// req_type → human-readable label (replaces the old requirement.label field)
const REQ_LABEL: Record<string, string> = {
  questions: "questions answered",
  accuracy: "% accuracy",
  streak: "day streak",
  sessions: "sessions",
  topics: "topics covered",
  xp: "XP earned",
  leaderboard: "leaderboard rank",
};

// req_type → emoji shown on the island body (replaces old node.emoji field)
const REQ_EMOJI: Record<string, string> = {
  questions: "❓",
  accuracy: "🎯",
  streak: "🔥",
  sessions: "📚",
  topics: "🗺️",
  xp: "⚡",
  leaderboard: "🏆",
};

// ─── Sea foam bubbles ─────────────────────────────────────────────────────────
const FOAM = Array.from({ length: 22 }, (_, i) => ({
  id: i,
  w: 10 + ((i * 17 + 7) % 24),
  top: `${3 + ((i * 13) % 88)}%`,
  left: `${(i * 19 + 5) % 96}%`,
  dur: `${4 + ((i * 7) % 7)}s`,
  delay: `${(i * 3) % 5}s`,
}));

// ─── Helpers ──────────────────────────────────────────────────────────────────
const completedCount = (arc: QuestArc) =>
  arc.nodes.filter((n) => n.status === "completed").length;

// Truncate island label to keep SVG tidy
const truncate = (str: string | undefined, max = 14): string => {
  if (!str) return "";
  return str.length > max ? str.slice(0, max - 1) + "…" : str;
};

// ─── SVG Island node ──────────────────────────────────────────────────────────
const IslandNode = ({
  node,
  accent,
  terrain,
  decos,
  userXp,
  index,
  cx,
  cy,
  onTap,
  onClaim,
}: {
  node: QuestNode;
  accent: string;
  terrain: ArcTheme["terrain"];
  decos: ArcTheme["decos"];
  userXp: number;
  index: number;
  cx: number;
  cy: number;
  onTap: (n: QuestNode) => void;
  onClaim: (n: QuestNode) => void;
}) => {
  // node.status is typed as string from the API; normalise to expected literals
  const status = node.status as "LOCKED" | "ACTIVE" | "CLAIMABLE" | "COMPLETED";
  const isCompleted = status === "COMPLETED";
  const isClaimable = status === "CLAIMABLE";
  const isActive = status === "ACTIVE";
  const isLocked = status === "LOCKED";

  // Progress percentage — uses new current_value / req_target fields
  const pct = Math.min(
    100,
    Math.round((node.current_value / node.req_target) * 100),
  );

  const hiC = isLocked ? "#4b5563" : isCompleted ? "#6ee7b7" : terrain.l;
  const midC = isLocked ? "#374151" : isCompleted ? "#10b981" : terrain.m;
  const loC = isLocked ? "#1f2937" : isCompleted ? "#065f46" : terrain.d;
  const shdC = isLocked ? "rgba(0,0,0,0.5)" : terrain.s;

  const gradId = `grad-${node.node_id}`;
  const clipId = `clip-${node.node_id}`;
  const shadowId = `shadow-${node.node_id}`;
  const glowId = `glow-${node.node_id}`;
  const shapeIdx = index % SHAPES.length;

  const LAND_H = 38;
  const cardTop = cy + LAND_H + 18;
  // Claimable cards render progress section + button — need more vertical room.
  // All other statuses fit in the base height.
  const cardH = isClaimable ? CARD_H_CLAIMABLE : CARD_H;

  const statusCard = isClaimable
    ? "is-claimable"
    : isActive
      ? "is-active"
      : isLocked
        ? "is-locked"
        : "is-completed";

  // Derive island emoji from req_type (replaces old node.emoji field)
  const nodeEmoji = REQ_EMOJI[node.req_type] ?? "🏝️";

  return (
    <g
      style={{ cursor: isLocked ? "default" : "pointer" }}
      onClick={() => !isLocked && onTap(node)}
    >
      <defs>
        <radialGradient id={gradId} cx="38%" cy="28%" r="65%">
          <stop offset="0%" stopColor={hiC} />
          <stop offset="55%" stopColor={midC} />
          <stop offset="100%" stopColor={loC} />
        </radialGradient>
        <filter id={shadowId} x="-40%" y="-40%" width="180%" height="180%">
          <feDropShadow
            dx="0"
            dy="9"
            stdDeviation="7"
            floodColor={shdC}
            floodOpacity="0.8"
          />
        </filter>
        <filter id={glowId} x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="7" result="blur" />
          <feFlood
            floodColor={isClaimable ? "#fbbf24" : accent}
            floodOpacity="0.55"
            result="col"
          />
          <feComposite in="col" in2="blur" operator="in" result="glow" />
          <feMerge>
            <feMergeNode in="glow" />
            <feMergeNode in="SourceGraphic" />
          </feMerge>
        </filter>
        <clipPath id={clipId}>
          <g dangerouslySetInnerHTML={{ __html: SHAPES[shapeIdx] }} />
        </clipPath>
      </defs>

      {/* Water shimmer halo */}
      <ellipse
        cx={cx}
        cy={cy + LAND_H - 4}
        rx={isLocked ? 40 : 56}
        ry={12}
        fill="rgba(56,189,248,0.22)"
        style={{ filter: "blur(5px)" }}
      >
        <animate
          attributeName="rx"
          values={`${isLocked ? 40 : 56};${isLocked ? 46 : 62};${isLocked ? 40 : 56}`}
          dur="3s"
          repeatCount="indefinite"
        />
        <animate
          attributeName="opacity"
          values="0.6;1;0.6"
          dur="3s"
          repeatCount="indefinite"
        />
      </ellipse>

      {/* Land shadow blob */}
      <g
        transform={`translate(${cx},${cy + 12})`}
        style={{ filter: "blur(10px)" }}
        opacity="0.5"
      >
        <g
          dangerouslySetInnerHTML={{ __html: SHAPES[shapeIdx] }}
          style={{ fill: shdC }}
        />
      </g>

      {/* Active / claimable glow ring */}
      {(isActive || isClaimable) && (
        <g transform={`translate(${cx},${cy}) scale(1.22)`}>
          <g
            dangerouslySetInnerHTML={{
              __html: SHAPES[shapeIdx]
                .replace(
                  ">",
                  ` fill="none" stroke="${isClaimable ? "#fbbf24" : accent}" stroke-width="1.8" stroke-dasharray="6 4" opacity="0.6">`,
                )
                .replace("<", "<"),
            }}
          />
        </g>
      )}

      {/* Land shape */}
      <g
        transform={`translate(${cx},${cy})`}
        filter={`url(#${isActive || isClaimable ? glowId : shadowId})`}
        opacity={isLocked ? 0.45 : 1}
      >
        <g
          dangerouslySetInnerHTML={{
            __html: SHAPES[shapeIdx].replace(">", ` fill="url(#${gradId})">`),
          }}
        />
      </g>

      {/* Decorations */}
      {!isLocked && (
        <>
          <text
            x={cx - 22}
            y={cy - LAND_H - 6}
            fontSize="13"
            textAnchor="middle"
            style={{ filter: "drop-shadow(0 2px 3px rgba(0,0,0,0.5))" }}
          >
            {decos[0]}
          </text>
          <text
            x={cx + 22}
            y={cy - LAND_H - 2}
            fontSize="15"
            textAnchor="middle"
            style={{ filter: "drop-shadow(0 2px 3px rgba(0,0,0,0.5))" }}
          >
            {decos[1]}
          </text>
        </>
      )}

      {/* Pirate flag on active */}
      {isActive && (
        <g transform={`translate(${cx - 8},${cy - LAND_H - 26})`}>
          <line
            x1="0"
            y1="0"
            x2="0"
            y2="-20"
            stroke="#6b4226"
            strokeWidth="2"
            strokeLinecap="round"
          />
          <path d="M0,-20 L16,-14 L0,-8Z" fill="#ef4444" />
        </g>
      )}

      {/* Bouncing chest on claimable */}
      {isClaimable && (
        <text
          x={cx}
          y={cy - LAND_H - 8}
          fontSize="18"
          textAnchor="middle"
          style={{ filter: "drop-shadow(0 4px 8px rgba(251,191,36,0.7))" }}
        >
          📦
          <animate
            attributeName="y"
            values={`${cy - LAND_H - 8};${cy - LAND_H - 18};${cy - LAND_H - 8}`}
            dur="1.4s"
            repeatCount="indefinite"
          />
        </text>
      )}

      {/* Lock icon */}
      {isLocked && (
        <text
          x={cx}
          y={cy + 6}
          fontSize="18"
          textAnchor="middle"
          dominantBaseline="middle"
          opacity="0.4"
        >
          🔒
        </text>
      )}

      {/* Node emoji — derived from req_type */}
      {!isLocked && (
        <text
          x={cx}
          y={cy + 6}
          fontSize="18"
          textAnchor="middle"
          dominantBaseline="middle"
          style={{ filter: "drop-shadow(0 2px 5px rgba(0,0,0,0.5))" }}
        >
          {nodeEmoji}
        </text>
      )}

      {/* Completed check */}
      {isCompleted && (
        <g transform={`translate(${cx + 40},${cy - LAND_H + 4})`}>
          <circle
            r="11"
            fill="#22c55e"
            stroke="rgba(255,255,255,0.9)"
            strokeWidth="2.2"
          />
          <text x="0" y="5" fontSize="12" textAnchor="middle" fill="white">
            ✓
          </text>
        </g>
      )}

      {/* Island name label — uses node.name (truncated) */}
      <text
        x={cx}
        y={cy + LAND_H + 10}
        fontSize="8.5"
        fontFamily="'Sorts Mill Goudy',serif"
        fontWeight="700"
        fill="rgba(255,255,255,0.45)"
        textAnchor="middle"
        letterSpacing="0.1em"
      >
        {truncate(node.name).toUpperCase()}
      </text>

      {/* Info card via foreignObject */}
      <foreignObject
        x={cx - CARD_W / 2}
        y={cardTop}
        width={CARD_W}
        height={cardH}
        style={{ overflow: "visible" }}
        onClick={(e) => e.stopPropagation()}
      >
        <div
          className={`qm-info-card ${statusCard}`}
          style={{ ["--arc-accent" as string]: accent }}
          onClick={() => !isLocked && onTap(node)}
        >
          <div className="qm-info-row1">
            {/* node.name replaces old node.title */}
            <p className="qm-info-title">{node.name ?? "—"}</p>
            {/* Live XP from auth store */}
            <div className="qm-xp-badge">
              <span style={{ fontSize: "0.58rem" }}>⚡</span>
              <span className="qm-xp-badge-val">{userXp} XP</span>
            </div>
          </div>

          {(isActive || isClaimable) && (
            <>
              <div className="qm-prog-track">
                <div className="qm-prog-fill" style={{ width: `${pct}%` }} />
              </div>
              {/* req_type + current_value / req_target + derived label */}
              <p className="qm-prog-label">
                {REQ_ICON[node.req_type]}&nbsp;
                {node.current_value}/{node.req_target}&nbsp;
                {REQ_LABEL[node.req_type] ?? node.req_type}
              </p>
            </>
          )}

          {isLocked && (
            <p className="qm-prog-label">
              🔒 {node.req_target} {REQ_LABEL[node.req_type] ?? node.req_type}{" "}
              to unlock
            </p>
          )}

          {isCompleted && (
            <p className="qm-prog-label" style={{ color: "#4ade80" }}>
              ✅ Conquered!
            </p>
          )}

          {isClaimable && (
            <button
              className="qm-claim-btn"
              onClick={(e) => {
                e.stopPropagation();
                onClaim(node);
              }}
            >
              ⚓ Open Chest
            </button>
          )}
        </div>
      </foreignObject>
    </g>
  );
};

// ─── SVG Path between two island centres ─────────────────────────────────────
const RoutePath = ({
  x1,
  y1,
  x2,
  y2,
  done,
  accent,
  showShip,
}: {
  x1: number;
  y1: number;
  x2: number;
  y2: number;
  done: boolean;
  accent: string;
  showShip: boolean;
}) => {
  const mx = (x1 + x2) / 2;
  const my = (y1 + y2) / 2;
  const dx = x2 - x1;
  const dy = y2 - y1;
  const len = Math.sqrt(dx * dx + dy * dy) || 1;
  const perp = 55;
  const side = x1 < x2 ? 1 : -1;
  const cpx = mx - (dy / len) * perp * side;
  const cpy = my + (dx / len) * perp * side;

  const path = `M ${x1} ${y1} Q ${cpx} ${cpy} ${x2} ${y2}`;
  const shipX = 0.25 * x1 + 0.5 * cpx + 0.25 * x2;
  const shipY = 0.25 * y1 + 0.5 * cpy + 0.25 * y2;

  return (
    <g>
      <path
        d={path}
        fill="none"
        stroke="rgba(255,255,255,0.06)"
        strokeWidth="18"
        strokeLinecap="round"
      />
      <path
        d={path}
        fill="none"
        stroke={done ? accent : "rgba(255,255,255,0.2)"}
        strokeWidth={done ? "2.5" : "1.8"}
        strokeDasharray={done ? "10 6" : "6 8"}
        strokeLinecap="round"
        style={{ filter: done ? `drop-shadow(0 0 5px ${accent})` : "none" }}
      />
      {[0.25, 0.5, 0.75].map((t, ti) => {
        const ex = (1 - t) * (1 - t) * x1 + 2 * (1 - t) * t * cpx + t * t * x2;
        const ey = (1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * cpy + t * t * y2;
        return (
          <ellipse
            key={ti}
            cx={ex}
            cy={ey}
            rx="8"
            ry="2.5"
            fill="rgba(255,255,255,0.04)"
          />
        );
      })}
      {showShip && (
        <text
          x={shipX}
          y={shipY}
          fontSize="18"
          textAnchor="middle"
          dominantBaseline="middle"
          style={{ filter: "drop-shadow(0 3px 6px rgba(0,0,0,0.5))" }}
        >
          ⛵
          <animateTransform
            attributeName="transform"
            type="translate"
            values="0,0;0,-5;0,0"
            dur="2.5s"
            additive="sum"
            repeatCount="indefinite"
          />
          <animateTransform
            attributeName="transform"
            type="rotate"
            values={`-6,${shipX},${shipY};6,${shipX},${shipY};-6,${shipX},${shipY}`}
            dur="2.5s"
            additive="sum"
            repeatCount="indefinite"
          />
        </text>
      )}
    </g>
  );
};

// ─── Main ─────────────────────────────────────────────────────────────────────
export const QuestMap = () => {
  // ── Store ──
  const arcs = useQuestStore((s) => s.arcs);
  const activeArcId = useQuestStore((s) => s.activeArcId);
  const setActiveArc = useQuestStore((s) => s.setActiveArc);
  const claimNode = useQuestStore((s) => s.claimNode);
  const syncFromAPI = useQuestStore((s) => s.syncFromAPI);
  const user = useAuthStore((s) => s.user);
  const token = useAuthStore((s) => s.token);
  const userXp = user?.total_xp ?? 0;

  // ── Fetch state ──
  const [loading, setLoading] = useState(true);
  const [fetchError, setFetchError] = useState<string | null>(null);

  // ── Claim state ──
  const [claimingNode, setClaimingNode] = useState<QuestNode | null>(null);
  const [claimResult, setClaimResult] = useState<ClaimedRewardResponse | null>(
    null,
  );
  const [claimLoading, setClaimLoading] = useState(false);
  const [claimError, setClaimError] = useState<string | null>(null);

  // ── UI state ──
  const [selectedNode, setSelectedNode] = useState<QuestNode | null>(null);
  const scrollRef = useRef<HTMLDivElement>(null);

  // ── Fetch journey on mount ────────────────────────────────────────────────
  useEffect(() => {
    if (!token) return;
    let cancelled = false;

    const fetchJourney = async () => {
      try {
        setLoading(true);
        setFetchError(null);
        const data = await api.fetchUserJourney(token);
        if (!cancelled) syncFromAPI(data);
      } catch (err) {
        if (!cancelled)
          setFetchError(
            err instanceof Error ? err.message : "Failed to load quests",
          );
      } finally {
        if (!cancelled) setLoading(false);
      }
    };

    fetchJourney();
    return () => {
      cancelled = true;
    };
  }, [token, syncFromAPI]);

  // ── Derived ───────────────────────────────────────────────────────────────
  const arc = arcs.find((a) => a.id === activeArcId) ?? arcs[0];
  const theme = arc ? getArcTheme(arc) : null;
  const done = arc ? completedCount(arc) : 0;
  const pct = arc ? Math.round((done / arc.nodes.length) * 100) : 0;

  // ── Claim flow ────────────────────────────────────────────────────────────
  // Step 1: user taps "Open Chest" — open the modal immediately (animation
  // starts) and fire the API call in parallel so the result is usually ready
  // by the time the chest animation finishes (~2.5s).
  const handleClaim = useCallback(
    async (node: QuestNode) => {
      if (!token) return;
      setClaimingNode(node);
      setClaimResult(null);
      setClaimError(null);
      setClaimLoading(true);

      try {
        const result = await api.claimReward(token, node.node_id);
        setClaimResult(result);
      } catch (err) {
        setClaimError(err instanceof Error ? err.message : "Claim failed");
      } finally {
        setClaimLoading(false);
      }
    },
    [token],
  );

  // Step 2: user taps "Set Sail" in ChestOpenModal — commit to store & close.
  const handleChestClose = useCallback(() => {
    if (!claimingNode) return;
    const titlesUnlocked = Array.isArray(claimResult?.title_unlocked)
      ? claimResult!.title_unlocked
      : claimResult?.title_unlocked
        ? [claimResult.title_unlocked]
        : [];
    claimNode(
      arc.id,
      claimingNode.node_id,
      claimResult?.xp_awarded ?? 0,
      titlesUnlocked.map((t) => t.name),
    );
    setClaimingNode(null);
    setClaimResult(null);
    setClaimError(null);
  }, [claimingNode, claimResult, arc, claimNode]);

  // ── Loading screen ────────────────────────────────────────────────────────
  if (loading) {
    return (
      <div
        className="qm-screen"
        style={{ alignItems: "center", justifyContent: "center" }}
      >
        <style>{STYLES}</style>
        <div
          style={{
            textAlign: "center",
            color: "rgba(255,255,255,0.5)",
            fontFamily: "'Nunito',sans-serif",
          }}
        >
          <div
            style={{
              fontSize: "2.5rem",
              marginBottom: "1rem",
              animation: "qmFabFloat 2s ease-in-out infinite",
            }}
          >
            ⛵
          </div>
          <p
            style={{
              fontSize: "0.85rem",
              fontWeight: 800,
              letterSpacing: "0.1em",
            }}
          >
            CHARTING YOUR COURSE...
          </p>
        </div>
      </div>
    );
  }

  // ── Error screen ──────────────────────────────────────────────────────────
  if (fetchError || !arc) {
    return (
      <div
        className="qm-screen"
        style={{
          alignItems: "center",
          justifyContent: "center",
          padding: "2rem",
        }}
      >
        <style>{STYLES}</style>
        <div
          style={{
            textAlign: "center",
            color: "rgba(255,255,255,0.5)",
            fontFamily: "'Nunito',sans-serif",
          }}
        >
          <div style={{ fontSize: "2.5rem", marginBottom: "1rem" }}>🌊</div>
          <p
            style={{
              fontSize: "0.85rem",
              fontWeight: 800,
              color: "#ef4444",
              marginBottom: "0.5rem",
            }}
          >
            {fetchError ?? "No quest data found"}
          </p>
          <button
            onClick={() => window.location.reload()}
            style={{
              marginTop: "1rem",
              padding: "0.5rem 1.25rem",
              borderRadius: "100px",
              border: "1px solid rgba(255,255,255,0.15)",
              background: "transparent",
              color: "rgba(255,255,255,0.5)",
              cursor: "pointer",
              fontFamily: "'Nunito',sans-serif",
              fontWeight: 800,
              fontSize: "0.75rem",
            }}
          >
            Try again
          </button>
        </div>
      </div>
    );
  }

  const sorted = [...arc.nodes].sort(
    (a, b) => a.sequence_order - b.sequence_order,
  );
  const centres = sorted.map((_, i) => ({
    x: islandCX(i, arc.id),
    y: islandCY(i),
  }));
  const totalSvgH = svgHeight(sorted.length);

  return (
    <div className="qm-screen">
      <style>{STYLES}</style>

      {/* Header */}
      <div className="qm-header">
        <InfoHeader mode="QUEST_EXTENDED" />
        <div className="qm-arc-tabs">
          {[...arcs]
            .sort((a, b) => a.sequence_order - b.sequence_order)
            .map((a) => {
              const t = getArcTheme(a);
              return (
                <button
                  key={a.id}
                  className={`qm-arc-tab${activeArcId === a.id ? " active" : ""}`}
                  style={{ "--arc-accent": t.accent } as React.CSSProperties}
                  onClick={() => {
                    setActiveArc(a.id);
                    scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" });
                  }}
                >
                  {t.emoji} {a.name}
                  {a.nodes.some((n) => n.status === "CLAIMABLE") && (
                    <span className="qm-tab-dot" />
                  )}
                </button>
              );
            })}
        </div>
      </div>

      {/* Sea */}
      <div className="qm-sea-scroll" ref={scrollRef}>
        <div className="qm-sea">
          <div className="qm-sea-shimmer" />
          {FOAM.map((b) => (
            <div
              key={b.id}
              className="qm-bubble"
              style={
                {
                  width: b.w,
                  height: b.w,
                  top: b.top,
                  left: b.left,
                  "--bdur": b.dur,
                  "--bdelay": b.delay,
                } as React.CSSProperties
              }
            />
          ))}

          {/* Arc banner */}
          <div
            className="qm-arc-banner"
            style={{
              background: `linear-gradient(135deg,${theme!.bgFrom}dd,${theme!.bgTo}ee)`,
            }}
          >
            <div className="qm-arc-banner-bg-emoji">{theme!.emoji}</div>
            <p className="qm-arc-banner-name">{arc.name}</p>
            <p className="qm-arc-banner-sub">{arc.description}</p>
            <div className="qm-arc-banner-prog">
              <div className="qm-arc-banner-track">
                <div
                  className="qm-arc-banner-fill"
                  style={{ width: `${pct}%` }}
                />
              </div>
              <span className="qm-arc-banner-count">
                {done}/{arc.nodes.length} islands
              </span>
            </div>
          </div>

          {/* SVG map canvas */}
          <svg
            className="qm-map-svg"
            viewBox={`0 0 ${VW} ${totalSvgH}`}
            height={totalSvgH}
            preserveAspectRatio="xMidYMin meet"
          >
            {sorted.map((node, i) => {
              if (i >= sorted.length - 1) return null;
              const c1 = centres[i];
              const c2 = centres[i + 1];
              const ship =
                node.status === "completed" &&
                sorted[i + 1]?.status === "active";
              return (
                <RoutePath
                  key={`route-${i}`}
                  x1={c1.x}
                  y1={c1.y}
                  x2={c2.x}
                  y2={c2.y}
                  done={node.status === "completed"}
                  accent={theme!.accent}
                  showShip={ship}
                />
              );
            })}

            {sorted.map((node, i) => (
              <IslandNode
                key={node.node_id}
                node={node}
                accent={theme!.accent}
                terrain={theme!.terrain}
                decos={theme!.decos}
                userXp={userXp}
                index={i}
                cx={centres[i].x}
                cy={centres[i].y}
                onTap={setSelectedNode}
                onClaim={handleClaim}
              />
            ))}

            {done === sorted.length && (
              <g transform={`translate(${VW / 2},${totalSvgH - 60})`}>
                <circle
                  r="42"
                  fill="rgba(251,191,36,0.12)"
                  stroke="rgba(251,191,36,0.5)"
                  strokeWidth="1.5"
                  strokeDasharray="8 4"
                />
                <circle
                  r="34"
                  fill="rgba(255,248,200,0.9)"
                  stroke="rgba(180,120,20,0.4)"
                  strokeWidth="1.5"
                />
                <text
                  x="0"
                  y="-8"
                  fontFamily="'Cinzel',serif"
                  fontSize="8"
                  fontWeight="900"
                  fill="#92400e"
                  textAnchor="middle"
                  letterSpacing="0.12em"
                >
                  ARC
                </text>
                <text
                  x="0"
                  y="5"
                  fontFamily="'Cinzel',serif"
                  fontSize="8"
                  fontWeight="900"
                  fill="#92400e"
                  textAnchor="middle"
                  letterSpacing="0.12em"
                >
                  COMPLETE
                </text>
                <text x="0" y="19" fontSize="13" textAnchor="middle">
                  ⚓
                </text>
              </g>
            )}
          </svg>
        </div>
      </div>

      <div
        className="qm-fab"
        onClick={() =>
          scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" })
        }
      >
        🏴‍☠️
      </div>

      {selectedNode && (
        <QuestNodeModal
          node={selectedNode}
          arc={arc}
          arcAccent={theme!.accent}
          arcDark={theme!.accentDark}
          arcId={arc.id}
          nodeIndex={selectedNode.sequence_order}
          onClose={() => setSelectedNode(null)}
          onClaim={() => {
            setSelectedNode(null);
            handleClaim(selectedNode);
          }}
        />
      )}

      {claimingNode && (
        <ChestOpenModal
          node={claimingNode}
          claimResult={claimResult}
          onClose={handleChestClose}
        />
      )}

      {/* Claim error toast — shown if API call failed but modal is already open */}
      {claimError && (
        <div
          style={{
            position: "fixed",
            bottom: "calc(2rem + env(safe-area-inset-bottom))",
            left: "50%",
            transform: "translateX(-50%)",
            zIndex: 100,
            background: "#7f1d1d",
            border: "1px solid #ef4444",
            borderRadius: "12px",
            padding: "0.6rem 1.1rem",
            color: "white",
            fontFamily: "'Nunito',sans-serif",
            fontSize: "0.78rem",
            fontWeight: 800,
            boxShadow: "0 4px 20px rgba(0,0,0,0.4)",
            whiteSpace: "nowrap",
          }}
        >
          ⚠️ {claimError} — your progress is saved
        </div>
      )}
    </div>
  );
};
