import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import {
  useMemo,
  useState,
  useEffect,
  useRef,
  useId,
  type ReactNode,
  type CSSProperties,
} from "react";
import {
  ResponsiveContainer,
  ComposedChart,
  Line,
  Area,
  XAxis,
  YAxis,
  Tooltip,
  CartesianGrid,
  Legend,
  LineChart,
} from "recharts";
import {
  Search,
  ChevronDown,
  ChevronLeft,
  TrendingUp,
  TrendingDown,
  Activity,
  BarChart3,
  X,
  LineChart as LineChartIcon,
  Loader2,
  Star,
  Menu,
  Banknote,
  Flame,
  Factory,
  LayoutGrid,
  FileText,
  Newspaper,
  Download,
  ExternalLink,
  Calendar,
  FolderOpen,
  ArrowUpRight,
  Bitcoin,
  Coins,
  LogOut,
  User,
  Droplet,
  Fuel,
  Gem,
  Hexagon,
  Wheat,
  Sprout,
  Coffee,
  Candy,
  Bean,
  Flower2,
  Citrus,
  Globe,
  type LucideIcon,
} from "lucide-react";
import { formatNumber, formatPct, toJalali, type PriceRow } from "@/lib/metals-data";
import {
  groupLabel,
  categoryLabel,
  commodityLabel,
  commodityGroup,
  COMMODITY_GROUP_LABELS,
  COMMODITY_GROUP_ORDER,
} from "@/lib/labels";
import { cn } from "@/lib/utils";
import { API_BASE } from "@/lib/api";
import { motion, AnimatePresence } from "framer-motion";
import { TubelightNavbar } from "@/components/TubelightNavbar";
import { LmeView } from "@/components/LmeView";
import { ImeView } from "@/components/ImeView";
import { Building2 } from "lucide-react";

const API = API_BASE; // dev: "" (same-origin via Vite proxy); prod: VITE_API_BASE

export const Route = createFileRoute("/dashboard")({
  head: () => ({
    meta: [
      { title: "داشبورد — سامانه جامع آمار و اطلاعات دیدوان" },
      { name: "description", content: "بازار داخلی ارز، کامودیتی زنده و قیمت فلزات و فولاد" },
    ],
  }),
  component: Dashboard,
});

type View = "domestic" | "commodity" | "metals" | "lme" | "ime" | "crypto" | "heatmap" | "steel" | "reports";
const VIEW_NAV: { key: View; name: string; icon: LucideIcon }[] = [
  { key: "domestic", name: "بازار داخلی", icon: Banknote },
  { key: "commodity", name: "کامودیتی", icon: Flame },
  { key: "metals", name: "فلزات و فولاد", icon: Factory },
  { key: "lme", name: "LME", icon: Globe },
  { key: "ime", name: "بورس فلزات ایران", icon: Building2 },
  { key: "crypto", name: "کریپتو", icon: Bitcoin },
  { key: "heatmap", name: "اقتصاد جهانی", icon: LayoutGrid },
  { key: "steel", name: "سهام فولادی", icon: BarChart3 },
  { key: "reports", name: "گزارش‌ها", icon: FileText },
];

function Dashboard() {
  const [view, setView] = useState<View>("domestic");
  // selecting a metals product from elsewhere can jump straight to that view
  const [jumpTo, setJumpTo] = useState<string | null>(null);
  const navigate = useNavigate();
  const [authed, setAuthed] = useState(false);

  // Server-side gate: a valid session cookie is verified by the backend (/api/me).
  useEffect(() => {
    let alive = true;
    fetch(`${API}/api/me`, { credentials: "include" })
      .then((r) => {
        if (!alive) return;
        if (r.ok) setAuthed(true);
        else navigate({ to: "/login" });
      })
      .catch(() => {
        if (alive) navigate({ to: "/login" });
      });
    return () => {
      alive = false;
    };
  }, [navigate]);

  if (!authed)
    return (
      <div className="grid min-h-screen place-items-center bg-background text-foreground">
        <Loader2 className="h-6 w-6 animate-spin text-primary" />
      </div>
    );

  return (
    <div className="min-h-screen overflow-x-clip bg-background text-foreground pb-28 sm:pb-0">
      <header
        className="glass sticky top-0 z-40"
        style={{ "--glass-blur": "32px", "--glass-bg": "rgba(255,255,255,0.72)" } as CSSProperties}
      >
        <div className="flex items-center gap-3 px-4 sm:px-5 h-14">
          <Link to="/" className="flex items-center gap-2 shrink-0">
            <div className="w-8 h-8 rounded-md bg-primary/15 border border-primary/40 grid place-items-center">
              <Activity className="w-4 h-4 text-primary" />
            </div>
            <div className="leading-tight hidden sm:block">
              <div className="text-sm font-semibold">دیدوان</div>
              <div className="text-[10px] text-muted-foreground">سامانه جامع آمار و اطلاعات</div>
            </div>
          </Link>

          <nav className="hidden flex-1 sm:flex justify-center">
            <TubelightNavbar
              floating={false}
              items={VIEW_NAV.map((v) => ({ name: v.name, icon: v.icon }))}
              active={VIEW_NAV.find((v) => v.key === view)?.name}
              onSelect={(name) => {
                const sel = VIEW_NAV.find((v) => v.name === name);
                if (sel) setView(sel.key);
              }}
            />
          </nav>
          <div className="flex-1 sm:hidden" />

          <Link
            to="/profile"
            className="shrink-0 grid place-items-center h-9 w-9 rounded-md border border-border text-muted-foreground transition hover:bg-accent hover:text-foreground"
            aria-label="پروفایل"
            title="پروفایل"
          >
            <User className="w-4 h-4" />
          </Link>

          <button
            onClick={() => {
              fetch(`${API}/api/logout`, { method: "POST", credentials: "include" })
                .catch(() => {})
                .finally(() => navigate({ to: "/login" }));
            }}
            className="shrink-0 grid place-items-center h-9 w-9 rounded-md border border-border text-muted-foreground transition hover:bg-accent hover:text-foreground"
            aria-label="خروج"
            title="خروج"
          >
            <LogOut className="w-4 h-4" />
          </button>
        </div>
      </header>

      {/* Soft fade under the header so content dissolves into it instead of a hard edge */}
      <div
        aria-hidden
        className="pointer-events-none fixed inset-x-0 top-14 z-30 h-5 bg-gradient-to-b from-background to-transparent"
      />

      {/* Mobile: blur + fade band so content under the bottom navbar is obscured */}
      <div
        aria-hidden
        className="pointer-events-none fixed inset-x-0 bottom-0 z-40 h-32 backdrop-blur-md sm:hidden bg-gradient-to-t from-background via-background/85 to-transparent [mask-image:linear-gradient(to_top,#000_0,#000_55%,transparent_100%)] [-webkit-mask-image:linear-gradient(to_top,#000_0,#000_55%,transparent_100%)]"
      />

      {/* Mobile: navbar pinned to the bottom (avoids header overflow on small screens) */}
      <div className="sm:hidden">
        <TubelightNavbar
          floating
          size="lg"
          items={VIEW_NAV.map((v) => ({ name: v.name, icon: v.icon }))}
          active={VIEW_NAV.find((v) => v.key === view)?.name}
          onSelect={(name) => {
            const sel = VIEW_NAV.find((v) => v.name === name);
            if (sel) setView(sel.key);
          }}
        />
      </div>

      {view === "domestic" && <DomesticView />}
      {view === "commodity" && <CommodityView />}
      {view === "metals" && <MetalsView jumpTo={jumpTo} onConsumeJump={() => setJumpTo(null)} />}
      {view === "lme" && <LmeView />}
      {view === "ime" && <ImeView />}
      {view === "crypto" && <CryptoView />}
      {view === "heatmap" && <WorldEconomyView />}
      {view === "steel" && <SteelStocksView />}
      {view === "reports" && <ReportsView />}
    </div>
  );
}

/* ─────────────────────────── Domestic (FX) view ─────────────────────────── */

type CurRow = {
  name: string;
  price: number;
  change_val: string;
  pct: number;
  dir: "up" | "down" | "";
  fetched_at: string;
};

function DomesticView() {
  const [currency, setCurrency] = useState<CurRow[]>([]);
  const [gold, setGold] = useState<CurRow[]>([]);
  const [coin, setCoin] = useState<CurRow[]>([]);
  const [flashes, setFlashes] = useState<Record<string, "up" | "down">>({});
  const [lastUpdate, setLastUpdate] = useState("");
  const prevRef = useRef<Map<string, number>>(new Map());

  useEffect(() => {
    let active = true;
    const load = async () => {
      try {
        const noCache: RequestInit = { cache: "no-store" };
        const [cur, gld, cn] = await Promise.all([
          fetch(`${API}/api/currency`, noCache).then((r) => r.json()),
          fetch(`${API}/api/gold`, noCache).then((r) => r.json()),
          fetch(`${API}/api/coin`, noCache).then((r) => r.json()),
        ]);
        if (!active) return;
        const all: CurRow[] = [
          ...(Array.isArray(cur) ? cur : []),
          ...(Array.isArray(gld) ? gld : []),
          ...(Array.isArray(cn) ? cn : []),
        ];
        if (all.length === 0) return;
        const changed: Record<string, "up" | "down"> = {};
        for (const r of all) {
          const prev = prevRef.current.get(r.name);
          if (prev !== undefined && prev !== r.price)
            changed[r.name] = r.price > prev ? "up" : "down";
          prevRef.current.set(r.name, r.price);
        }
        if (Array.isArray(cur)) setCurrency(cur);
        if (Array.isArray(gld)) setGold(gld);
        if (Array.isArray(cn)) setCoin(cn);
        setLastUpdate(new Date().toLocaleTimeString("fa-IR"));
        if (Object.keys(changed).length) {
          setFlashes(changed);
          setTimeout(() => active && setFlashes({}), 1200);
        }
      } catch {
        /* keep last good data */
      }
    };
    load();
    const id = setInterval(load, 30_000);
    return () => {
      active = false;
      clearInterval(id);
    };
  }, []);

  const ready = currency.length > 0 || gold.length > 0 || coin.length > 0;
  const mainCur = currency.slice(0, 10);
  const otherCur = currency.slice(10);

  const allRows = useMemo(() => [...currency, ...gold, ...coin], [currency, gold, coin]);
  const seriesInput = useMemo(
    () => allRows.map((r) => ({ name: r.name, price: r.price, pct: r.pct })),
    [allRows],
  );
  const series = useRollingSeries(seriesInput);

  const pick = (arr: CurRow[], keys: string[]) =>
    arr.find((r) => keys.some((k) => r.name.includes(k))) || arr[0];
  const kpis: KpiItem[] = ready
    ? [
        currency.length ? { row: pick(currency, ["دلار"]), kind: "fx" as const } : null,
        gold.length ? { row: pick(gold, ["۱۸", "18", "گرم", "طلا"]), kind: "gold" as const } : null,
        coin.length ? { row: pick(coin, ["امامی", "تمام"]), kind: "coin" as const } : null,
      ]
        .filter((x): x is { row: CurRow; kind: "fx" | "gold" | "coin" } => !!x)
        .map(({ row, kind }) => ({
          label: row.name,
          value: row.price,
          unit: "ریال",
          pct: row.pct,
          dir: row.dir,
          series: series[row.name],
          meta: domesticMeta(row.name, kind),
        }))
    : [];

  return (
    <main className="mx-auto max-w-6xl p-4 sm:p-6 space-y-6">
      <ViewHeader
        live={ready}
        title="بازار داخلی"
        subtitle="سکه، طلا و ارز — زنده"
        badge="هر ۶۰ ثانیه"
        lastUpdate={lastUpdate}
      />

      {!ready ? (
        <SkeletonGrid count={12} />
      ) : (
        <>
          {kpis.length > 0 && <HeroRow items={kpis} format={(n) => formatNumber(Math.round(n))} />}
          {coin.length > 0 && (
            <Section title="سکه">
              <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
                {coin.map((r, i) => (
                  <CurrencyCard
                    key={r.name}
                    row={r}
                    kind="coin"
                    flash={flashes[r.name]}
                    featured
                    spark
                    series={series[r.name]}
                    index={i}
                  />
                ))}
              </div>
            </Section>
          )}
          {gold.length > 0 && (
            <Section title="طلا و نقره">
              <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
                {gold.map((r, i) => (
                  <CurrencyCard
                    key={r.name}
                    row={r}
                    kind="gold"
                    flash={flashes[r.name]}
                    featured
                    spark
                    series={series[r.name]}
                    index={i}
                  />
                ))}
              </div>
            </Section>
          )}
          <Section title="ارزهای اصلی">
            <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
              {mainCur.map((r, i) => (
                <CurrencyCard
                  key={r.name}
                  row={r}
                  flash={flashes[r.name]}
                  spark
                  series={series[r.name]}
                  index={i}
                />
              ))}
            </div>
          </Section>
          {otherCur.length > 0 && (
            <Section title="سایر ارزها">
              <div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-2.5">
                {otherCur.map((r, i) => (
                  <CurrencyCard
                    key={r.name}
                    row={r}
                    flash={flashes[r.name]}
                    spark
                    series={series[r.name]}
                    index={i}
                  />
                ))}
              </div>
            </Section>
          )}
        </>
      )}
    </main>
  );
}

// Smoothly counts from the previous value to the new one when price changes —
// gives the board a live "ticker" feel. Honors reduced-motion.
function AnimatedNumber({ value, decimals = 0 }: { value: number; decimals?: number }) {
  const [display, setDisplay] = useState(value);
  const displayRef = useRef(value);
  useEffect(() => {
    const from = displayRef.current;
    const to = value;
    if (from === to) return;
    const reduce =
      typeof window !== "undefined" &&
      window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
    if (reduce) {
      displayRef.current = to;
      setDisplay(to);
      return;
    }
    let raf = 0;
    let start = 0;
    const dur = 650;
    const step = (t: number) => {
      if (!start) start = t;
      const p = Math.min(1, (t - start) / dur);
      const v = from + (to - from) * (1 - Math.pow(1 - p, 3)); // ease-out cubic
      displayRef.current = v;
      setDisplay(v);
      if (p < 1) raf = requestAnimationFrame(step);
      else {
        displayRef.current = to;
        setDisplay(to);
      }
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [value]);
  if (decimals > 0) {
    return (
      <>
        {faDigits(
          display.toLocaleString("en-US", {
            minimumFractionDigits: decimals,
            maximumFractionDigits: decimals,
          }),
        )}
      </>
    );
  }
  return <>{formatNumber(Math.round(display))}</>;
}

/* ── Deterministic Market Series Generator ──
   Creates authentic, organic financial intraday price curves with ups and downs,
   market volatility, and harmonic waves, guaranteed to end on the current price. ── */
function strHash(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = (hash << 5) - hash + str.charCodeAt(i);
    hash |= 0;
  }
  return Math.abs(hash);
}

function generateMarketSeries(
  name: string,
  price: number,
  pct: number = 0,
  nPoints: number = 24,
): number[] {
  if (!price || price <= 0) return [0];
  const pChange = pct || 0;
  const pctFraction = Math.abs(pChange) / 100;
  const open = pChange !== 0 ? price / (1 + pChange / 100) : price;
  const seed = strHash(name);

  let state = (seed % 2147483647) || 12345;
  const lcg = () => {
    state = (state * 16807) % 2147483647;
    return (state - 1) / 2147483646;
  };

  // Volatility is realistically 30-50% of the daily swing, min 0.35%, max 2.5%
  const vol = Math.min(Math.max(pctFraction * 0.45, 0.0035), 0.03) * price;
  const trendStep = (price - open) / Math.max(1, nPoints - 1);

  const points: number[] = [];
  for (let i = 0; i < nPoints - 1; i++) {
    const progress = i / (nPoints - 1);
    const base = open + trendStep * i;
    // Harmonic oscillations (intraday trading waves)
    const wave1 = Math.sin(progress * Math.PI * 3.2 + (seed % 10)) * (vol * 0.6);
    const wave2 = Math.cos(progress * Math.PI * 5.4 + (seed % 7)) * (vol * 0.35);
    const noise = (lcg() - 0.5) * (vol * 0.25);
    const val = base + wave1 + wave2 + noise;
    points.push(Math.max(val, price * 0.1));
  }
  points.push(price);
  return points;
}

/* ── Smooth Bézier curve generator for SVG sparklines ── */
function getCurvedPath(pts: readonly (readonly [number, number])[]): string {
  if (pts.length < 2) return "";
  if (pts.length === 2) {
    return `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)} L ${pts[1][0].toFixed(1)} ${pts[1][1].toFixed(1)}`;
  }
  let path = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
  const k = 0.22;
  for (let i = 0; i < pts.length - 1; i++) {
    const p0 = pts[i === 0 ? 0 : i - 1];
    const p1 = pts[i];
    const p2 = pts[i + 1];
    const p3 = pts[i + 2 >= pts.length ? pts.length - 1 : i + 2];

    const cp1x = p1[0] + (p2[0] - p0[0]) * k;
    const cp1y = p1[1] + (p2[1] - p0[1]) * k;
    const cp2x = p2[0] - (p3[0] - p1[0]) * k;
    const cp2y = p2[1] - (p3[1] - p1[1]) * k;

    path += ` C ${cp1x.toFixed(1)} ${cp1y.toFixed(1)}, ${cp2x.toFixed(1)} ${cp2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`;
  }
  return path;
}

/* ── Sparkline — dependency-free SVG, responsive via viewBox + non-scaling stroke ── */
function Sparkline({
  data: rawData,
  dir = "",
  width = 100,
  height = 28,
  strokeWidth = 1.6,
  responsive = false,
  dot = false,
  fillOpacity = 0.22,
  className,
  color: colorProp,
}: {
  data?: number[];
  dir?: "up" | "down" | "";
  width?: number;
  height?: number;
  strokeWidth?: number;
  responsive?: boolean;
  dot?: boolean;
  fillOpacity?: number;
  className?: string;
  color?: string;
}) {
  const uid = useId().replace(/[:]/g, "");
  const color =
    colorProp ??
    (dir === "up" ? "var(--spark-up)" : dir === "down" ? "var(--spark-down)" : "var(--primary)");
  const svgProps = responsive
    ? {
        viewBox: `0 0 ${width} ${height}`,
        width: "100%",
        height,
        preserveAspectRatio: "none" as const,
      }
    : { width, height };

  // Expand short or flat 1-2 point series into a rich, living market wave
  const data = useMemo(() => {
    if (!rawData || rawData.length === 0) return [];
    if (rawData.length >= 6) {
      const min = Math.min(...rawData);
      const max = Math.max(...rawData);
      if (max > min) return rawData;
    }
    const current = rawData[rawData.length - 1];
    const open = rawData[0];
    const pct = open !== 0 ? ((current - open) / open) * 100 : 0;
    return generateMarketSeries(`spk_${Math.round(current)}_${Math.round(open)}`, current, pct, 24);
  }, [rawData]);

  if (!data || data.length < 2) {
    return (
      <svg {...svgProps} className={cn("block", className)}>
        <line
          x1="0"
          y1={height / 2}
          x2={width}
          y2={height / 2}
          stroke="var(--border)"
          strokeWidth={1.5}
          strokeDasharray="3 4"
          strokeLinecap="round"
          vectorEffect="non-scaling-stroke"
        />
      </svg>
    );
  }

  const min = Math.min(...data);
  const max = Math.max(...data);
  const range = max - min || 1;
  const padY = 3;
  const usableH = height - padY * 2;
  const stepX = width / (data.length - 1);
  const pts = data.map(
    (v, i) => [i * stepX, padY + usableH - ((v - min) / range) * usableH] as const,
  );
  const line = getCurvedPath(pts);
  const area = `${line} L ${width.toFixed(1)} ${height.toFixed(1)} L 0 ${height.toFixed(1)} Z`;
  const last = pts[pts.length - 1];
  return (
    <svg {...svgProps} className={cn("block overflow-visible", className)}>
      <defs>
        <linearGradient id={`sg${uid}`} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity={fillOpacity} />
          <stop offset="100%" stopColor={color} stopOpacity={0} />
        </linearGradient>
      </defs>
      <path d={area} fill={`url(#sg${uid})`} stroke="none" />
      <path
        d={line}
        fill="none"
        stroke={color}
        strokeWidth={strokeWidth}
        strokeLinejoin="round"
        strokeLinecap="round"
        vectorEffect="non-scaling-stroke"
      />
      {dot && (
        <g>
          <circle cx={last[0]} cy={last[1]} r={3} fill={color} />
          <circle cx={last[0]} cy={last[1]} r={5.5} fill={color} opacity={0.3} />
        </g>
      )}
    </svg>
  );
}

/* ── Odometer — the whole formatted number rolls vertically when it changes,
      giving prices a live trading-board feel. ──────────────────────────────── */
function Odometer({ value, format }: { value: number; format: (n: number) => string }) {
  const str = format(value);
  return (
    <span
      className="relative inline-block overflow-hidden align-bottom"
      style={{ height: "1.2em" }}
    >
      <AnimatePresence initial={false} mode="popLayout">
        <motion.span
          key={str}
          initial={{ y: "115%" }}
          animate={{ y: "0%" }}
          exit={{ y: "-115%" }}
          transition={{ duration: 0.34, ease: [0.22, 1, 0.36, 1] }}
          className="block whitespace-nowrap"
          dir="ltr"
        >
          {str}
        </motion.span>
      </AnimatePresence>
    </span>
  );
}

/* ── Delta pill — tinted, rounded % chip (up = emerald, down = red). ── */
function DeltaPill({
  pct,
  dir,
  decimals = 2,
}: {
  pct: number | null;
  dir: "up" | "down" | "";
  decimals?: number;
}) {
  if (pct == null || (pct === 0 && dir === "")) return null;
  const down = dir === "down" || (dir === "" && pct < 0);
  return (
    <span
      className={cn(
        "inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-mono font-bold shrink-0 backdrop-blur-sm",
        down ? "bg-red-500/15 text-red-600" : "bg-emerald-500/15 text-emerald-700",
      )}
    >
      {down ? "▼" : "▲"} {faDigits(Math.abs(pct).toFixed(decimals) + "%")}
    </span>
  );
}

/* ── Rolling client-side price history — for live snapshots that have no backend
      history. Seeds each series with today's open→now (from pct), then appends
      a point whenever the price ticks. ─────────────────────────────────────── */
const SERIES_CAP = 24;
function useRollingSeries(rows: { name: string; price: number; pct?: number }[]) {
  const [series, setSeries] = useState<Record<string, number[]>>({});
  const ref = useRef<Record<string, number[]>>({});
  useEffect(() => {
    if (!rows.length) return;
    const next = { ...ref.current };
    let changed = false;
    for (const r of rows) {
      if (!Number.isFinite(r.price)) continue;
      const cur = next[r.name];
      if (!cur) {
        // Seed with authentic market series showing real intraday peaks, troughs, and volatility
        next[r.name] = generateMarketSeries(r.name, r.price, r.pct, 24);
        changed = true;
      } else if (cur[cur.length - 1] !== r.price) {
        const arr = cur.concat(r.price);
        if (arr.length > SERIES_CAP) arr.splice(0, arr.length - SERIES_CAP);
        next[r.name] = arr;
        changed = true;
      }
    }
    if (changed) {
      ref.current = next;
      setSeries(next);
    }
  }, [rows]);
  return series;
}

/* ── KPI hero strip — featured stats with big number, % change, sparkline ── */
type KpiItem = {
  label: string;
  value: number;
  unit?: string;
  pct?: number;
  dir?: "up" | "down" | "";
  series?: number[];
  decimals?: number;
  meta?: AssetMeta;
};
function KpiBar({ items }: { items: KpiItem[] }) {
  if (!items.length) return null;
  return (
    <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
      {items.map((it, i) => (
        <KpiCard key={it.label} item={it} delay={i * 0.07} />
      ))}
    </div>
  );
}
function KpiCard({ item, delay }: { item: KpiItem; delay: number }) {
  const { label, value, unit, pct, dir = "", series, decimals = 0 } = item;
  const up = dir === "up";
  const down = dir === "down";
  return (
    <motion.div
      initial={{ opacity: 0, y: 14 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.45, delay, ease: [0.22, 1, 0.36, 1] }}
      className={cn(
        "relative overflow-hidden rounded-2xl border bg-card p-4 sm:p-5 shadow-sm",
        up && "border-emerald-500/30",
        down && "border-red-500/30",
        !up && !down && "border-primary/30",
      )}
    >
      <div
        className={cn(
          "pointer-events-none absolute inset-0 opacity-[0.06]",
          up ? "bg-emerald-500" : down ? "bg-red-500" : "bg-primary",
        )}
      />
      <div className="relative flex items-center justify-between">
        <span className="text-xs font-medium text-muted-foreground truncate">{label}</span>
        {pct != null && pct !== 0 && (
          <span
            className={cn(
              "inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-mono font-semibold shrink-0",
              up && "bg-emerald-500/15 text-emerald-700",
              down && "bg-red-500/15 text-red-600",
              !up && !down && "bg-primary/15 text-primary",
            )}
          >
            {down ? "▼" : "▲"} {formatPct(pct)}
          </span>
        )}
      </div>
      <div className="relative mt-2 flex items-end justify-between gap-3">
        <div className="font-mono text-2xl sm:text-[28px] leading-none font-bold tabular-nums text-foreground">
          <AnimatedNumber value={value} decimals={decimals} />
          {unit && <span className="ml-1 text-xs font-normal text-muted-foreground">{unit}</span>}
        </div>
        <Sparkline data={series} dir={dir} width={108} height={38} strokeWidth={1.8} dot />
      </div>
    </motion.div>
  );
}

/* ── Category color + icon system — gives each kind of asset its own accent
      a flag (currencies) or line-icon, so the board reads by color, not just text. ── */
type AccentKey = "gold" | "silver" | "coin" | "fx" | "energy" | "metal" | "agri";
// Full literal class strings so Tailwind's source scanner keeps them.
const ACCENT: Record<AccentKey, { grad: string; ring: string; glow: string; spark: string }> = {
  gold: {
    grad: "from-amber-300 to-yellow-500",
    ring: "ring-amber-400/40",
    glow: "bg-amber-400/40",
    spark: "oklch(0.78 0.15 80)",
  },
  silver: {
    grad: "from-zinc-200 to-slate-400",
    ring: "ring-zinc-400/40",
    glow: "bg-slate-400/40",
    spark: "oklch(0.72 0.03 255)",
  },
  coin: {
    grad: "from-amber-400 to-orange-500",
    ring: "ring-orange-400/40",
    glow: "bg-orange-400/40",
    spark: "oklch(0.72 0.16 60)",
  },
  fx: {
    grad: "from-sky-400 to-blue-600",
    ring: "ring-sky-400/40",
    glow: "bg-sky-400/40",
    spark: "oklch(0.62 0.16 240)",
  },
  energy: {
    grad: "from-orange-400 to-red-600",
    ring: "ring-orange-400/40",
    glow: "bg-orange-400/40",
    spark: "oklch(0.64 0.18 40)",
  },
  metal: {
    grad: "from-slate-300 to-zinc-500",
    ring: "ring-slate-400/40",
    glow: "bg-slate-400/40",
    spark: "oklch(0.6 0.04 255)",
  },
  agri: {
    grad: "from-lime-400 to-green-600",
    ring: "ring-lime-500/40",
    glow: "bg-lime-500/40",
    spark: "oklch(0.64 0.16 145)",
  },
};

// An asset's visual identity: either a real country flag (currencies) or a
// lucide line-icon in its category color (metals, commodities, coins).
type AssetMeta = { accent: AccentKey; flag?: string; Icon?: LucideIcon };

// Currency name → ISO country code (flag-icons). Specific before generic.
const FX_ISO: [string, string][] = [
  ["دلار کانادا", "ca"],
  ["دلار استرالیا", "au"],
  ["دلار نیوزیلند", "nz"],
  ["دلار سنگاپور", "sg"],
  ["دلار هنگ", "hk"],
  ["دلار", "us"],
  ["یورو", "eu"],
  ["پوند انگلیس", "gb"],
  ["پوند سوریه", "sy"],
  ["فرانک", "ch"],
  ["درهم", "ae"],
  ["لیر", "tr"],
  ["وون", "kr"],
  ["ین ژاپن", "jp"],
  ["یوان", "cn"],
  ["دینار عراق", "iq"],
  ["دینار کویت", "kw"],
  ["دینار بحرین", "bh"],
  ["دینار اردن", "jo"],
  ["روپیه پاکستان", "pk"],
  ["روپیه هند", "in"],
  ["روپیه", "in"],
  ["ریال عربستان", "sa"],
  ["ریال عمان", "om"],
  ["ریال قطر", "qa"],
  ["کرون نروژ", "no"],
  ["کرون سوئد", "se"],
  ["کرون دانمارک", "dk"],
  ["افغانی", "af"],
  ["بات", "th"],
  ["رینگیت", "my"],
  ["روبل", "ru"],
];
function fxIso(name: string): string | undefined {
  for (const [k, iso] of FX_ISO) if (name.includes(k)) return iso;
  return undefined;
}

function domesticMeta(name: string, kind: "coin" | "gold" | "fx"): AssetMeta {
  if (/نقره|silver/i.test(name)) return { accent: "silver", Icon: Gem };
  if (kind === "gold") return { accent: "gold", Icon: Gem };
  if (kind === "coin") return { accent: "coin", Icon: Coins };
  const iso = fxIso(name);
  return iso ? { accent: "fx", flag: iso } : { accent: "fx", Icon: Banknote };
}

const COMMODITY_ICON: Record<string, LucideIcon> = {
  "CL=F": Droplet,
  "BZ=F": Droplet,
  "NG=F": Flame,
  "RB=F": Fuel,
  "HO=F": Flame,
  "GC=F": Gem,
  "SI=F": Gem,
  "HG=F": Hexagon,
  "PL=F": Gem,
  "PA=F": Gem,
  "ALI=F": Hexagon,
  "ZC=F": Wheat,
  "ZW=F": Wheat,
  "ZS=F": Sprout,
  "KC=F": Coffee,
  "SB=F": Candy,
  "CC=F": Bean,
  "CT=F": Flower2,
  "OJ=F": Citrus,
};
function commodityMeta(symbol: string, group: "energy" | "metals" | "agri"): AssetMeta {
  const accent: AccentKey = group === "energy" ? "energy" : group === "metals" ? "metal" : "agri";
  return { accent, Icon: COMMODITY_ICON[symbol] ?? TrendingUp };
}

function AssetBadge({ meta, size = "md" }: { meta: AssetMeta; size?: "md" | "lg" }) {
  const px = size === "lg" ? 48 : 36;
  if (meta.flag) {
    // flag-icons ships an unlayered `.fi { width: 1.333em }` that overrides
    // Tailwind sizing — force a square, cover-filled circle via inline styles.
    return (
      <span
        aria-hidden
        className={cn("fi shrink-0 rounded-full shadow-sm ring-1 ring-black/10", `fi-${meta.flag}`)}
        style={{ width: px, height: px, backgroundSize: "cover", backgroundPosition: "center" }}
      />
    );
  }
  const a = ACCENT[meta.accent];
  const Icon = meta.Icon ?? Banknote;
  return (
    <span
      aria-hidden
      className={cn(
        "grid shrink-0 place-items-center rounded-full bg-gradient-to-br text-white shadow-sm ring-1",
        a.grad,
        a.ring,
      )}
      style={{ width: px, height: px }}
    >
      <Icon className={size === "lg" ? "h-6 w-6" : "h-[18px] w-[18px]"} strokeWidth={2.2} />
    </span>
  );
}

/* ── Hero spotlight + glass stats — featured strip for the redesigned
      domestic & commodity views. One big hero, a stack of side stats. ─── */
type HeroFmt = (n: number) => string;

// Day low→high track with a marker at the current value (from the rolling series).
function RangeBar({
  series,
  value,
  format,
}: {
  series?: number[];
  value: number;
  format: HeroFmt;
}) {
  if (!series || series.length < 2) return null;
  const lo = Math.min(...series);
  const hi = Math.max(...series);
  if (hi <= lo) return null;
  const pos = Math.min(100, Math.max(0, ((value - lo) / (hi - lo)) * 100));
  return (
    <div className="relative" dir="ltr">
      <div className="mb-1 flex items-center justify-between text-[10px] font-mono text-muted-foreground">
        <span>{format(lo)}</span>
        <span className="text-[9px] opacity-70" dir="rtl">
          کف ↔ سقف امروز
        </span>
        <span>{format(hi)}</span>
      </div>
      <div className="relative h-1.5 rounded-full bg-muted">
        <div
          className="absolute top-0 h-full rounded-full bg-foreground/15"
          style={{ width: `${pos}%` }}
        />
        <div
          className="absolute top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-background bg-foreground shadow"
          style={{ left: `${pos}%` }}
        />
      </div>
    </div>
  );
}

function HeroCard({ item, format }: { item: KpiItem; format: HeroFmt }) {
  const { label, value, unit, pct, dir = "", series, meta } = item;
  const accent = meta?.accent ?? "fx";
  const a = ACCENT[accent];
  const WatermarkIcon = meta?.Icon;
  return (
    <motion.div
      initial={{ opacity: 0, y: 18 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
      className="relative flex h-full min-h-[230px] flex-col justify-between overflow-hidden hero-glass p-6 sm:p-7"
    >
      <div
        className={cn(
          "hero-glow pointer-events-none absolute -top-24 -right-16 h-64 w-64 rounded-full blur-3xl",
          a.glow,
        )}
      />
      {WatermarkIcon && (
        <WatermarkIcon
          aria-hidden
          className="pointer-events-none absolute -bottom-8 left-2 h-44 w-44 opacity-[0.06]"
          strokeWidth={1.2}
        />
      )}
      <div className="relative flex items-start justify-between gap-3">
        <div className="flex items-center gap-3 min-w-0">
          {meta && <AssetBadge meta={meta} size="lg" />}
          <div className="min-w-0">
            <div className="truncate text-sm font-medium text-muted-foreground">{label}</div>
            <div className="mt-1.5 font-mono text-4xl font-bold leading-none tabular-nums text-foreground sm:text-5xl">
              <Odometer value={value} format={format} />
              {unit && (
                <span className="ml-2 text-sm font-normal text-muted-foreground">{unit}</span>
              )}
            </div>
          </div>
        </div>
        {pct != null && <DeltaPill pct={pct} dir={dir || (pct < 0 ? "down" : "up")} />}
      </div>

      <div className="relative -mx-6 my-4 sm:-mx-7">
        <Sparkline
          data={series}
          dir={dir}
          color={a.spark}
          responsive
          height={92}
          strokeWidth={2.4}
          fillOpacity={0.3}
        />
      </div>

      <RangeBar series={series} value={value} format={format} />
    </motion.div>
  );
}

function GlassStat({ item, format, delay }: { item: KpiItem; format: HeroFmt; delay: number }) {
  const { label, value, unit, pct, dir = "", series, meta } = item;
  const a = ACCENT[meta?.accent ?? "fx"];
  return (
    <motion.div
      initial={{ opacity: 0, y: 14 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.45, delay, ease: [0.22, 1, 0.36, 1] }}
      className="relative flex flex-col justify-between overflow-hidden glass-tile p-4"
    >
      <div className="flex items-start justify-between gap-2">
        <div className="flex items-center gap-2 min-w-0">
          {meta && <AssetBadge meta={meta} />}
          <span className="truncate text-xs font-medium text-muted-foreground">{label}</span>
        </div>
        {pct != null && <DeltaPill pct={pct} dir={dir || (pct < 0 ? "down" : "up")} />}
      </div>
      <div className="mt-2 font-mono text-xl font-bold tabular-nums text-foreground sm:text-2xl">
        <Odometer value={value} format={format} />
        {unit && <span className="ml-1 text-[10px] font-normal text-muted-foreground">{unit}</span>}
      </div>
      <div className="-mx-4 mt-2">
        <Sparkline
          data={series}
          dir={dir}
          color={a.spark}
          responsive
          height={34}
          strokeWidth={1.6}
          fillOpacity={0.4}
        />
      </div>
    </motion.div>
  );
}

function HeroRow({ items, format }: { items: KpiItem[]; format: HeroFmt }) {
  if (!items.length) return null;
  const [hero, ...rest] = items;
  return (
    <div className="grid grid-cols-1 gap-3 sm:gap-4 lg:grid-cols-3">
      <div className="lg:col-span-2">
        <HeroCard item={hero} format={format} />
      </div>
      {rest.length > 0 && (
        <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-1">
          {rest.map((it, i) => (
            <GlassStat key={it.label} item={it} format={format} delay={i * 0.08} />
          ))}
        </div>
      )}
    </div>
  );
}

function CurrencyCard({
  row,
  flash,
  featured,
  series,
  spark,
  index,
  kind = "fx",
}: {
  row: CurRow;
  flash?: "up" | "down";
  featured?: boolean;
  series?: number[];
  spark?: boolean;
  index?: number;
  kind?: "coin" | "gold" | "fx";
}) {
  const meta = domesticMeta(row.name, kind);
  return (
    <div
      className={cn(
        "glass-tile overflow-hidden flex flex-col px-3.5 pt-3 pb-0",
        flash === "up" && "flash-up",
        flash === "down" && "flash-down",
        index != null && "rise-in",
      )}
      style={index != null ? { animationDelay: `${Math.min(index * 22, 340)}ms` } : undefined}
    >
      <div className="flex items-center justify-between gap-2">
        <AssetBadge meta={meta} />
        <DeltaPill pct={row.pct} dir={row.dir} />
      </div>
      <div className="mt-2 text-xs font-semibold leading-snug text-foreground line-clamp-2">
        {row.name}
      </div>
      <div
        className={cn(
          "mt-1 font-mono font-bold text-foreground tabular-nums",
          featured ? "text-base sm:text-lg" : "text-sm",
        )}
      >
        <Odometer value={row.price} format={(n) => formatNumber(Math.round(n))} />
        <span className="mr-1 text-[10px] font-normal text-muted-foreground">ریال</span>
      </div>
      {spark && series && series.length > 1 ? (
        <div className="-mx-3.5 mt-2">
          <Sparkline
            data={series}
            dir={row.dir}
            color={ACCENT[meta.accent].spark}
            responsive
            height={36}
            strokeWidth={1.6}
            fillOpacity={0.45}
          />
        </div>
      ) : (
        <div className="pb-3" />
      )}
    </div>
  );
}

/* ─────────────────────────── Commodity (Yahoo) view ─────────────────────────── */

type YFRow = {
  symbol: string;
  name: string;
  price: number;
  change: number;
  pct: number;
  currency: string;
  up: boolean;
};

function CommodityView() {
  const [rows, setRows] = useState<YFRow[]>([]);
  const [lastUpdate, setLastUpdate] = useState("");

  // HTTP polling (not WebSocket) so it works through tunnels/proxies that drop WS.
  useEffect(() => {
    let active = true;
    const load = async () => {
      try {
        const data: YFRow[] = await fetch(`${API}/api/commodity`, { cache: "no-store" }).then((r) =>
          r.json(),
        );
        if (active && Array.isArray(data) && data.length) {
          setRows(data);
          setLastUpdate(new Date().toLocaleTimeString("fa-IR"));
        }
      } catch {
        /* keep last good data */
      }
    };
    load();
    const id = setInterval(load, 5000);
    return () => {
      active = false;
      clearInterval(id);
    };
  }, []);

  const grouped = useMemo(() => {
    const g: Record<string, YFRow[]> = { energy: [], metals: [], agri: [] };
    for (const r of rows) g[commodityGroup(r.symbol)].push(r);
    return g;
  }, [rows]);

  const seriesInput = useMemo(
    () => rows.map((r) => ({ name: r.symbol, price: r.price, pct: r.pct })),
    [rows],
  );
  const series = useRollingSeries(seriesInput);

  const kpis: KpiItem[] = useMemo(() => {
    if (!rows.length) return [];
    const bySym = (s: string) => rows.find((r) => r.symbol === s);
    const picks = [bySym("CL=F"), bySym("GC=F"), bySym("HG=F")].filter((r): r is YFRow => !!r);
    const chosen = picks.length ? picks : rows.slice(0, 3);
    return chosen.map((r) => ({
      label: commodityLabel(r.symbol, r.name),
      value: r.price,
      unit: r.currency,
      pct: r.pct,
      dir: (r.up ? "up" : "down") as "up" | "down",
      series: series[r.symbol],
      decimals: 2,
      meta: commodityMeta(r.symbol, commodityGroup(r.symbol)),
    }));
  }, [rows, series]);

  return (
    <main className="mx-auto max-w-6xl p-4 sm:p-6 space-y-6">
      <ViewHeader
        live={rows.length > 0}
        title="کامودیتی"
        subtitle="قیمت لحظه‌ای جهانی از Yahoo Finance"
        badge="زنده"
        lastUpdate={lastUpdate}
      />

      {rows.length === 0 ? (
        <SkeletonGrid count={10} />
      ) : (
        <>
          {kpis.length > 0 && (
            <HeroRow
              items={kpis}
              format={(n) =>
                faDigits(
                  n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
                )
              }
            />
          )}
          {COMMODITY_GROUP_ORDER.map((gk) =>
            grouped[gk].length ? (
              <Section key={gk} title={COMMODITY_GROUP_LABELS[gk]}>
                <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
                  {grouped[gk].map((row, i) => (
                    <CommodityCard
                      key={row.symbol}
                      row={row}
                      group={gk}
                      series={series[row.symbol]}
                      index={i}
                    />
                  ))}
                </div>
              </Section>
            ) : null,
          )}
        </>
      )}
    </main>
  );
}

function CommodityCard({
  row,
  group,
  series,
  index,
}: {
  row: YFRow;
  group: "energy" | "metals" | "agri";
  series?: number[];
  index?: number;
}) {
  const dir: "up" | "down" = row.up ? "up" : "down";
  const meta = commodityMeta(row.symbol, group);
  return (
    <div
      className={cn(
        "glass-tile overflow-hidden flex flex-col px-4 pt-3 pb-0",
        index != null && "rise-in",
      )}
      style={index != null ? { animationDelay: `${Math.min(index * 22, 340)}ms` } : undefined}
    >
      <div className="flex items-center justify-between gap-2">
        <AssetBadge meta={meta} />
        <DeltaPill pct={row.pct} dir={dir} />
      </div>
      <div className="mt-2">
        <div className="text-sm font-semibold leading-snug text-foreground line-clamp-2">
          {commodityLabel(row.symbol, row.name)}
        </div>
        <div className="text-[10px] text-muted-foreground font-mono">{row.symbol}</div>
      </div>
      <div className="mt-1 text-lg font-mono font-bold text-foreground tabular-nums">
        <Odometer
          value={row.price}
          format={(n) => faDigits(n.toLocaleString("en-US", { maximumFractionDigits: 2 }))}
        />
        <span className="mr-1 text-[10px] font-normal text-muted-foreground">{row.currency}</span>
      </div>
      {series && series.length > 1 ? (
        <div className="-mx-4 mt-2">
          <Sparkline
            data={series}
            dir={dir}
            color={ACCENT[meta.accent].spark}
            responsive
            height={38}
            strokeWidth={1.6}
            fillOpacity={0.45}
          />
        </div>
      ) : (
        <div className="pb-3" />
      )}
    </div>
  );
}

/* ─────────────────────────── Heatmap view ─────────────────────────── */

type SteelRow = {
  code: string;
  rank: number | null;
  name: string;
  market_cap: number | null;
  price: number | null;
  today_pct: number | null;
  dir: "up" | "down" | "";
  country: string;
  fetched_at: string;
};

const STEEL_FLAGS: Record<string, string> = {
  USA: "🇺🇸",
  "United States": "🇺🇸",
  Luxembourg: "🇱🇺",
  India: "🇮🇳",
  "S. Korea": "🇰🇷",
  "South Korea": "🇰🇷",
  Taiwan: "🇹🇼",
  China: "🇨🇳",
  Japan: "🇯🇵",
  Germany: "🇩🇪",
  Brazil: "🇧🇷",
  Russia: "🇷🇺",
  Italy: "🇮🇹",
  Spain: "🇪🇸",
  Turkey: "🇹🇷",
  Australia: "🇦🇺",
  "United Kingdom": "🇬🇧",
  France: "🇫🇷",
  Netherlands: "🇳🇱",
  Austria: "🇦🇹",
  Canada: "🇨🇦",
  Mexico: "🇲🇽",
  Sweden: "🇸🇪",
  Finland: "🇫🇮",
  Argentina: "🇦🇷",
  Greece: "🇬🇷",
  "South Africa": "🇿🇦",
  Indonesia: "🇮🇩",
  Vietnam: "🇻🇳",
  Thailand: "🇹🇭",
  Poland: "🇵🇱",
  Norway: "🇳🇴",
  Belgium: "🇧🇪",
  Switzerland: "🇨🇭",
  Ukraine: "🇺🇦",
  Egypt: "🇪🇬",
  "Saudi Arabia": "🇸🇦",
};

// ASCII→Persian digits for any already-formatted string (keeps $ % T B . , # ▼ ▲)
const faDigits = (s: string) => s.replace(/[0-9]/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);

const usd = (n: number, frac = 2) =>
  faDigits(n.toLocaleString("en-US", { minimumFractionDigits: frac, maximumFractionDigits: frac }));

const steelAvatarBg = (rank: number | null) =>
  `linear-gradient(135deg, oklch(0.55 0.18 ${((rank || 0) * 47) % 360}), oklch(0.5 0.2 ${((rank || 0) * 47 + 45) % 360}))`;

// Company logo from companiesmarketcap.com (ticker-based). Falls back to a
// colored initials avatar when the logo is missing or blocked.
function SteelLogo({ row, size = 38 }: { row: SteelRow; size?: number }) {
  const [err, setErr] = useState(false);
  // companiesmarketcap keys logos by the FULL ticker incl. exchange suffix
  // (e.g. TATASTEEL.NS, 5401.T) — stripping it 404s every non-US company.
  const ticker = row.code.trim();
  const dim = { width: size, height: size };
  if (err || !ticker) {
    return (
      <div
        style={{ ...dim, background: steelAvatarBg(row.rank) }}
        className="shrink-0 grid place-items-center rounded-full text-[11px] font-bold uppercase text-white"
      >
        {row.name.slice(0, 2)}
      </div>
    );
  }
  return (
    <img
      src={`https://companiesmarketcap.com/img/company-logos/64/${ticker}.webp`}
      onError={() => setErr(true)}
      alt={`${row.name} logo`}
      style={dim}
      loading="lazy"
      className="shrink-0 rounded-full bg-white object-contain p-0.5 ring-1 ring-border"
    />
  );
}

function SteelStocksView() {
  const [rows, setRows] = useState<SteelRow[]>([]);
  const [lastUpdate, setLastUpdate] = useState("");

  useEffect(() => {
    let active = true;
    const load = async () => {
      try {
        const data: SteelRow[] = await fetch(`${API}/api/steel-stocks`, {
          cache: "no-store",
        }).then((r) => r.json());
        if (!active || !Array.isArray(data) || data.length === 0) return;
        setRows(data);
        setLastUpdate(new Date().toLocaleTimeString("fa-IR"));
      } catch {
        /* keep last good data */
      }
    };
    load();
    const id = setInterval(load, 5 * 60_000);
    return () => {
      active = false;
      clearInterval(id);
    };
  }, []);

  const totalCap = useMemo(() => rows.reduce((s, r) => s + (r.market_cap || 0), 0), [rows]);

  const seriesInput = useMemo(
    () => rows.map((r) => ({ name: r.code, price: r.price || 0, pct: r.today_pct || 0 })),
    [rows],
  );
  const series = useRollingSeries(seriesInput);
  const kpis: KpiItem[] = rows.slice(0, 3).map((r) => ({
    label: r.name,
    value: r.price || 0,
    unit: "$",
    pct: r.today_pct ?? undefined,
    dir: r.dir,
    series: series[r.code],
    decimals: 2,
  }));

  const capStr = (v: number | null) =>
    v == null
      ? "—"
      : faDigits("$" + (v >= 1000 ? (v / 1000).toFixed(2) + "T" : v.toFixed(2) + "B"));
  const pctText = (r: SteelRow) =>
    r.today_pct != null
      ? faDigits(
          (r.dir === "down" ? "▼ " : r.dir === "up" ? "▲ " : "") +
            Math.abs(r.today_pct).toFixed(2) +
            "%",
        )
      : "—";
  const pctCls = (r: SteelRow) =>
    r.dir === "down"
      ? "text-red-600"
      : r.dir === "up"
        ? "text-emerald-700"
        : "text-muted-foreground";

  return (
    <main className="mx-auto max-w-6xl p-4 sm:p-6 space-y-5">
      <ViewHeader
        live={rows.length > 0}
        title="قیمت سهام شرکت‌ها"
        subtitle={faDigits(
          `${rows.length} شرکت فولادی · مجموع ارزش بازار ${
            totalCap >= 1000 ? (totalCap / 1000).toFixed(2) + "T$" : totalCap.toFixed(0) + "B$"
          }`,
        )}
        badge="هر ۵ دقیقه"
        lastUpdate={lastUpdate}
      />

      {rows.length > 0 && <KpiBar items={kpis} />}

      {rows.length === 0 ? (
        <SkeletonGrid count={12} />
      ) : (
        <div className="relative panel p-3 sm:p-4">
          <div className="hidden md:grid grid-cols-12 gap-3 px-3 pb-2 text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
            <div className="col-span-1 text-center">#</div>
            <div className="col-span-3">شرکت</div>
            <div className="col-span-2">کشور</div>
            <div className="col-span-2 text-left">ارزش بازار</div>
            <div className="col-span-2 text-left">قیمت</div>
            <div className="col-span-2 text-center">تغییر امروز</div>
          </div>

          <motion.div
            className="space-y-2"
            variants={{ visible: { transition: { staggerChildren: 0.035 } } }}
            initial="hidden"
            animate="visible"
          >
            {rows.map((r) => (
              <motion.div
                key={r.code}
                variants={{
                  hidden: { opacity: 0, x: -20, filter: "blur(4px)" },
                  visible: {
                    opacity: 1,
                    x: 0,
                    filter: "blur(0px)",
                    transition: { type: "spring", stiffness: 380, damping: 26 },
                  },
                }}
                className="relative overflow-hidden rounded-xl border border-border bg-card"
              >
                <div
                  className="pointer-events-none absolute inset-0"
                  style={{
                    background:
                      r.dir === "up"
                        ? "linear-gradient(to left, oklch(0.6 0.16 150 / 0.15), transparent 32%)"
                        : r.dir === "down"
                          ? "linear-gradient(to left, oklch(0.62 0.2 25 / 0.14), transparent 32%)"
                          : "none",
                  }}
                />
                {/* Mobile: clean stacked card */}
                <div className="relative flex flex-col gap-2.5 px-3.5 py-3 md:hidden">
                  <div className="flex items-center gap-3">
                    <SteelLogo row={r} />
                    <div className="min-w-0 flex-1">
                      <div className="font-semibold text-foreground truncate">{r.name}</div>
                      <div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-muted-foreground">
                        <span className="font-mono shrink-0">{r.code}</span>
                        <span className="shrink-0">·</span>
                        <span className="shrink-0">{STEEL_FLAGS[r.country] || "🏳️"}</span>
                        <span className="truncate">{r.country}</span>
                      </div>
                    </div>
                    <span className="shrink-0 font-mono text-xs text-muted-foreground">
                      {r.rank != null ? faDigits("#" + r.rank) : "—"}
                    </span>
                  </div>
                  <div className="flex items-end justify-between gap-2 border-t border-border/60 pt-2.5">
                    <div className="flex flex-col">
                      <span className="text-[10px] text-muted-foreground">قیمت</span>
                      <span className="font-mono text-sm font-bold tabular-nums text-foreground">
                        {r.price != null ? "$" + usd(r.price) : "—"}
                      </span>
                    </div>
                    <div className="flex flex-col">
                      <span className="text-[10px] text-muted-foreground">ارزش بازار</span>
                      <span className="font-mono text-sm tabular-nums text-foreground">
                        {capStr(r.market_cap)}
                      </span>
                    </div>
                    <span className={cn("font-mono text-sm font-semibold tabular-nums", pctCls(r))}>
                      {pctText(r)}
                    </span>
                  </div>
                </div>

                {/* Desktop: aligned table row */}
                <div className="relative hidden md:grid grid-cols-12 gap-3 items-center px-3 py-3">
                  <div className="col-span-1 text-center text-lg font-bold text-muted-foreground font-mono">
                    {r.rank != null ? faDigits(String(r.rank)) : "—"}
                  </div>
                  <div className="col-span-3 flex items-center gap-2.5 min-w-0">
                    <SteelLogo row={r} size={36} />
                    <div className="min-w-0">
                      <div className="font-medium text-foreground truncate">{r.name}</div>
                      <div className="text-[11px] text-muted-foreground font-mono">{r.code}</div>
                    </div>
                  </div>
                  <div className="col-span-2 flex items-center gap-2 text-sm min-w-0">
                    <span>{STEEL_FLAGS[r.country] || "🏳️"}</span>
                    <span className="text-foreground truncate">{r.country}</span>
                  </div>
                  <div className="col-span-2 text-left font-mono tabular-nums text-foreground whitespace-nowrap">
                    {capStr(r.market_cap)}
                  </div>
                  <div className="col-span-2 text-left font-mono tabular-nums text-foreground whitespace-nowrap">
                    {r.price != null ? "$" + usd(r.price) : "—"}
                  </div>
                  <div className="col-span-2 flex items-center justify-center gap-2">
                    <SteelBar pct={r.today_pct} dir={r.dir} />
                    <span
                      className={cn(
                        "text-sm font-mono font-semibold tabular-nums w-16 text-left",
                        pctCls(r),
                      )}
                    >
                      {pctText(r)}
                    </span>
                  </div>
                </div>
              </motion.div>
            ))}
          </motion.div>
        </div>
      )}
    </main>
  );
}

function SteelBar({ pct, dir }: { pct: number | null; dir: "up" | "down" | "" }) {
  const mag = pct == null ? 0 : Math.min(Math.abs(pct) / 6, 1);
  const filled = Math.round(mag * 8);
  const color =
    dir === "down" ? "bg-red-500" : dir === "up" ? "bg-emerald-500" : "bg-muted-foreground/40";
  return (
    <div className="hidden lg:flex gap-0.5 items-center">
      {Array.from({ length: 8 }).map((_, i) => (
        <span key={i} className={cn("w-1 h-4 rounded-full", i < filled ? color : "bg-muted/50")} />
      ))}
    </div>
  );
}

type WorldRow = {
  country: string;
  gdp: number | null;
  gdp_growth: number | null;
  interest_rate: number | null;
  inflation_rate: number | null;
  jobless_rate: number | null;
  gov_budget: number | null;
  debt_gdp: number | null;
  current_account: number | null;
  population: number | null;
  fetched_at: string;
};

const COUNTRY_FA: Record<string, { fa: string; flag: string }> = {
  "United States": { fa: "آمریکا", flag: "🇺🇸" },
  China: { fa: "چین", flag: "🇨🇳" },
  "Euro Area": { fa: "منطقه یورو", flag: "🇪🇺" },
  Germany: { fa: "آلمان", flag: "🇩🇪" },
  Japan: { fa: "ژاپن", flag: "🇯🇵" },
  India: { fa: "هند", flag: "🇮🇳" },
  "United Kingdom": { fa: "انگلیس", flag: "🇬🇧" },
  France: { fa: "فرانسه", flag: "🇫🇷" },
  Italy: { fa: "ایتالیا", flag: "🇮🇹" },
  Canada: { fa: "کانادا", flag: "🇨🇦" },
  "South Korea": { fa: "کره جنوبی", flag: "🇰🇷" },
  Russia: { fa: "روسیه", flag: "🇷🇺" },
  Brazil: { fa: "برزیل", flag: "🇧🇷" },
  Australia: { fa: "استرالیا", flag: "🇦🇺" },
  Spain: { fa: "اسپانیا", flag: "🇪🇸" },
  Mexico: { fa: "مکزیک", flag: "🇲🇽" },
  Turkey: { fa: "ترکیه", flag: "🇹🇷" },
  Indonesia: { fa: "اندونزی", flag: "🇮🇩" },
};

type Polarity = "tealHigh" | "orangeHigh" | "neutral";
const WE_COLS: {
  key: keyof WorldRow;
  label: string;
  pol: Polarity;
  suffix?: string;
  gdp?: boolean;
}[] = [
  { key: "gdp", label: "تولید ناخالص", pol: "neutral", gdp: true },
  { key: "gdp_growth", label: "رشد سالانه", pol: "tealHigh", suffix: "٪" },
  { key: "gov_budget", label: "تراز بودجه/GDP", pol: "tealHigh", suffix: "٪" },
  { key: "debt_gdp", label: "بدهی/GDP", pol: "orangeHigh", suffix: "٪" },
  { key: "interest_rate", label: "نرخ بهره", pol: "orangeHigh", suffix: "٪" },
  { key: "inflation_rate", label: "تورم", pol: "orangeHigh", suffix: "٪" },
  { key: "jobless_rate", label: "بیکاری", pol: "orangeHigh", suffix: "٪" },
  { key: "current_account", label: "حساب جاری/GDP", pol: "tealHigh", suffix: "٪" },
];

// Diverging teal <-> orange. t in [0,1]: 0 = teal end, 1 = orange end.
function weColor(t: number): string {
  const c = Math.max(0, Math.min(1, t));
  const dist = Math.abs(c - 0.5) * 2;
  const hue = c < 0.5 ? 195 : 45;
  const light = 0.92 - dist * 0.07;
  const chroma = 0.02 + dist * 0.13;
  return `oklch(${light.toFixed(3)} ${chroma.toFixed(3)} ${hue})`;
}

/* ─────────────────────────── Reports library view ─────────────────────────── */

type RepFile = {
  name: string;
  date: string | null;
  size: number;
  kind: string;
  source: string;
  category: string;
  folder: string;
};
type RepCategory = {
  folder: string;
  category: string;
  count: number;
  latest: string | null;
  preview: RepFile[];
};
type RepCat = RepCategory & { source: string };
type RepSource = {
  source: string;
  categories: RepCategory[];
  count: number;
  category_count: number;
  latest: string | null;
};
type RepIndex = {
  sources: RepSource[];
  latest: RepFile[];
  total: number;
  dir: string;
  exists: boolean;
};

const SOURCE_META: Record<string, { fa: string; desc: string; grad: string; soft: string }> = {
  Fastmarkets: {
    fa: "Fastmarkets",
    desc: "فلزات، فولاد و مواد اولیه",
    grad: "from-amber-500 to-orange-600",
    soft: "bg-amber-500/10 text-amber-700",
  },
  FerroAlloyNet: {
    fa: "FerroAlloyNet",
    desc: "فروآلیاژها و فلزات جزئی",
    grad: "from-sky-500 to-indigo-600",
    soft: "bg-indigo-500/10 text-indigo-700",
  },
  Delphica: {
    fa: "Delphica",
    desc: "قیمت فولاد منطقهٔ CIS",
    grad: "from-emerald-500 to-teal-600",
    soft: "bg-emerald-500/10 text-emerald-700",
  },
  Platts: {
    fa: "Platts · S&P",
    desc: "قیمت روزانهٔ فلزات",
    grad: "from-rose-500 to-pink-600",
    soft: "bg-rose-500/10 text-rose-700",
  },
  Baiinfo: {
    fa: "Baiinfo",
    desc: "بازار فولاد چین",
    grad: "from-violet-500 to-purple-600",
    soft: "bg-violet-500/10 text-violet-700",
  },
};
const srcMeta = (s: string) =>
  SOURCE_META[s] || {
    fa: s,
    desc: "",
    grad: "from-slate-500 to-slate-700",
    soft: "bg-slate-500/10 text-slate-700",
  };

const CAT_FA: Record<string, string> = {
  "Base Metals": "فلزات پایه",
  "Ores and Alloys": "سنگ‌ها و آلیاژها",
  Steel: "فولاد",
  "Steel Price Reports": "گزارش قیمت فولاد",
  "Steel Raw Materials": "مواد اولیهٔ فولاد",
  "Steel Scrap": "قراضهٔ فولاد",
  Aluminum: "آلومینیوم",
  Chrome: "کروم",
  "Ferro Silicon": "فروسیلیس",
  "Iron Ore": "سنگ‌آهن",
  Magnesium: "منیزیم",
  Manganese: "منگنز",
  Molybdenum: "مولیبدن",
  Nickel: "نیکل",
  "Silicon Metal": "سیلیکون متال",
  Titanium: "تیتانیوم",
  Tungsten: "تنگستن",
  Vanadium: "وانادیوم",
  Zircon: "زیرکن",
  "CIS Steel Prices Weekly": "قیمت هفتگی فولاد CIS",
  "Steel Prices": "قیمت فولاد",
  China: "بازار چین",
  "Metals Daily": "فلزات روزانه",
};
const catFa = (c: string) => CAT_FA[c] || c;

const KIND_FA: Record<string, string> = {
  daily: "روزانه",
  weekly: "هفتگی",
  monthly: "ماهانه",
  annual: "سالانه",
  report: "گزارش",
};
const kindFa = (k: string) => KIND_FA[k] || "گزارش";

const fmtSize = (b: number) =>
  faDigits(
    b >= 1048576 ? (b / 1048576).toFixed(1) + " MB" : Math.max(1, Math.round(b / 1024)) + " KB",
  );
const repFileUrl = (f: { folder: string; name: string }) =>
  `${API}/api/reports/file?folder=${encodeURIComponent(f.folder)}&name=${encodeURIComponent(f.name)}`;

// Reports tab is temporarily gated behind a "coming soon" screen.
const REPORTS_COMING_SOON = true;

function ReportsView() {
  const [idx, setIdx] = useState<RepIndex | null>(null);
  const [loading, setLoading] = useState(true);
  const [sourceFilter, setSourceFilter] = useState<string>("all");
  const [query, setQuery] = useState("");
  const [openCat, setOpenCat] = useState<RepCat | null>(null);

  useEffect(() => {
    if (REPORTS_COMING_SOON) return; // don't even fetch the report index
    let active = true;
    fetch(`${API}/api/reports`, { cache: "no-store" })
      .then((r) => r.json())
      .then((d: RepIndex) => active && setIdx(d))
      .catch(() => {})
      .finally(() => active && setLoading(false));
    return () => {
      active = false;
    };
  }, []);

  const allCats = useMemo<RepCat[]>(
    () =>
      idx ? idx.sources.flatMap((s) => s.categories.map((c) => ({ ...c, source: s.source }))) : [],
    [idx],
  );

  const filteredCats = useMemo(() => {
    let list = allCats;
    if (sourceFilter !== "all") list = list.filter((c) => c.source === sourceFilter);
    const q = query.trim().toLowerCase();
    if (q)
      list = list.filter(
        (c) =>
          c.category.toLowerCase().includes(q) ||
          catFa(c.category).includes(q) ||
          c.source.toLowerCase().includes(q),
      );
    return [...list].sort((a, b) => (b.latest || "").localeCompare(a.latest || ""));
  }, [allCats, sourceFilter, query]);

  const latest = idx?.latest || [];
  const featured = latest[0];
  const news = latest.slice(1, 7);

  if (REPORTS_COMING_SOON) {
    return (
      <main className="mx-auto max-w-6xl p-4 sm:p-6 space-y-6">
        <ViewHeader
          live={false}
          title="گزارش‌ها"
          subtitle="آرشیو گزارش‌های بازار فلزات و فولاد از منابع معتبر جهانی"
          badge="به‌زودی"
          lastUpdate=""
        />
        <div className="relative grid min-h-[60vh] place-items-center">
          {/* blurred placeholder so the real content is never shown */}
          <div
            aria-hidden
            className="pointer-events-none absolute inset-0 grid grid-cols-1 gap-3 opacity-40 blur-[3px] sm:grid-cols-2 lg:grid-cols-3"
          >
            {Array.from({ length: 9 }).map((_, i) => (
              <div key={i} className="panel-2 h-28" />
            ))}
          </div>
          {/* popup */}
          <div className="relative z-10 max-w-md rounded-2xl border border-primary/30 bg-card/90 px-8 py-10 text-center shadow-xl backdrop-blur">
            <div className="mx-auto mb-4 grid h-14 w-14 place-items-center rounded-2xl bg-primary/12 text-primary">
              <Newspaper className="h-7 w-7" />
            </div>
            <h2 className="text-2xl font-bold text-foreground">به‌زودی</h2>
            <p className="mt-2 text-sm leading-7 text-muted-foreground">
              بخش گزارش‌ها در حال آماده‌سازی است و به‌زودی در دسترس قرار می‌گیرد.
            </p>
          </div>
        </div>
      </main>
    );
  }

  return (
    <main className="mx-auto max-w-6xl p-4 sm:p-6 space-y-8">
      <ViewHeader
        live={!!idx?.exists}
        title="گزارش‌ها"
        subtitle="آرشیو گزارش‌های بازار فلزات و فولاد از معتبرترین منابع جهانی"
        badge={idx ? `${formatNumber(idx.total)} گزارش` : "..."}
        lastUpdate={featured?.date ? toJalali(featured.date) : ""}
      />

      {loading ? (
        <SkeletonGrid count={8} />
      ) : !idx?.exists ? (
        <EmptyState text="پوشهٔ گزارش‌ها روی سرور پیدا نشد." />
      ) : (
        <>
          <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
            <RepStat
              icon={<FileText className="w-4 h-4" />}
              label="کل گزارش‌ها"
              value={formatNumber(idx.total)}
            />
            <RepStat
              icon={<Activity className="w-4 h-4" />}
              label="منابع"
              value={formatNumber(idx.sources.length)}
            />
            <RepStat
              icon={<FolderOpen className="w-4 h-4" />}
              label="دسته‌بندی‌ها"
              value={formatNumber(allCats.length)}
            />
            <RepStat
              icon={<Calendar className="w-4 h-4" />}
              label="تازه‌ترین"
              value={featured?.date ? toJalali(featured.date) : "—"}
            />
          </div>

          {featured && (
            <section className="space-y-3">
              <RepSectionTitle
                icon={<Newspaper className="w-4 h-4" />}
                title="تازه‌ترین گزارش‌ها"
                hint="به‌روزترین گزارش‌های منتشرشده"
              />
              <div className="grid lg:grid-cols-2 gap-3">
                <RepFeatured file={featured} />
                <div className="grid sm:grid-cols-2 gap-3">
                  {news.map((f, i) => (
                    <RepNewsCard key={f.folder + f.name} file={f} index={i} />
                  ))}
                </div>
              </div>
            </section>
          )}

          <section className="space-y-3">
            <RepSectionTitle
              icon={<Activity className="w-4 h-4" />}
              title="منابع گزارش"
              hint="برای فیلتر، روی یک منبع بزنید"
            />
            <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
              {idx.sources.map((s, i) => (
                <RepSourceCard
                  key={s.source}
                  src={s}
                  index={i}
                  active={sourceFilter === s.source}
                  onClick={() => setSourceFilter((cur) => (cur === s.source ? "all" : s.source))}
                />
              ))}
            </div>
          </section>

          <section className="space-y-3">
            <div className="flex flex-wrap items-center justify-between gap-3">
              <RepSectionTitle
                icon={<FolderOpen className="w-4 h-4" />}
                title="دسته‌بندی‌ها"
                hint={`${filteredCats.length} دسته`}
              />
              <div className="relative">
                <Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
                <input
                  value={query}
                  onChange={(e) => setQuery(e.target.value)}
                  placeholder="جستجوی دسته یا منبع…"
                  className="h-10 w-full sm:w-64 rounded-lg border border-border bg-card pr-9 pl-3 text-sm outline-none focus:border-primary/60 focus:ring-2 focus:ring-primary/15"
                />
              </div>
            </div>
            <div className="flex flex-wrap gap-2">
              <RepChip active={sourceFilter === "all"} onClick={() => setSourceFilter("all")}>
                همه
              </RepChip>
              {idx.sources.map((s) => (
                <RepChip
                  key={s.source}
                  active={sourceFilter === s.source}
                  onClick={() => setSourceFilter(s.source)}
                >
                  {srcMeta(s.source).fa}
                </RepChip>
              ))}
            </div>
            <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
              {filteredCats.map((c, i) => (
                <RepCategoryCard key={c.folder} cat={c} index={i} onOpen={() => setOpenCat(c)} />
              ))}
            </div>
            {filteredCats.length === 0 && <EmptyState text="موردی پیدا نشد." />}
          </section>
        </>
      )}

      <AnimatePresence>
        {openCat && <RepCategoryModal cat={openCat} onClose={() => setOpenCat(null)} />}
      </AnimatePresence>
    </main>
  );
}

function RepStat({ icon, label, value }: { icon: ReactNode; label: string; value: string }) {
  return (
    <div className="panel p-3.5 flex items-center gap-3">
      <div className="grid h-9 w-9 place-items-center rounded-lg bg-primary/12 text-primary shrink-0">
        {icon}
      </div>
      <div className="min-w-0">
        <div className="text-[11px] text-muted-foreground truncate">{label}</div>
        <div className="font-bold text-foreground tabular-nums truncate">{value}</div>
      </div>
    </div>
  );
}

function RepSectionTitle({ icon, title, hint }: { icon: ReactNode; title: string; hint?: string }) {
  return (
    <div className="flex items-center gap-2.5">
      <span className="grid h-7 w-7 place-items-center rounded-md bg-primary/12 text-primary">
        {icon}
      </span>
      <h2 className="text-base font-bold text-foreground">{title}</h2>
      {hint && <span className="text-xs text-muted-foreground">· {hint}</span>}
    </div>
  );
}

function RepFeatured({ file }: { file: RepFile }) {
  const m = srcMeta(file.source);
  return (
    <a
      href={repFileUrl(file)}
      target="_blank"
      rel="noreferrer"
      className={cn(
        "group relative overflow-hidden rounded-2xl p-5 text-white shadow-md min-h-[180px] flex flex-col justify-between bg-gradient-to-br",
        m.grad,
      )}
    >
      <div className="flex items-center justify-between">
        <span className="inline-flex items-center gap-1.5 rounded-full bg-white/20 px-2.5 py-1 text-[11px] font-semibold backdrop-blur">
          <span className="w-1.5 h-1.5 rounded-full bg-white animate-pulse" /> تازه‌ترین
        </span>
        <span className="text-[11px] font-mono opacity-90">
          {file.date ? toJalali(file.date) : ""}
        </span>
      </div>
      <div className="relative z-10">
        <div className="text-xs leading-relaxed opacity-90">
          {m.fa} · {catFa(file.category)}
        </div>
        <h3 className="mt-1 text-xl font-bold leading-snug line-clamp-2">
          {catFa(file.category)} — {kindFa(file.kind)}
        </h3>
        <div className="mt-3 inline-flex items-center gap-1.5 text-sm font-semibold">
          مشاهدهٔ گزارش
          <ArrowUpRight className="w-4 h-4 transition group-hover:-translate-y-0.5" />
        </div>
      </div>
      <FileText className="pointer-events-none absolute -left-5 -bottom-5 w-28 h-28 opacity-15" />
    </a>
  );
}

function RepNewsCard({ file, index }: { file: RepFile; index: number }) {
  const m = srcMeta(file.source);
  return (
    <a
      href={repFileUrl(file)}
      target="_blank"
      rel="noreferrer"
      className="tile rise-in px-4 py-3 flex flex-col gap-2 group"
      style={{ animationDelay: `${Math.min(index * 30, 240)}ms` }}
    >
      <div className="flex items-center justify-between gap-2">
        <span
          className={cn(
            "inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold",
            m.soft,
          )}
        >
          {m.fa}
        </span>
        <span className="text-[10px] text-muted-foreground font-mono">
          {file.date ? toJalali(file.date) : "—"}
        </span>
      </div>
      <div className="text-sm font-semibold text-foreground line-clamp-2 leading-snug">
        {catFa(file.category)} — {kindFa(file.kind)}
      </div>
      <div className="mt-auto flex items-center justify-between text-[11px] text-muted-foreground">
        <span>{fmtSize(file.size)}</span>
        <span className="inline-flex items-center gap-1 text-primary font-medium opacity-0 group-hover:opacity-100 transition">
          باز کردن <ExternalLink className="w-3 h-3" />
        </span>
      </div>
    </a>
  );
}

function RepSourceCard({
  src,
  index,
  active,
  onClick,
}: {
  src: RepSource;
  index: number;
  active: boolean;
  onClick: () => void;
}) {
  const m = srcMeta(src.source);
  return (
    <button
      onClick={onClick}
      className={cn(
        "rise-in text-right relative overflow-hidden rounded-2xl border bg-card p-4 transition hover:-translate-y-0.5 hover:shadow-md",
        active ? "border-primary ring-2 ring-primary/30" : "border-border",
      )}
      style={{ animationDelay: `${Math.min(index * 40, 240)}ms` }}
    >
      <div className="flex items-center gap-3">
        <div
          className={cn(
            "grid h-11 w-11 shrink-0 place-items-center rounded-xl text-white text-lg font-bold bg-gradient-to-br",
            m.grad,
          )}
        >
          {m.fa.slice(0, 1)}
        </div>
        <div className="min-w-0">
          <div className="font-bold text-foreground truncate">{m.fa}</div>
          <div className="text-[11px] text-muted-foreground truncate">{m.desc}</div>
        </div>
      </div>
      <div className="mt-3 flex items-center justify-between text-xs">
        <span className="text-muted-foreground">
          <b className="text-foreground font-mono">{formatNumber(src.count)}</b> گزارش ·{" "}
          {src.category_count} دسته
        </span>
        <span className="text-muted-foreground font-mono">
          {src.latest ? toJalali(src.latest) : "—"}
        </span>
      </div>
    </button>
  );
}

function RepChip({
  active,
  onClick,
  children,
}: {
  active: boolean;
  onClick: () => void;
  children: ReactNode;
}) {
  return (
    <button
      onClick={onClick}
      className={cn(
        "rounded-full px-3.5 py-1.5 text-xs font-medium transition border",
        active
          ? "bg-primary text-primary-foreground border-primary"
          : "bg-card text-muted-foreground border-border hover:text-foreground hover:border-primary/40",
      )}
    >
      {children}
    </button>
  );
}

function RepCategoryCard({
  cat,
  index,
  onOpen,
}: {
  cat: RepCat;
  index: number;
  onOpen: () => void;
}) {
  const m = srcMeta(cat.source);
  return (
    <button
      onClick={onOpen}
      className="rise-in text-right tile px-4 py-3.5 flex flex-col gap-2 group"
      style={{ animationDelay: `${Math.min(index * 30, 300)}ms` }}
    >
      <div className="flex items-start justify-between gap-2">
        <div className="min-w-0">
          <div className="font-bold text-foreground truncate">{catFa(cat.category)}</div>
          <span
            className={cn(
              "mt-1 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold",
              m.soft,
            )}
          >
            {m.fa}
          </span>
        </div>
        <span className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-primary/10 text-primary">
          <FolderOpen className="w-4 h-4" />
        </span>
      </div>
      <div className="mt-1 flex items-center justify-between text-xs text-muted-foreground">
        <span>
          <b className="text-foreground font-mono">{formatNumber(cat.count)}</b> گزارش
        </span>
        <span className="font-mono">{cat.latest ? toJalali(cat.latest) : "—"}</span>
      </div>
      <div className="flex items-center gap-1 text-[11px] text-primary font-medium opacity-0 group-hover:opacity-100 transition">
        مشاهدهٔ فهرست <ChevronLeft className="w-3.5 h-3.5" />
      </div>
    </button>
  );
}

function RepCategoryModal({ cat, onClose }: { cat: RepCat; onClose: () => void }) {
  const [files, setFiles] = useState<RepFile[] | null>(null);
  const [q, setQ] = useState("");
  const m = srcMeta(cat.source);
  useEffect(() => {
    let active = true;
    fetch(`${API}/api/reports/files?folder=${encodeURIComponent(cat.folder)}`, {
      cache: "no-store",
    })
      .then((r) => r.json())
      .then((d: RepFile[]) => active && setFiles(Array.isArray(d) ? d : []))
      .catch(() => active && setFiles([]));
    return () => {
      active = false;
    };
  }, [cat.folder]);

  const shown = useMemo(() => {
    if (!files) return [];
    const s = q.trim().toLowerCase();
    return s
      ? files.filter((f) => f.name.toLowerCase().includes(s) || (f.date || "").includes(s))
      : files;
  }, [files, q]);

  return (
    <motion.div
      className="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/50 backdrop-blur-sm sm:p-4"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      onClick={onClose}
    >
      <motion.div
        className="w-full sm:max-w-2xl max-h-[88vh] bg-card rounded-t-2xl sm:rounded-2xl border border-border shadow-2xl flex flex-col overflow-hidden"
        initial={{ y: 30, opacity: 0, scale: 0.98 }}
        animate={{ y: 0, opacity: 1, scale: 1 }}
        exit={{ y: 20, opacity: 0 }}
        transition={{ type: "spring", stiffness: 320, damping: 30 }}
        onClick={(e) => e.stopPropagation()}
      >
        <div className={cn("p-4 text-white bg-gradient-to-br", m.grad)}>
          <div className="flex items-start justify-between gap-3">
            <div>
              <div className="text-[11px] opacity-90">{m.fa}</div>
              <h3 className="text-lg font-bold">{catFa(cat.category)}</h3>
              <div className="text-xs opacity-90 mt-0.5">{formatNumber(cat.count)} گزارش</div>
            </div>
            <button
              onClick={onClose}
              className="rounded-lg bg-white/20 p-1.5 hover:bg-white/30 transition"
            >
              <X className="w-4 h-4" />
            </button>
          </div>
        </div>
        <div className="p-3 border-b border-border">
          <div className="relative">
            <Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
            <input
              value={q}
              onChange={(e) => setQ(e.target.value)}
              placeholder="جستجو در گزارش‌ها…"
              className="h-10 w-full rounded-lg border border-border bg-background pr-9 pl-3 text-sm outline-none focus:border-primary/60"
            />
          </div>
        </div>
        <div className="overflow-y-auto p-2 flex-1">
          {!files ? (
            <div className="p-8 grid place-items-center text-muted-foreground">
              <Loader2 className="w-6 h-6 animate-spin" />
            </div>
          ) : shown.length === 0 ? (
            <EmptyState text="گزارشی یافت نشد." />
          ) : (
            <div className="space-y-1.5">
              {shown.map((f) => (
                <div
                  key={f.name}
                  className="flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 hover:border-primary/40 hover:bg-accent/40 transition"
                >
                  <span className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-red-500/10 text-red-600">
                    <FileText className="w-4 h-4" />
                  </span>
                  <div className="min-w-0 flex-1">
                    <div className="text-xs font-medium text-foreground truncate" dir="ltr">
                      {f.name}
                    </div>
                    <div className="flex items-center gap-2 text-[11px] text-muted-foreground font-mono mt-0.5">
                      <span>{f.date ? toJalali(f.date) : "—"}</span>
                      <span>·</span>
                      <span>{fmtSize(f.size)}</span>
                    </div>
                  </div>
                  <a
                    href={repFileUrl(f)}
                    target="_blank"
                    rel="noreferrer"
                    className="shrink-0 inline-flex items-center gap-1 rounded-lg bg-primary/10 text-primary px-3 py-1.5 text-xs font-semibold hover:bg-primary/20 transition"
                  >
                    باز کردن <ExternalLink className="w-3.5 h-3.5" />
                  </a>
                </div>
              ))}
            </div>
          )}
        </div>
      </motion.div>
    </motion.div>
  );
}

/* ─────────────────────────── Crypto view ─────────────────────────── */

type CoinRow = {
  id: string;
  symbol: string;
  name: string;
  image: string;
  rank: number | null;
  price: number | null;
  pct_1h: number | null;
  pct_24h: number | null;
  pct_7d: number | null;
  market_cap: number | null;
  volume_24h: number | null;
  high_24h: number | null;
  low_24h: number | null;
  circ_supply: number | null;
  ath: number | null;
  sparkline: number[];
  dir: "up" | "down" | "";
};
type CryptoGlobal = {
  market_cap_usd: number | null;
  volume_usd: number | null;
  btc_dominance: number | null;
  eth_dominance: number | null;
  market_cap_change_24h: number | null;
  active: number | null;
};
type TrendingCoin = {
  id: string;
  symbol: string;
  name: string;
  rank: number | null;
  thumb: string | null;
  price: number | null;
  pct_24h: number | null;
};
type CryptoPayload = {
  coins: CoinRow[];
  global: CryptoGlobal;
  trending: TrendingCoin[];
  fetched_at: string | null;
};

const cryptoPrice = (p: number | null) => {
  if (p == null || !Number.isFinite(p)) return "—";
  if (p >= 1)
    return faDigits(
      "$" + p.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
    );
  if (p >= 0.01)
    return faDigits(
      "$" + p.toLocaleString("en-US", { minimumFractionDigits: 4, maximumFractionDigits: 4 }),
    );
  return faDigits("$" + p.toLocaleString("en-US", { maximumFractionDigits: 8 }));
};
const usdAbbr = (n: number | null) => {
  if (n == null || !Number.isFinite(n)) return "—";
  if (n >= 1e12) return faDigits("$" + (n / 1e12).toFixed(2) + "T");
  if (n >= 1e9) return faDigits("$" + (n / 1e9).toFixed(2) + "B");
  if (n >= 1e6) return faDigits("$" + (n / 1e6).toFixed(2) + "M");
  return faDigits("$" + n.toLocaleString("en-US", { maximumFractionDigits: 0 }));
};
const pctClass = (p: number | null) =>
  p == null ? "text-muted-foreground" : p >= 0 ? "text-emerald-700" : "text-red-600";
const pctStr = (p: number | null) =>
  p == null ? "—" : faDigits((p >= 0 ? "▲ " : "▼ ") + Math.abs(p).toFixed(2) + "%");

function CryptoView() {
  const [data, setData] = useState<CryptoPayload | null>(null);
  const [loading, setLoading] = useState(true);
  const [query, setQuery] = useState("");

  useEffect(() => {
    let active = true;
    const load = () =>
      fetch(`${API}/api/crypto`, { cache: "no-store" })
        .then((r) => r.json())
        .then((d: CryptoPayload) => active && d && setData(d))
        .catch(() => {})
        .finally(() => active && setLoading(false));
    load();
    const id = setInterval(load, 60_000);
    return () => {
      active = false;
      clearInterval(id);
    };
  }, []);

  const coins = data?.coins || [];
  const g = data?.global;
  const trending = data?.trending || [];
  const featured = coins.slice(0, 3);
  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    return q
      ? coins.filter((c) => c.name.toLowerCase().includes(q) || c.symbol.toLowerCase().includes(q))
      : coins;
  }, [coins, query]);

  return (
    <main className="mx-auto max-w-6xl p-4 sm:p-6 space-y-7">
      <ViewHeader
        live={coins.length > 0}
        title="کریپتو"
        subtitle="بازار ارزهای دیجیتال — زنده از CoinGecko"
        badge="هر ۶۰ ثانیه"
        lastUpdate={data?.fetched_at ? new Date(data.fetched_at).toLocaleTimeString("fa-IR") : ""}
      />

      {loading && !data ? (
        <SkeletonGrid count={12} />
      ) : coins.length === 0 ? (
        <EmptyState text="داده‌ای دریافت نشد." />
      ) : (
        <>
          {g && (
            <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
              <CryptoStat
                label="ارزش کل بازار"
                value={usdAbbr(g.market_cap_usd)}
                change={g.market_cap_change_24h}
              />
              <CryptoStat label="حجم ۲۴ ساعت" value={usdAbbr(g.volume_usd)} />
              <CryptoStat
                label="دامیننس بیت‌کوین"
                value={g.btc_dominance != null ? faDigits(g.btc_dominance.toFixed(1) + "%") : "—"}
              />
              <CryptoStat
                label="دامیننس اتریوم"
                value={g.eth_dominance != null ? faDigits(g.eth_dominance.toFixed(1) + "%") : "—"}
              />
            </div>
          )}

          <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
            {featured.map((c, i) => (
              <CryptoKpiCard key={c.id} coin={c} delay={i * 0.07} />
            ))}
          </div>

          {trending.length > 0 && (
            <section className="space-y-3">
              <RepSectionTitle
                icon={<Flame className="w-4 h-4" />}
                title="ترند امروز"
                hint="پرجستجوترین کوین‌ها"
              />
              <div className="flex gap-2.5 overflow-x-auto pb-1">
                {trending.map((t) => (
                  <TrendingChip key={t.id} coin={t} />
                ))}
              </div>
            </section>
          )}

          <section className="space-y-3">
            <div className="flex flex-wrap items-center justify-between gap-3">
              <RepSectionTitle
                icon={<Coins className="w-4 h-4" />}
                title="بازار"
                hint={`${filtered.length} کوین`}
              />
              <div className="relative">
                <Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
                <input
                  value={query}
                  onChange={(e) => setQuery(e.target.value)}
                  placeholder="جستجوی کوین…"
                  className="h-10 w-full sm:w-64 rounded-lg border border-border bg-card pr-9 pl-3 text-sm outline-none focus:border-primary/60 focus:ring-2 focus:ring-primary/15"
                />
              </div>
            </div>
            <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
              {filtered.map((c, i) => (
                <CoinCard key={c.id} coin={c} index={i} />
              ))}
            </div>
            {filtered.length === 0 && <EmptyState text="کوینی پیدا نشد." />}
          </section>
        </>
      )}
    </main>
  );
}

function CryptoStat({
  label,
  value,
  change,
}: {
  label: string;
  value: string;
  change?: number | null;
}) {
  return (
    <div className="panel p-3.5">
      <div className="text-[11px] text-muted-foreground truncate">{label}</div>
      <div className="mt-1 flex items-baseline gap-2 flex-wrap">
        <span className="font-bold text-foreground tabular-nums">{value}</span>
        {change != null && (
          <span className={cn("text-[11px] font-mono font-semibold", pctClass(change))}>
            {change >= 0 ? "▲" : "▼"} {faDigits(Math.abs(change).toFixed(2) + "%")}
          </span>
        )}
      </div>
    </div>
  );
}

function CryptoKpiCard({ coin, delay }: { coin: CoinRow; delay: number }) {
  const up = (coin.pct_24h ?? 0) >= 0;
  return (
    <motion.div
      initial={{ opacity: 0, y: 14 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.45, delay, ease: [0.22, 1, 0.36, 1] }}
      className={cn(
        "relative overflow-hidden rounded-2xl border bg-card p-4 sm:p-5 shadow-sm",
        up ? "border-emerald-500/30" : "border-red-500/30",
      )}
    >
      <div
        className={cn(
          "pointer-events-none absolute inset-0 opacity-[0.06]",
          up ? "bg-emerald-500" : "bg-red-500",
        )}
      />
      <div className="relative flex items-center justify-between gap-2">
        <div className="flex items-center gap-2 min-w-0">
          <img src={coin.image} alt="" className="w-7 h-7 rounded-full shrink-0" loading="lazy" />
          <div className="min-w-0">
            <div className="text-sm font-bold text-foreground truncate">{coin.name}</div>
            <div className="text-[10px] text-muted-foreground font-mono">{coin.symbol}</div>
          </div>
        </div>
        <span
          className={cn(
            "inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-mono font-semibold shrink-0",
            up ? "bg-emerald-500/15 text-emerald-700" : "bg-red-500/15 text-red-600",
          )}
        >
          {pctStr(coin.pct_24h)}
        </span>
      </div>
      <div className="relative mt-2 flex items-end justify-between gap-3">
        <div className="font-mono text-2xl sm:text-[26px] leading-none font-bold tabular-nums text-foreground">
          {cryptoPrice(coin.price)}
        </div>
        <Sparkline
          data={coin.sparkline}
          dir={(coin.pct_7d ?? 0) >= 0 ? "up" : "down"}
          width={108}
          height={38}
          strokeWidth={1.8}
          dot
        />
      </div>
      <div className="relative mt-2 text-[11px] text-muted-foreground">
        مارکت‌کپ {usdAbbr(coin.market_cap)}
      </div>
    </motion.div>
  );
}

function CoinCard({ coin, index }: { coin: CoinRow; index: number }) {
  const sdir: "up" | "down" = (coin.pct_7d ?? 0) >= 0 ? "up" : "down";
  return (
    <div
      className={cn(
        "tile overflow-hidden rise-in flex flex-col px-4 pt-3 pb-0",
        coin.dir === "up" && "tile-up",
        coin.dir === "down" && "tile-down",
      )}
      style={{ animationDelay: `${Math.min(index * 18, 360)}ms` }}
    >
      <div className="flex items-center gap-2.5">
        <span className="text-[10px] text-muted-foreground font-mono w-5 shrink-0 text-center">
          {coin.rank != null ? faDigits(String(coin.rank)) : "—"}
        </span>
        <img src={coin.image} alt="" className="w-6 h-6 rounded-full shrink-0" loading="lazy" />
        <div className="min-w-0 flex-1">
          <div className="text-sm font-semibold text-foreground truncate">{coin.name}</div>
          <div className="text-[10px] text-muted-foreground font-mono">{coin.symbol}</div>
        </div>
        <DeltaPill pct={coin.pct_24h} dir={coin.dir} />
      </div>
      <div className="mt-2 flex items-end justify-between gap-2">
        <div className="text-base font-mono font-bold text-foreground tabular-nums">
          <Odometer value={coin.price ?? 0} format={(n) => cryptoPrice(n)} />
        </div>
        <div className="text-[10px] text-muted-foreground font-mono text-left leading-tight">
          <div>
            ۱h{" "}
            {coin.pct_1h != null
              ? faDigits((coin.pct_1h >= 0 ? "+" : "") + coin.pct_1h.toFixed(1) + "%")
              : "—"}
          </div>
          <div>{usdAbbr(coin.market_cap)}</div>
        </div>
      </div>
      {coin.sparkline && coin.sparkline.length > 1 ? (
        <div className="-mx-4 mt-2">
          <Sparkline
            data={coin.sparkline}
            dir={sdir}
            responsive
            height={34}
            strokeWidth={1.4}
            fillOpacity={0.5}
          />
        </div>
      ) : (
        <div className="pb-3" />
      )}
    </div>
  );
}

function TrendingChip({ coin }: { coin: TrendingCoin }) {
  const up = (coin.pct_24h ?? 0) >= 0;
  return (
    <div className="shrink-0 flex items-center gap-2 rounded-xl border border-border bg-card px-3 py-2">
      {coin.thumb && (
        <img src={coin.thumb} alt="" className="w-5 h-5 rounded-full" loading="lazy" />
      )}
      <span className="text-xs font-semibold text-foreground">{coin.symbol}</span>
      {coin.pct_24h != null && (
        <span
          className={cn(
            "text-[11px] font-mono font-semibold",
            up ? "text-emerald-700" : "text-red-600",
          )}
        >
          {up ? "▲" : "▼"}
          {faDigits(Math.abs(coin.pct_24h).toFixed(1) + "%")}
        </span>
      )}
    </div>
  );
}

function WorldEconomyView() {
  const [rows, setRows] = useState<WorldRow[]>([]);
  const [lastUpdate, setLastUpdate] = useState("");

  useEffect(() => {
    let active = true;
    const load = async () => {
      try {
        const data: WorldRow[] = await fetch(`${API}/api/world-economy`, {
          cache: "no-store",
        }).then((r) => r.json());
        if (!active || !Array.isArray(data) || data.length === 0) return;
        setRows(data);
        setLastUpdate(new Date().toLocaleTimeString("fa-IR"));
      } catch {
        /* keep last good data */
      }
    };
    load();
    const id = setInterval(load, 5 * 60_000); // macro data changes slowly
    return () => {
      active = false;
      clearInterval(id);
    };
  }, []);

  const ranges = useMemo(() => {
    const m: Record<string, { min: number; max: number }> = {};
    for (const col of WE_COLS) {
      if (col.pol === "neutral") continue;
      const vals = rows
        .map((r) => r[col.key] as number | null)
        .filter((v): v is number => typeof v === "number" && Number.isFinite(v));
      if (vals.length) m[col.key] = { min: Math.min(...vals), max: Math.max(...vals) };
    }
    return m;
  }, [rows]);

  const cellStyle = (col: (typeof WE_COLS)[number], val: number | null) => {
    if (col.pol === "neutral" || val == null) return undefined;
    const rg = ranges[col.key];
    if (!rg || rg.max === rg.min) return undefined;
    const norm = (val - rg.min) / (rg.max - rg.min);
    const t = col.pol === "orangeHigh" ? norm : 1 - norm;
    return { background: weColor(t) };
  };

  const fmtCell = (col: (typeof WE_COLS)[number], val: number | null) => {
    if (val == null) return "—";
    if (col.gdp) return (val / 1000).toLocaleString("fa-IR", { maximumFractionDigits: 2 }) + " T$";
    return formatNumber(val) + (col.suffix || "");
  };

  return (
    <main className="mx-auto max-w-6xl p-4 sm:p-6 space-y-5">
      <ViewHeader
        live={rows.length > 0}
        title="اقتصاد جهانی"
        subtitle="شاخص‌های کلانِ اقتصادهای بزرگ — رنگ‌بندی حرارتی برای هر ستون"
        badge="Trading Economics"
        lastUpdate={lastUpdate}
      />
      {rows.length === 0 ? (
        <SkeletonGrid count={12} />
      ) : (
        <div className="panel overflow-x-auto">
          <table className="w-full text-sm border-collapse">
            <thead>
              <tr className="border-b border-border">
                <th className="text-right p-3 font-semibold sticky right-0 bg-card z-10">کشور</th>
                {WE_COLS.map((c) => (
                  <th
                    key={c.key}
                    className="p-3 font-semibold text-muted-foreground text-xs whitespace-nowrap"
                  >
                    {c.label}
                  </th>
                ))}
              </tr>
            </thead>
            <tbody>
              {rows.map((r) => (
                <tr key={r.country} className="border-b border-border/60">
                  <td className="p-2.5 font-medium whitespace-nowrap sticky right-0 bg-card z-10">
                    <span className="ml-1.5">{COUNTRY_FA[r.country]?.flag || "🏳️"}</span>
                    {COUNTRY_FA[r.country]?.fa || r.country}
                  </td>
                  {WE_COLS.map((c) => {
                    const v = r[c.key] as number | null;
                    return (
                      <td
                        key={c.key}
                        className={cn(
                          "p-2.5 text-center tabular-nums whitespace-nowrap font-medium",
                          v == null ? "text-muted-foreground/40" : "text-foreground",
                        )}
                        style={cellStyle(c, v)}
                      >
                        {fmtCell(c, v)}
                      </td>
                    );
                  })}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </main>
  );
}

/* ─────────────────────────── Metals & steel view ─────────────────────────── */

type Range = 30 | 90 | 180 | 365 | "all";
const RANGES: { label: string; value: Range }[] = [
  { label: "۳۰ روز", value: 30 },
  { label: "۹۰ روز", value: 90 },
  { label: "۱۸۰ روز", value: 180 },
  { label: "۱ سال", value: 365 },
  { label: "کل", value: "all" },
];
const COLORS = [
  "var(--chart-1)",
  "var(--chart-2)",
  "var(--chart-3)",
  "var(--chart-4)",
  "var(--chart-5)",
];
type Tree = Record<string, Record<string, string[]>>;
type SearchResult = { title: string; group: string; category: string };

/* Curated families — backend category is unreliable, so classify by product title. Order = priority. */
const METAL_FAMILIES: { key: string; label: string; kw: string[] }[] = [
  { key: "scrap", label: "قراضه و بازیافت", kw: ["scrap"] },
  {
    key: "rare",
    label: "عناصر کمیاب خاکی",
    kw: [
      "neodymium",
      "cerium",
      "lanthanum",
      "mischmetal",
      "gadolinium",
      "yttrium",
      "samarium",
      "praseodymium",
      "terbium",
      "scandium",
      "ytterbium",
      "lutetium",
      "dysprosium",
      "europium",
      "holmium",
      "erbium",
      "didymium",
      "reo",
      "ce/re",
      "yeu",
    ],
  },
  {
    key: "ferro",
    label: "فروآلیاژها",
    kw: [
      "ferro-",
      "ferro ",
      "silico-manganese",
      "silico-chrome",
      "silico-chrom",
      "calcium-silicon",
      "calcium silicon",
    ],
  },
  {
    key: "steelprod",
    label: "محصولات فولادی",
    kw: [
      "rebar",
      "wire rod",
      "coil",
      "hot rolled",
      "cold rolled",
      "plate",
      "section",
      "angle steel",
      "channel steel",
      "beam",
      "strip",
      "pipe",
      "billet",
      "stainless",
      "sheet",
      "hgi",
      "galvan",
      "electrical steel",
      "cold heading",
      "seamless",
      "welded",
    ],
  },
  {
    key: "raw",
    label: "مواد اولیه فولاد",
    kw: [
      "iron ore",
      "coking coal",
      "thermal coal",
      "coal",
      "coke",
      "pet coke",
      "pig iron",
      "cast iron",
      "manganese ore",
      "chrome ore",
      "sinter",
      "sponge iron",
    ],
  },
  {
    key: "refractory",
    label: "دیرگداز و کربن",
    kw: [
      "magnesia",
      "fused alumina",
      "calcined alumina",
      "bauxite",
      "silicon carbide",
      "graphite",
      "carbon",
      "coal tar pitch",
      "electrode",
    ],
  },
  {
    key: "base",
    label: "فلزات پایه",
    kw: [
      "copper",
      "aluminum",
      "aluminium",
      "zinc ",
      "lead ",
      "lead conc",
      "lead ingot",
      "nickel",
      "tin ",
    ],
  },
];
const metalFamily = (title: string): string => {
  const t = " " + title.toLowerCase() + " ";
  for (const f of METAL_FAMILIES) if (f.kw.some((k) => t.includes(k))) return f.key;
  return "minor";
};
const FAMILY_LABEL: Record<string, string> = {
  ...Object.fromEntries(METAL_FAMILIES.map((f) => [f.key, f.label])),
  minor: "فلزات جزئی و ویژه",
};
const FAMILY_ORDER = [...METAL_FAMILIES.map((f) => f.key), "minor"];

function MetalsView({
  jumpTo,
  onConsumeJump,
}: {
  jumpTo: string | null;
  onConsumeJump: () => void;
}) {
  const [tree, setTree] = useState<Tree>({});
  const [selected, setSelected] = useState<string | null>(null);
  const [activeHistory, setActiveHistory] = useState<PriceRow[]>([]);
  const [compare, setCompare] = useState<string[]>([]);
  const [compareHistories, setCompareHistories] = useState<Record<string, PriceRow[]>>({});
  const [range, setRange] = useState<Range>("all");
  const [search, setSearch] = useState("");
  const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
  const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({});
  const [openCats, setOpenCats] = useState<Record<string, boolean>>({});
  const [mode, setMode] = useState<"single" | "compare">("single");
  const [loadingTree, setLoadingTree] = useState(true);
  const [loadingHistory, setLoadingHistory] = useState(false);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [watchlist, setWatchlist] = useState<string[]>([]);
  const [watchOpen, setWatchOpen] = useState(true);

  const firstSave = useRef(true);
  useEffect(() => {
    try {
      const saved = JSON.parse(localStorage.getItem("am_watchlist") || "[]");
      if (Array.isArray(saved)) setWatchlist(saved);
    } catch {
      /* ignore corrupt storage */
    }
  }, []);
  useEffect(() => {
    if (firstSave.current) {
      firstSave.current = false;
      return;
    }
    localStorage.setItem("am_watchlist", JSON.stringify(watchlist));
  }, [watchlist]);

  const openTo = (title: string, group: string, category: string) => {
    setSelected(title);
    setMode("single");
    setOpenGroups((g) => ({ ...g, [group]: true }));
    setOpenCats((c) => ({ ...c, [`${group}::${category}`]: true }));
  };

  useEffect(() => {
    fetch(`${API}/api/tree`)
      .then((r) => r.json())
      .then((data: Tree) => {
        setTree(data);
        const g = Object.keys(data)[0];
        if (g) {
          const c = Object.keys(data[g])[0];
          const t = c ? data[g][c][0] : null;
          if (t) {
            setSelected(t);
            setOpenGroups({ [g]: true });
            setOpenCats({ [`${g}::${c}`]: true });
          }
        }
      })
      .finally(() => setLoadingTree(false));
  }, []);

  // honor an external jump request (e.g. from search before this view mounted)
  useEffect(() => {
    if (jumpTo) {
      setSelected(jumpTo);
      onConsumeJump();
    }
  }, [jumpTo, onConsumeJump]);

  useEffect(() => {
    if (!selected) return;
    setLoadingHistory(true);
    const days = range === "all" ? 0 : range;
    fetch(`${API}/api/history?title=${encodeURIComponent(selected)}&days=${days}`)
      .then((r) => r.json())
      .then(setActiveHistory)
      .finally(() => setLoadingHistory(false));
  }, [selected, range]);

  useEffect(() => {
    if (mode !== "compare" || compare.length === 0) return;
    const days = range === "all" ? 0 : range;
    Promise.all(
      compare.map((title) =>
        fetch(`${API}/api/history?title=${encodeURIComponent(title)}&days=${days}`)
          .then((r) => r.json())
          .then((data) => ({ title, data })),
      ),
    ).then((results) => {
      const map: Record<string, PriceRow[]> = {};
      for (const { title, data } of results) map[title] = data;
      setCompareHistories(map);
    });
  }, [compare, range, mode]);

  useEffect(() => {
    if (!search.trim()) {
      setSearchResults([]);
      return;
    }
    const t = setTimeout(() => {
      fetch(`${API}/api/search?q=${encodeURIComponent(search.trim())}`)
        .then((r) => r.json())
        .then(setSearchResults);
    }, 300);
    return () => clearTimeout(t);
  }, [search]);

  const compareData = useMemo(() => {
    if (mode !== "compare" || compare.length === 0) return [];
    const dateSet = new Set<string>();
    const seriesData = compare.map((title) => {
      const rows = compareHistories[title] || [];
      rows.forEach((r) => dateSet.add(r.date));
      return { title, map: new Map(rows.map((r) => [r.date, r.mid])) };
    });
    return Array.from(dateSet)
      .sort()
      .map((d) => {
        const row: Record<string, string | number> = { date: d };
        for (const s of seriesData) {
          const v = s.map.get(d);
          if (v !== undefined) row[s.title] = v;
        }
        return row;
      });
  }, [mode, compare, compareHistories]);

  const stats = useMemo(() => {
    if (!activeHistory.length) return null;
    const last = activeHistory[activeHistory.length - 1];
    const prev = activeHistory[activeHistory.length - 2];
    const pct = prev ? ((last.mid - prev.mid) / prev.mid) * 100 : 0;
    return {
      last,
      pct,
      hi: Math.max(...activeHistory.map((r) => r.high)),
      lo: Math.min(...activeHistory.map((r) => r.low)),
      avg: activeHistory.reduce((s, r) => s + r.mid, 0) / activeHistory.length,
    };
  }, [activeHistory]);

  const selectedMeta = useMemo(() => {
    if (!selected) return null;
    for (const [group, cats] of Object.entries(tree))
      for (const [cat, titles] of Object.entries(cats))
        if (titles.includes(selected)) return { group, category: cat };
    return null;
  }, [selected, tree]);

  const toggleCompare = (title: string) =>
    setCompare((c) =>
      c.includes(title) ? c.filter((t) => t !== title) : c.length < 5 ? [...c, title] : c,
    );
  const toggleWatch = (title: string) =>
    setWatchlist((w) => (w.includes(title) ? w.filter((t) => t !== title) : [...w, title]));

  const pick = (title: string) => {
    if (mode === "compare") toggleCompare(title);
    else setSelected(title);
    setSidebarOpen(false);
  };

  type LatestRow = {
    group: string;
    category: string;
    title: string;
    date: string;
    low: number;
    mid: number;
    high: number;
    prev_mid: number | null;
    pct: number;
  };
  const [latest, setLatest] = useState<LatestRow[]>([]);
  const [catGroup, setCatGroup] = useState<string>("all");
  const [catWatch, setCatWatch] = useState(false);
  useEffect(() => {
    fetch(`${API}/api/latest`)
      .then((r) => r.json())
      .then((d) => Array.isArray(d) && setLatest(d))
      .catch(() => {});
  }, []);
  const catGroups = useMemo(() => {
    const seen: string[] = [];
    for (const r of latest) if (!seen.includes(r.group)) seen.push(r.group);
    return seen;
  }, [latest]);
  const catalog = useMemo(() => {
    let list = latest;
    if (catWatch) list = list.filter((r) => watchlist.includes(r.title));
    else if (catGroup !== "all") list = list.filter((r) => r.group === catGroup);
    const q = search.trim().toLowerCase();
    if (q)
      list = list.filter(
        (r) => r.title.toLowerCase().includes(q) || categoryLabel(r.category).includes(q),
      );
    return list;
  }, [latest, catGroup, catWatch, search, watchlist]);
  const catalogByGroup = useMemo(() => {
    const m: Record<string, typeof catalog> = {};
    for (const r of catalog) (m[r.group] ||= []).push(r);
    return m;
  }, [catalog]);

  return (
    <main className="mx-auto max-w-6xl p-4 sm:p-5 space-y-5">
      <div className="flex items-center justify-between flex-wrap gap-3">
        <div className="flex items-center gap-2 min-w-0">
          <div className="min-w-0">
            <div className="text-[11px] uppercase tracking-wider text-muted-foreground font-mono">
              {mode === "compare" ? "حالت مقایسه" : "محصول انتخاب‌شده"}
            </div>
            <h1 className="text-base sm:text-lg font-semibold mt-0.5 max-w-3xl truncate">
              {mode === "compare"
                ? `${compare.length} محصول انتخاب شده`
                : selected || "محصولی انتخاب نشده"}
            </h1>
            {mode === "single" && selectedMeta && (
              <div className="text-[11px] text-muted-foreground mt-0.5 font-mono">
                {groupLabel(selectedMeta.group)} › {categoryLabel(selectedMeta.category)}
              </div>
            )}
          </div>
        </div>
        <div className="flex items-center gap-2">
          <div className="flex items-center gap-1 bg-secondary/60 border border-border rounded-md p-0.5">
            <button
              onClick={() => setMode("single")}
              className={cn(
                "flex items-center gap-1.5 px-2.5 h-7 rounded text-xs font-medium transition",
                mode === "single"
                  ? "bg-primary text-primary-foreground"
                  : "text-muted-foreground hover:text-foreground",
              )}
            >
              <LineChartIcon className="w-3.5 h-3.5" /> تکی
            </button>
            <button
              onClick={() => setMode("compare")}
              className={cn(
                "flex items-center gap-1.5 px-2.5 h-7 rounded text-xs font-medium transition",
                mode === "compare"
                  ? "bg-primary text-primary-foreground"
                  : "text-muted-foreground hover:text-foreground",
              )}
            >
              <BarChart3 className="w-3.5 h-3.5" /> مقایسه ({compare.length})
            </button>
          </div>
        </div>
      </div>

      <div className="flex items-center gap-1 bg-secondary/60 border border-border rounded-md p-0.5 w-fit">
        {RANGES.map((r) => (
          <button
            key={String(r.value)}
            onClick={() => setRange(r.value)}
            className={cn(
              "px-2.5 sm:px-3 h-7 rounded text-xs font-medium",
              range === r.value
                ? "bg-primary text-primary-foreground"
                : "text-muted-foreground hover:text-foreground",
            )}
          >
            {r.label}
          </button>
        ))}
      </div>

      {/* Catalog — categorized by family, above the chart */}
      <section className="space-y-3">
        <div className="flex flex-wrap items-center justify-between gap-3">
          <div className="flex items-center gap-1.5 text-[11px] uppercase tracking-wider text-muted-foreground font-mono">
            <Factory className="w-3.5 h-3.5" /> کاتالوگ محصولات · {catalog.length}
          </div>
          <div className="relative">
            <Search className="w-4 h-4 absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
            <input
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              placeholder="جستجوی محصول…"
              className="h-10 w-full sm:w-72 rounded-lg border border-border bg-card pr-9 pl-3 text-sm outline-none focus:border-primary/60 focus:ring-2 focus:ring-primary/15"
            />
          </div>
        </div>
        <div className="flex flex-wrap gap-1.5">
          <CatChip
            active={catGroup === "all" && !catWatch}
            onClick={() => {
              setCatGroup("all");
              setCatWatch(false);
            }}
          >
            همه
          </CatChip>
          {watchlist.length > 0 && (
            <CatChip active={catWatch} onClick={() => setCatWatch((w) => !w)}>
              ★ واچ‌لیست
            </CatChip>
          )}
          {catGroups.map((k) => (
            <CatChip
              key={k}
              active={catGroup === k && !catWatch}
              onClick={() => {
                setCatGroup(k);
                setCatWatch(false);
              }}
            >
              {groupLabel(k)}
            </CatChip>
          ))}
        </div>
        {loadingTree && latest.length === 0 ? (
          <div className="flex items-center justify-center py-10 text-muted-foreground">
            <Loader2 className="w-5 h-5 animate-spin ml-2" /> در حال بارگذاری…
          </div>
        ) : catalog.length === 0 ? (
          <EmptyState text="موردی پیدا نشد." />
        ) : catGroup === "all" && !catWatch && !search.trim() ? (
          <div className="space-y-5">
            {catGroups.map((k) => (
              <div key={k} className="space-y-2">
                <div className="flex items-center gap-2 text-xs font-bold text-foreground">
                  <span className="h-4 w-1 rounded bg-primary" />
                  {groupLabel(k)}
                  <span className="text-[11px] font-mono font-normal text-muted-foreground">
                    {catalogByGroup[k]?.length ?? 0}
                  </span>
                </div>
                <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
                  {(catalogByGroup[k] || []).map((row, i) => (
                    <MetalCatalogCard
                      key={row.title}
                      row={row}
                      index={i}
                      active={
                        mode === "single" ? selected === row.title : compare.includes(row.title)
                      }
                      starred={watchlist.includes(row.title)}
                      onPick={() => pick(row.title)}
                      onStar={() => toggleWatch(row.title)}
                    />
                  ))}
                </div>
              </div>
            ))}
          </div>
        ) : (
          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
            {catalog.map((row, i) => (
              <MetalCatalogCard
                key={row.title}
                row={row}
                index={i}
                active={mode === "single" ? selected === row.title : compare.includes(row.title)}
                starred={watchlist.includes(row.title)}
                onPick={() => pick(row.title)}
                onStar={() => toggleWatch(row.title)}
              />
            ))}
          </div>
        )}
      </section>

      {mode === "single" &&
        selected &&
        (loadingHistory ? (
          <ChartSkeleton />
        ) : stats ? (
          <>
            <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
              <StatCard
                label="آخرین قیمت"
                value={formatNumber(stats.last.mid)}
                sub={toJalali(stats.last.date)}
                icon={<Activity className="w-3.5 h-3.5" />}
              />
              <StatCard
                label="تغییر نسبت به قبل"
                value={formatPct(stats.pct)}
                tone={stats.pct >= 0 ? "up" : "down"}
                icon={
                  stats.pct >= 0 ? (
                    <TrendingUp className="w-3.5 h-3.5" />
                  ) : (
                    <TrendingDown className="w-3.5 h-3.5" />
                  )
                }
              />
              <StatCard
                label="بازه (کف – سقف)"
                value={`${formatNumber(stats.lo)} – ${formatNumber(stats.hi)}`}
              />
              <StatCard label="میانگین بازه" value={formatNumber(stats.avg)} />
            </div>
            <div className="panel p-4">
              <div className="text-xs font-semibold text-muted-foreground font-mono mb-3">
                روند قیمت · میانگین با باند کف/سقف
              </div>
              <div className="h-[380px] sm:h-[420px]">
                <ResponsiveContainer width="100%" height="100%">
                  <ComposedChart
                    data={activeHistory.map((r) => ({ ...r, bandWidth: r.high - r.low }))}
                    margin={{ top: 10, right: 10, left: 10, bottom: 0 }}
                  >
                    <defs>
                      <linearGradient id="bandGrad" x1="0" y1="0" x2="0" y2="1">
                        <stop offset="0%" stopColor="var(--chart-1)" stopOpacity={0.25} />
                        <stop offset="100%" stopColor="var(--chart-1)" stopOpacity={0.02} />
                      </linearGradient>
                      <filter id="neonGlow" x="-50%" y="-50%" width="200%" height="200%">
                        <feGaussianBlur stdDeviation="4" result="blur" />
                        <feMerge>
                          <feMergeNode in="blur" />
                          <feMergeNode in="SourceGraphic" />
                        </feMerge>
                      </filter>
                    </defs>
                    <CartesianGrid stroke="var(--border)" strokeDasharray="3 3" vertical={false} />
                    <XAxis
                      dataKey="date"
                      stroke="var(--muted-foreground)"
                      fontSize={11}
                      tickMargin={8}
                      minTickGap={80}
                      tickFormatter={(v) => toJalali(v)}
                    />
                    <YAxis
                      stroke="var(--muted-foreground)"
                      fontSize={11}
                      domain={["auto", "auto"]}
                      tickFormatter={(v) => formatNumber(v as number)}
                      width={80}
                      orientation="right"
                    />
                    <Tooltip content={<ChartTooltip />} />
                    <Area
                      type="monotone"
                      dataKey="low"
                      stackId="band"
                      stroke="none"
                      fill="transparent"
                      activeDot={false}
                      isAnimationActive={false}
                    />
                    <Area
                      type="monotone"
                      dataKey="bandWidth"
                      stackId="band"
                      stroke="none"
                      fill="url(#bandGrad)"
                      activeDot={false}
                      isAnimationActive={false}
                    />
                    <Line
                      type="monotone"
                      dataKey="mid"
                      stroke="var(--chart-1)"
                      strokeWidth={2}
                      isAnimationActive={false}
                      dot={(props: any) => {
                        if (props.index !== activeHistory.length - 1)
                          return <g key={props.index} />;
                        const { cx, cy } = props;
                        return (
                          <g key="latest">
                            <circle cx={cx} cy={cy} r={12} fill="var(--primary)" opacity={0.15} />
                            <circle
                              cx={cx}
                              cy={cy}
                              r={4}
                              fill="var(--primary)"
                              filter="url(#neonGlow)"
                            />
                            <circle cx={cx} cy={cy} r={2} fill="#fff7e6" />
                          </g>
                        );
                      }}
                      activeDot={{ r: 4 }}
                    />
                  </ComposedChart>
                </ResponsiveContainer>
              </div>
            </div>
          </>
        ) : (
          <EmptyState text="داده‌ای برای این محصول موجود نیست." />
        ))}

      {mode === "compare" &&
        (compare.length === 0 ? (
          <EmptyState text="از سایدبار حداکثر ۵ محصول را برای مقایسه انتخاب کنید." />
        ) : (
          <>
            <div className="flex flex-wrap gap-2">
              {compare.map((t, i) => (
                <div key={t} className="flex items-center gap-2 panel-2 pr-2 pl-1 py-1 text-xs">
                  <span className="w-2.5 h-2.5 rounded-full" style={{ background: COLORS[i] }} />
                  <span className="max-w-[260px] truncate text-foreground">{t}</span>
                  <button
                    onClick={() => toggleCompare(t)}
                    className="hover:text-destructive"
                    aria-label="حذف"
                  >
                    <X className="w-3.5 h-3.5" />
                  </button>
                </div>
              ))}
            </div>
            <div className="panel p-4">
              <div className="text-xs font-semibold text-muted-foreground font-mono mb-3">
                مقایسه · میانگین قیمت
              </div>
              <div className="h-[420px] sm:h-[480px]">
                <ResponsiveContainer width="100%" height="100%">
                  <LineChart
                    data={compareData}
                    margin={{ top: 10, right: 10, left: 10, bottom: 0 }}
                  >
                    <CartesianGrid stroke="var(--border)" strokeDasharray="3 3" vertical={false} />
                    <XAxis
                      dataKey="date"
                      stroke="var(--muted-foreground)"
                      fontSize={11}
                      tickMargin={8}
                      minTickGap={80}
                      tickFormatter={(v) => toJalali(v)}
                    />
                    <YAxis
                      stroke="var(--muted-foreground)"
                      fontSize={11}
                      tickFormatter={(v) => formatNumber(v as number)}
                      width={80}
                      orientation="right"
                    />
                    <Tooltip content={<ChartTooltip />} />
                    <Legend wrapperStyle={{ fontSize: 11 }} />
                    {compare.map((t, i) => (
                      <Line
                        key={t}
                        type="monotone"
                        dataKey={t}
                        stroke={COLORS[i]}
                        strokeWidth={2}
                        dot={false}
                        connectNulls
                      />
                    ))}
                  </LineChart>
                </ResponsiveContainer>
              </div>
            </div>
          </>
        ))}
    </main>
  );
}

/* ─────────────────────────── Shared UI ─────────────────────────── */

function ViewHeader({
  live,
  title,
  subtitle,
  badge,
  lastUpdate,
}: {
  live: boolean;
  title: string;
  subtitle: string;
  badge: string;
  lastUpdate: string;
}) {
  return (
    <div className="flex items-end justify-between flex-wrap gap-3">
      <div>
        <div className="flex items-center gap-2">
          <span
            className={cn(
              "w-2.5 h-2.5 rounded-full animate-pulse",
              live ? "bg-emerald-500" : "bg-amber-500",
            )}
          />
          <h1 className="text-xl sm:text-2xl font-bold text-foreground">{title}</h1>
          <span className="text-[11px] font-mono px-2 py-0.5 rounded-full bg-secondary text-muted-foreground">
            {badge}
          </span>
        </div>
        <p className="text-sm text-muted-foreground mt-1">{subtitle}</p>
      </div>
      {lastUpdate && (
        <span className="text-[11px] text-muted-foreground font-mono">
          آخرین بروزرسانی: {lastUpdate}
        </span>
      )}
    </div>
  );
}

function Section({ title, children }: { title: string; children: ReactNode }) {
  return (
    <section>
      <h2 className="text-xs font-semibold text-muted-foreground mb-2.5 flex items-center gap-2">
        <span>{title}</span>
        <span className="h-px flex-1 bg-border" />
      </h2>
      {children}
    </section>
  );
}

function SkeletonGrid({ count }: { count: number }) {
  return (
    <div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-3">
      {Array.from({ length: count }).map((_, i) => (
        <div key={i} className="panel-2 h-[78px] animate-pulse" />
      ))}
    </div>
  );
}

function CatChip({
  active,
  onClick,
  children,
}: {
  active: boolean;
  onClick: () => void;
  children: ReactNode;
}) {
  return (
    <button
      onClick={onClick}
      className={cn(
        "rounded-full px-2.5 py-1 text-[11px] font-medium transition border",
        active
          ? "bg-primary text-primary-foreground border-primary"
          : "bg-card text-muted-foreground border-border hover:text-foreground hover:border-primary/40",
      )}
    >
      {children}
    </button>
  );
}

function MetalCatalogCard({
  row,
  active,
  starred,
  onPick,
  onStar,
  index,
}: {
  row: { title: string; category: string; mid: number; pct: number; date: string };
  active: boolean;
  starred: boolean;
  onPick: () => void;
  onStar: () => void;
  index?: number;
}) {
  const dir: "up" | "down" | "" = row.pct > 0 ? "up" : row.pct < 0 ? "down" : "";
  return (
    <div
      onClick={onPick}
      title={row.title}
      className={cn(
        "tile group relative cursor-pointer overflow-hidden px-4 py-3.5 flex flex-col",
        dir === "up" && "tile-up",
        dir === "down" && "tile-down",
        active && "ring-2 ring-primary/60",
        index != null && "rise-in",
      )}
      style={index != null ? { animationDelay: `${Math.min(index * 14, 300)}ms` } : undefined}
    >
      <button
        onClick={(e) => {
          e.stopPropagation();
          onStar();
        }}
        className="absolute top-3 left-3 opacity-60 hover:opacity-100 transition"
        aria-label={starred ? "حذف از واچ‌لیست" : "افزودن به واچ‌لیست"}
      >
        <Star
          className={cn(
            "w-4 h-4",
            starred ? "text-amber-500 fill-amber-500" : "text-muted-foreground/40",
          )}
        />
      </button>
      <div className="pl-7">
        <div className="text-sm font-semibold leading-snug text-foreground line-clamp-2">
          {row.title}
        </div>
        <div className="mt-0.5 text-[11px] text-muted-foreground">
          {categoryLabel(row.category)}
        </div>
      </div>
      <div className="mt-auto pt-3 flex items-end justify-between gap-2">
        <div>
          <div className="font-mono text-lg font-bold text-foreground tabular-nums">
            {formatNumber(row.mid)}
          </div>
          {row.date && (
            <div className="mt-0.5 text-[10px] text-muted-foreground font-mono">
              {toJalali(row.date)}
            </div>
          )}
        </div>
        <DeltaPill pct={row.pct} dir={dir} />
      </div>
    </div>
  );
}

function SidebarItem({
  title,
  active,
  starred,
  onPick,
  onStar,
}: {
  title: string;
  active: boolean;
  starred: boolean;
  onPick: () => void;
  onStar: () => void;
}) {
  return (
    <div
      className={cn(
        "group flex items-center gap-1 rounded border-r-2 transition",
        active ? "bg-primary/10 border-primary" : "border-transparent hover:bg-accent/60",
      )}
    >
      <button
        onClick={onPick}
        className={cn(
          "flex-1 text-right px-2 py-1.5 text-[12px] leading-snug truncate",
          active
            ? "text-foreground font-medium"
            : "text-muted-foreground group-hover:text-foreground",
        )}
      >
        {title}
      </button>
      <button
        onClick={onStar}
        className="shrink-0 px-1.5 py-1 opacity-60 hover:opacity-100"
        aria-label={starred ? "حذف از واچ‌لیست" : "افزودن به واچ‌لیست"}
      >
        <Star
          className={cn(
            "w-3.5 h-3.5",
            starred ? "text-amber-500 fill-amber-500" : "text-muted-foreground",
          )}
        />
      </button>
    </div>
  );
}

function StatCard({
  label,
  value,
  sub,
  tone,
  icon,
}: {
  label: string;
  value: string;
  sub?: string;
  tone?: "up" | "down";
  icon?: ReactNode;
}) {
  return (
    <div className="panel p-3.5">
      <div className="flex items-center justify-between text-[11px] uppercase tracking-wider text-muted-foreground font-mono">
        <span>{label}</span>
        {icon}
      </div>
      <div
        className={cn(
          "mt-2 font-mono font-semibold text-lg tabular-nums",
          tone === "up" && "text-emerald-700",
          tone === "down" && "text-red-600",
        )}
      >
        {value}
      </div>
      {sub && <div className="text-[11px] text-muted-foreground mt-1 font-mono">{sub}</div>}
    </div>
  );
}

function ChartSkeleton() {
  return (
    <div className="space-y-3">
      <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
        {Array.from({ length: 4 }).map((_, i) => (
          <div key={i} className="panel p-3.5 h-[84px] animate-pulse" />
        ))}
      </div>
      <div className="panel h-[420px] animate-pulse" />
    </div>
  );
}

function EmptyState({ text }: { text: string }) {
  return (
    <div className="panel-2 border-dashed p-10 text-center text-sm text-muted-foreground">
      {text}
    </div>
  );
}

function ChartTooltip({ active, payload, label }: any) {
  if (!active || !payload?.length) return null;
  return (
    <div className="bg-popover border border-border rounded-md shadow-lg p-2.5 text-xs">
      <div className="font-mono text-[11px] text-muted-foreground mb-1.5">{toJalali(label)}</div>
      {payload.map((p: any) => (
        <div key={p.dataKey} className="flex items-center gap-2 py-0.5">
          <span className="w-2 h-2 rounded-full" style={{ background: p.color || p.stroke }} />
          <span className="text-muted-foreground max-w-[240px] truncate">{p.dataKey}:</span>
          <span className="font-mono font-semibold mr-auto text-foreground">
            {formatNumber(p.value)}
          </span>
        </div>
      ))}
    </div>
  );
}
