import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { Check, X, Sparkles, TrendingUp, TrendingDown, Clock, Crown, Zap, Hourglass, ChevronLeft, ChevronRight, Search, Filter, Wifi, WifiOff, Loader2 } from "lucide-react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { backendApi, type TradeSignal } from "@/lib/backend-client";
import { fmtNum, fmtUsd } from "@/lib/mock-data";
import { useApprovalMode, useTradeThreshold } from "@/lib/trade-approval";
import { AlgoManager } from "@/components/algo-manager";
import { checkTradeLimits, recordTradeInitiated, isCoinAllowed, useRiskProfile } from "@/lib/user-prefs";
import { getCoinRisk, isRiskAllowed, RISK_META, type CoinRisk } from "@/lib/coin-risk";


const ALGO_META: Record<string, { label: string; tone: string }> = {
  confluence: { label: "Confluence", tone: "border-primary/40 text-primary bg-primary/10" },
  ema_cross: { label: "EMA Cross", tone: "border-sky-400/40 text-sky-300 bg-sky-500/10" },
  rsi_reversal: { label: "RSI Reversal", tone: "border-fuchsia-400/40 text-fuchsia-300 bg-fuchsia-500/10" },
  macd_momentum: { label: "MACD Momentum", tone: "border-amber-400/40 text-amber-300 bg-amber-500/10" },
  bb_reversion: { label: "BB Reversion", tone: "border-emerald-400/40 text-emerald-300 bg-emerald-500/10" },
  stochastic: { label: "Stochastic", tone: "border-violet-400/40 text-violet-300 bg-violet-500/10" },
};

function algoInfo(s: TradeSignal) {
  const key = s.algo ?? "confluence";
  const meta = ALGO_META[key] ?? { label: s.algo_name ?? key, tone: "border-border text-muted-foreground bg-muted/30" };
  return { key, ...meta };
}

export type PendingApprovalsConnectionState =
  | "idle"
  | "connecting"
  | "open"
  | "reconnecting"
  | "closed";

export function PendingApprovals({
  signals,
  onResolved,
  connection,
  activeCount = 0,
  openPositions = [],
}: {
  signals: TradeSignal[];
  onResolved: (id: string) => void;
  connection?: { state: PendingApprovalsConnectionState; attempt: number };
  activeCount?: number;
  openPositions?: Array<{ coin_symbol: string; position_side: "LONG" | "SHORT" }>;
}) {
  const [busy, setBusy] = useState<Record<string, "approve" | "reject" | undefined>>({});
  const [sizeOverrides, setSizeOverrides] = useState<Record<string, { type: "FIXED_AMOUNT" | "PERCENTAGE"; value: number }>>({});
  const [levOverrides, setLevOverrides] = useState<Record<string, number>>({});
  const [mode] = useApprovalMode();
  const [TRADE_THRESHOLD] = useTradeThreshold();
  const [riskProfile] = useRiskProfile();


  // Filters
  const [coinQuery, setCoinQuery] = useState("");
  const [algoFilter, setAlgoFilter] = useState<string>("all");
  const [sideFilter, setSideFilter] = useState<"all" | "LONG" | "SHORT">("all");
  const [statusFilter, setStatusFilter] = useState<"all" | "trade" | "wait">("all");
  const [sortBy, setSortBy] = useState<"confidence" | "coin" | "expires" | "side" | "algo">("confidence");
  const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");

  const availableAlgos = useMemo(() => {
    const set = new Set<string>();
    signals.forEach((s) => set.add(s.algo ?? "confluence"));
    return Array.from(set);
  }, [signals]);

  // Set of "COIN__SIDE" pairs that already have an open position — those
  // signals are suppressed so we never queue a duplicate trade on the same
  // trend that's already live in the book.
  const openSet = useMemo(() => {
    const s = new Set<string>();
    for (const p of openPositions) {
      if (p?.coin_symbol && p?.position_side) s.add(`${p.coin_symbol}__${p.position_side}`);
    }
    return s;
  }, [openPositions]);

  // Apply filters, then rank (default: confidence desc → strongest first).
  const filtered = useMemo(() => {
    const q = coinQuery.trim().toLowerCase();
    return signals.filter((s) => {
      if (openSet.has(`${s.coin_symbol}__${s.position_side}`)) return false;
      if (q && !s.coin_symbol.toLowerCase().includes(q)) return false;
      if (algoFilter !== "all" && (s.algo ?? "confluence") !== algoFilter) return false;
      if (sideFilter !== "all" && s.position_side !== sideFilter) return false;
      if (statusFilter !== "all") {
        const tradeable = (s.confidence ?? 0) >= TRADE_THRESHOLD;
        if (statusFilter === "trade" && !tradeable) return false;
        if (statusFilter === "wait" && tradeable) return false;
      }
      // Hide coins whose risk tier is disabled in Settings.
      if (!isRiskAllowed(s.coin_symbol)) return false;
      return true;
    });
  }, [signals, coinQuery, algoFilter, sideFilter, statusFilter, TRADE_THRESHOLD, openSet]);

  // Merge redundant signals: group by coin + side and surface a single combined
  // best trade per coin. The highest-confidence signal becomes the primary and
  // the other algos' verdicts are attached as "agreements" so the operator can
  // still see multi-algo confirmation at a glance.
  type Agreement = { algo: string; label: string; tone: string; confidence: number; tradeable: boolean };
  type MergedSignal = TradeSignal & { agreements: Agreement[]; agreeCount: number; avgConfidence: number };
  const merged: MergedSignal[] = useMemo(() => {
    const groups = new Map<string, TradeSignal[]>();
    for (const s of filtered) {
      const key = `${s.coin_symbol}__${s.position_side}`;
      const arr = groups.get(key) ?? [];
      arr.push(s);
      groups.set(key, arr);
    }
    const out: MergedSignal[] = [];
    for (const [, arr] of groups) {
      const sorted = [...arr].sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
      const primary = sorted[0];
      // Keep the strongest reading per algo so the consensus strip does not
      // repeat the same algorithm across multiple timeframes.
      const byAlgo = new Map<string, Agreement>();
      for (const x of sorted) {
        const info = algoInfo(x);
        const conf = x.confidence ?? 0;
        const prev = byAlgo.get(info.key);
        if (!prev || conf > prev.confidence) {
          byAlgo.set(info.key, {
            algo: info.key,
            label: info.label,
            tone: info.tone,
            confidence: conf,
            tradeable: conf >= TRADE_THRESHOLD,
          });
        }
      }
      const agreements: Agreement[] = Array.from(byAlgo.values()).sort((a, b) => b.confidence - a.confidence);
      const avg = agreements.reduce((n, a) => n + a.confidence, 0) / Math.max(1, agreements.length);
      out.push({ ...primary, agreements, agreeCount: agreements.length, avgConfidence: avg });
    }
    // Collapse to a single trade per coin — keep the side (LONG or SHORT)
    // with the strongest primary confidence. If a coin has both sides firing,
    // that's a conflict, so we pick the highest-confidence direction and
    // discard the opposite. Ties broken by average consensus.
    const bestPerCoin = new Map<string, MergedSignal>();
    for (const m of out) {
      const prev = bestPerCoin.get(m.coin_symbol);
      if (
        !prev ||
        (m.confidence ?? 0) > (prev.confidence ?? 0) ||
        ((m.confidence ?? 0) === (prev.confidence ?? 0) && m.avgConfidence > prev.avgConfidence)
      ) {
        bestPerCoin.set(m.coin_symbol, m);
      }
    }
    return Array.from(bestPerCoin.values());
  }, [filtered, TRADE_THRESHOLD]);

  const ranked = useMemo(() => {
    const arr = [...merged];
    arr.sort((a, b) => {
      switch (sortBy) {
        case "coin":
          return a.coin_symbol.localeCompare(b.coin_symbol);
        case "side":
          return String(a.position_side).localeCompare(String(b.position_side));
        case "algo":
          return String(a.algo ?? "").localeCompare(String(b.algo ?? ""));
        case "expires":
          return new Date(a.expires_at).getTime() - new Date(b.expires_at).getTime();
        case "confidence":
        default:
          return (a.confidence ?? 0) - (b.confidence ?? 0);
      }
    });
    return sortDir === "desc" ? arr.reverse() : arr;
  }, [merged, sortBy, sortDir]);
  const best = ranked[0];

  const activeFilterCount =
    (coinQuery ? 1 : 0) +
    (algoFilter !== "all" ? 1 : 0) +
    (sideFilter !== "all" ? 1 : 0) +
    (statusFilter !== "all" ? 1 : 0);
  const resetFilters = () => {
    setCoinQuery("");
    setAlgoFilter("all");
    setSideFilter("all");
    setStatusFilter("all");
  };

  // Pagination — keeps long signal lists scannable.
  const PAGE_SIZES = [5, 10, 20, 50] as const;
  const [pageSize, setPageSize] = useState<number>(5);
  const [page, setPage] = useState(1);
  const totalPages = Math.max(1, Math.ceil(ranked.length / pageSize));
  useEffect(() => { if (page > totalPages) setPage(1); }, [totalPages, page]);
  useEffect(() => { setPage(1); }, [coinQuery, algoFilter, sideFilter, statusFilter, sortBy, sortDir]);
  const pageStart = (page - 1) * pageSize;
  const pageItems = ranked.slice(pageStart, pageStart + pageSize);

  // Signals produced by the in-browser fallback engine carry a `local-` id
  // prefix; we resolve those without touching the backend so the panel works
  // in preview/demos even when the Node engine is offline. Backend adapters
  // sometimes hand us rows with `id` instead of `_id`, so read both.
  const idOf = (s: TradeSignal) =>
    (s?._id ?? (s as unknown as { id?: string })?.id ?? "") as string;
  const isLocal = (id: string) => typeof id === "string" && id.startsWith("local-");

  async function approve(s: TradeSignal) {
    const id = idOf(s);
    if (!id) {
      toast.error("Approval failed", { description: "Signal is missing an id" });
      return;
    }
    if (!isCoinAllowed(s.coin_symbol)) {
      toast.error("Coin not in your tradable list", {
        description: `${s.coin_symbol} is not selected in Settings → Tradable coins.`,
      });
      return;
    }
    if (!isRiskAllowed(s.coin_symbol)) {
      toast.error("Risk tier disabled", {
        description: `${s.coin_symbol} is classed as ${RISK_META[getCoinRisk(s.coin_symbol)].label} — enable that tier in Settings → Risk tiers.`,
      });
      return;
    }
    if (openSet.has(`${s.coin_symbol}__${s.position_side}`)) {
      toast.error("Already in a trade", {
        description: `${s.coin_symbol} ${s.position_side} position is already open.`,
      });
      return;
    }
    const limitReason = checkTradeLimits(activeCount);
    if (limitReason) {
      toast.error("Trade limit reached", { description: limitReason });
      return;
    }
    const override = sizeOverrides[id];
    const effectiveType = override?.type ?? riskProfile.sizingType;
    const effectiveValue = override?.value ?? riskProfile.sizingValue;
    const effectiveLev = levOverrides[id] ?? riskProfile.leverage;
    if (!Number.isFinite(effectiveValue) || effectiveValue <= 0) {
      toast.error("Invalid size", { description: "Size must be a positive number." });
      return;
    }
    setBusy((b) => ({ ...b, [id]: "approve" }));
    try {
      if (!isLocal(id)) {
        await backendApi.approveSignal(id, {
          order_sizing_type: effectiveType,
          order_size_value: effectiveValue,
          leverage: effectiveLev,
        });
      }
      recordTradeInitiated();
      const sizeLabel = effectiveType === "FIXED_AMOUNT" ? fmtUsd(effectiveValue) : `${effectiveValue}% eq`;
      toast.success(`Approved ${s.position_side} ${s.coin_symbol}`, {
        description: isLocal(id)
          ? `Entry ${fmtNum(s.entry_price, s.entry_price < 1 ? 4 : 2)} · ${sizeLabel} · Paper (backend offline)`
          : `Entry ${fmtNum(s.entry_price, s.entry_price < 1 ? 4 : 2)} · ${sizeLabel} · Order submitted`,
      });
      onResolved(id);
    } catch (e) {
      toast.error("Approval failed", { description: String(e instanceof Error ? e.message : e) });
    } finally {
      setBusy((b) => ({ ...b, [id]: undefined }));
    }
  }

  async function reject(s: TradeSignal) {
    const id = idOf(s);
    if (!id) {
      toast.error("Reject failed", { description: "Signal is missing an id" });
      return;
    }
    setBusy((b) => ({ ...b, [id]: "reject" }));
    try {
      if (!isLocal(id)) await backendApi.rejectSignal(id);
      toast(`Rejected ${s.coin_symbol}`);
      onResolved(id);
    } catch (e) {
      toast.error("Reject failed", { description: String(e instanceof Error ? e.message : e) });
    } finally {
      setBusy((b) => ({ ...b, [id]: undefined }));
    }
  }

  return (
    <Card className="glass p-0">
      <div className="flex flex-wrap items-center justify-between gap-2 border-b border-border/60 px-4 py-3">
        <div className="flex items-center gap-2">
          <Sparkles className="h-4 w-4 text-primary" aria-hidden />
          <h2 className="font-display text-sm font-bold tracking-tight">Pending Signals</h2>
          <Badge variant="outline" className="border-primary/40 text-primary">
            {ranked.length}
            {ranked.length !== signals.length && <span className="ml-1 text-muted-foreground">/ {signals.length}</span>}
          </Badge>
          {best && (
            <span className="ml-1 hidden items-center gap-1 rounded-md border border-warning/40 bg-warning/10 px-2 py-0.5 text-[10px] uppercase tracking-widest text-warning sm:inline-flex">
              <Crown className="h-3 w-3" aria-hidden />
              Best match · {best.coin_symbol} · {Math.round((best.confidence ?? 0) * 100)}%
            </span>
          )}
        </div>
        <div className="flex flex-wrap items-center gap-2">
          <ConnectionPill connection={connection} />
          <AlgoManager />
          <span className="text-[10px] uppercase tracking-widest text-muted-foreground">
            {mode === "auto" ? "Auto-approve on" : "Manual approval"} · Trade ≥ {Math.round(TRADE_THRESHOLD * 100)}%
          </span>
        </div>
      </div>

      {/* Filter + sort toolbar */}
      {signals.length > 0 && (
        <div className="flex flex-wrap items-center gap-2 border-b border-border/60 bg-background/30 px-4 py-2">
          <div className="relative">
            <Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" aria-hidden />
            <Input
              value={coinQuery}
              onChange={(e) => setCoinQuery(e.target.value)}
              placeholder="Coin…"
              aria-label="Filter by coin"
              className="h-8 w-[130px] pl-7 text-xs"
            />
          </div>
          <Select value={algoFilter} onValueChange={setAlgoFilter}>
            <SelectTrigger className="h-8 w-[140px] text-xs" aria-label="Filter by algorithm">
              <SelectValue placeholder="Algorithm" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">All algorithms</SelectItem>
              {availableAlgos.map((k) => (
                <SelectItem key={k} value={k}>{ALGO_META[k]?.label ?? k}</SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Select value={sideFilter} onValueChange={(v) => setSideFilter(v as "all" | "LONG" | "SHORT")}>
            <SelectTrigger className="h-8 w-[100px] text-xs" aria-label="Filter by side">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">All sides</SelectItem>
              <SelectItem value="LONG">Long</SelectItem>
              <SelectItem value="SHORT">Short</SelectItem>
            </SelectContent>
          </Select>
          <Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as "all" | "trade" | "wait")}>
            <SelectTrigger className="h-8 w-[110px] text-xs" aria-label="Filter by status">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">All status</SelectItem>
              <SelectItem value="trade">Trade</SelectItem>
              <SelectItem value="wait">Wait</SelectItem>
            </SelectContent>
          </Select>
          <div className="ml-auto flex items-center gap-1">
            <span className="text-[10px] uppercase tracking-widest text-muted-foreground">Sort</span>
            <Select value={sortBy} onValueChange={(v) => setSortBy(v as typeof sortBy)}>
              <SelectTrigger className="h-8 w-[140px] text-xs" aria-label="Sort by">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="confidence">Confidence</SelectItem>
                <SelectItem value="coin">Coin</SelectItem>
                <SelectItem value="side">Side</SelectItem>
                <SelectItem value="algo">Algorithm</SelectItem>
                <SelectItem value="expires">Expires</SelectItem>
              </SelectContent>
            </Select>
            <Button
              size="sm"
              variant="outline"
              className="h-8 px-2 text-xs"
              onClick={() => setSortDir((d) => (d === "asc" ? "desc" : "asc"))}
              title={sortDir === "desc" ? "Descending" : "Ascending"}
              aria-label={`Sort direction: ${sortDir === "desc" ? "descending" : "ascending"}`}
            >
              {sortDir === "desc" ? "↓" : "↑"}
            </Button>
            {activeFilterCount > 0 && (
              <Button
                size="sm"
                variant="ghost"
                className="h-8 px-2 text-xs"
                onClick={resetFilters}
                title="Clear filters"
              >
                <Filter className="mr-1 h-3 w-3" aria-hidden />
                Clear ({activeFilterCount})
              </Button>
            )}
          </div>
        </div>
      )}

      {ranked.length === 0 ? (
        <div className="px-4 py-10 text-center text-xs text-muted-foreground">
          {signals.length === 0
            ? "No pending signals — waiting for the strategy engine."
            : "No signals match the current filters."}
        </div>
      ) : (
        <ul className="divide-y divide-border/60">
          {pageItems.map((s, idx) => {
            const sid = idOf(s);
            const conf = Math.round((s.confidence ?? 0) * 100);
            const tradeable = (s.confidence ?? 0) >= TRADE_THRESHOLD;
            const algo = algoInfo(s);
            const isBest = page === 1 && idx === 0 && ranked.length > 1 && sortBy === "confidence" && sortDir === "desc";
            return (
              <li
                key={sid || `${s.coin_symbol}-${idx}`}
                className={`relative p-4 transition ${isBest ? "bg-warning/5 ring-1 ring-inset ring-warning/20" : ""}`}
              >
                {isBest && (
                  <span
                    aria-hidden
                    className="pointer-events-none absolute inset-y-0 left-0 w-0.5 bg-gradient-to-b from-warning via-warning/60 to-transparent"
                  />
                )}
                <div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start">
                  <div className="min-w-0">
                    <div className="flex flex-wrap items-center gap-1.5">
                      {isBest && (
                        <Badge className="gap-1 bg-warning/20 text-warning hover:bg-warning/30" title="Highest-confidence pending signal">
                          <Crown className="h-3 w-3" aria-hidden /> Best
                        </Badge>
                      )}
                      <span className="font-display text-base font-bold">{s.coin_symbol}</span>
                      <RiskFlag symbol={s.coin_symbol} />
                      <Badge
                        variant="outline"
                        className={
                          s.position_side === "LONG"
                            ? "border-bull/40 text-bull"
                            : "border-bear/40 text-bear"
                        }
                      >
                        {s.position_side === "LONG" ? (
                          <TrendingUp className="mr-1 h-3 w-3" aria-hidden />
                        ) : (
                          <TrendingDown className="mr-1 h-3 w-3" aria-hidden />
                        )}
                        {s.position_side}
                      </Badge>
                      <Badge variant="outline" className={algo.tone} title={`Best match: ${algo.label} · ${conf}%`}>
                        {algo.label} · {conf}%
                      </Badge>
                      {s.agreements && s.agreements.length > 1 && (
                        <Badge variant="outline" className="border-primary/30 bg-primary/5 text-primary" title="Number of algorithms firing on this coin/side">
                          {s.agreements.length}× algos agree
                        </Badge>
                      )}
                      {s.timeframe && (
                        <Badge variant="outline" className="border-border text-muted-foreground">
                          {s.timeframe}
                        </Badge>
                      )}
                      <StatusPill tradeable={tradeable} />
                    </div>
                    <div className="mt-2 flex items-center gap-3">
                      <ConfidenceMeter pct={conf} tradeable={tradeable} threshold={TRADE_THRESHOLD} />
                      <span
                        className={`num text-sm font-bold ${tradeable ? "text-bull" : "text-warning"}`}
                        aria-label={`Confidence ${conf} percent`}
                      >
                        {conf}%
                      </span>
                    </div>
                    <p className="mt-2 max-w-xl text-xs leading-relaxed text-muted-foreground">{s.rationale}</p>
                    {s.agreements && s.agreements.length > 1 && (
                      <div className="mt-2 flex flex-wrap items-center gap-1.5" aria-label="Algorithm agreements">
                        <span className="text-[9px] uppercase tracking-widest text-muted-foreground">Algo consensus</span>
                        {s.agreements.map((a, i) => (
                          <span
                            key={`${a.algo}-${i}`}
                            className={`inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[10px] ${a.tone}`}
                            title={`${a.label}: ${Math.round(a.confidence * 100)}% · ${a.tradeable ? "Trade" : "Wait"}`}
                          >
                            {a.label}
                            <span className="num font-semibold">{Math.round(a.confidence * 100)}%</span>
                          </span>
                        ))}
                        <span className="ml-1 text-[10px] text-muted-foreground">avg {Math.round(s.avgConfidence * 100)}%</span>
                      </div>
                    )}
                  </div>
                  <div className="grid grid-cols-2 gap-2 sm:flex sm:shrink-0 sm:flex-col sm:items-stretch">
                    <Button
                      size="sm"
                      onClick={() => approve(s)}
                      disabled={!!busy[sid]}
                      className={
                        tradeable
                          ? "min-h-9 gap-1 bg-bull font-semibold text-primary-foreground shadow-sm shadow-bull/20 hover:bg-bull/90 sm:min-w-[9rem]"
                          : "min-h-9 gap-1 bg-warning font-semibold text-primary-foreground shadow-sm shadow-warning/20 hover:bg-warning/90 sm:min-w-[9rem]"
                      }
                      title={tradeable ? "Confidence at or above trade threshold" : "Below trade threshold — approve to override"}
                    >
                      <Check className="h-3.5 w-3.5" aria-hidden />
                      {busy[sid] === "approve" ? "Placing…" : tradeable ? "Trade" : "Approve anyway"}
                    </Button>
                    <Button
                      size="sm"
                      variant="outline"
                      onClick={() => reject(s)}
                      disabled={!!busy[sid]}
                      className="min-h-9 gap-1 border-bear/40 text-bear hover:bg-bear-soft sm:min-w-[9rem]"
                    >
                      <X className="h-3.5 w-3.5" aria-hidden /> Reject
                    </Button>
                  </div>
                </div>
                <div className="mt-3 grid grid-cols-2 gap-2 text-xs sm:grid-cols-5">
                  <Kv label="Entry" value={fmtNum(s.entry_price, s.entry_price < 1 ? 4 : 2)} />
                  <Kv label="Stop" value={fmtNum(s.stop_loss, s.stop_loss < 1 ? 4 : 2)} tone="bear" />
                  <Kv label="Target" value={fmtNum(s.take_profit, s.take_profit < 1 ? 4 : 2)} tone="bull" />
                  <SizeEditor
                    signalId={sid}
                    initialType={riskProfile.sizingType}
                    initialValue={riskProfile.sizingValue}
                    override={sizeOverrides[sid]}
                    onChange={(next) => setSizeOverrides((m) => ({ ...m, [sid]: next }))}
                    disabled={!!busy[sid]}
                  />
                  <LeverageEditor
                    signalId={sid}
                    initialValue={riskProfile.leverage}
                    override={levOverrides[sid]}
                    onChange={(n) => setLevOverrides((m) => ({ ...m, [sid]: n }))}
                    disabled={!!busy[sid]}
                  />
                </div>
                <div className="mt-2 flex flex-wrap items-center gap-3 text-[10px] uppercase tracking-widest text-muted-foreground">
                  {s.indicators?.rsi != null && <span>RSI {Number(s.indicators.rsi).toFixed(0)}</span>}
                  {s.indicators?.macd && <span>MACD {s.indicators.macd}</span>}
                  {s.indicators?.ema && <span>EMA {s.indicators.ema}</span>}
                  <span className="inline-flex items-center gap-1">
                    <Clock className="h-3 w-3" aria-hidden /> expires{" "}
                    {new Date(s.expires_at).toLocaleTimeString([], {
                      hour: "2-digit",
                      minute: "2-digit",
                    })}
                  </span>
                </div>
              </li>
            );
          })}
        </ul>
      )}
      {ranked.length > 0 && (
        <div className="flex flex-wrap items-center justify-between gap-2 border-t border-border/60 px-4 py-2 text-xs text-muted-foreground">
          <div className="flex items-center gap-2">
            <span>Rows</span>
            <Select value={String(pageSize)} onValueChange={(v) => { setPageSize(Number(v)); setPage(1); }}>
              <SelectTrigger className="h-7 w-[70px]"><SelectValue /></SelectTrigger>
              <SelectContent>
                {PAGE_SIZES.map((n) => (
                  <SelectItem key={n} value={String(n)}>{n}</SelectItem>
                ))}
              </SelectContent>
            </Select>
            <span>
              {pageStart + 1}-{Math.min(pageStart + pageSize, ranked.length)} of {ranked.length}
            </span>
          </div>
          <div className="flex items-center gap-1">
            <Button size="sm" variant="outline" className="h-7 px-2" disabled={page <= 1} onClick={() => setPage((p) => Math.max(1, p - 1))}>
              <ChevronLeft className="h-3.5 w-3.5" aria-hidden />
            </Button>
            <span className="px-1">Page {page} / {totalPages}</span>
            <Button size="sm" variant="outline" className="h-7 px-2" disabled={page >= totalPages} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>
              <ChevronRight className="h-3.5 w-3.5" aria-hidden />
            </Button>
          </div>
        </div>
      )}
    </Card>
  );
}

function RiskFlag({ symbol }: { symbol: string }) {
  const risk: CoinRisk = getCoinRisk(symbol);
  const meta = RISK_META[risk];
  return (
    <span
      className={`inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-widest ${meta.tone}`}
      title={`${meta.label} — ${meta.description}`}
      aria-label={`Risk tier: ${meta.label}`}
    >
      <span className={`h-1.5 w-1.5 rounded-full ${meta.dot}`} aria-hidden />
      {meta.label}
    </span>
  );
}

function StatusPill({ tradeable }: { tradeable: boolean }) {
  return tradeable ? (
    <span className="inline-flex items-center gap-1 rounded-md border border-bull/50 bg-bull/10 px-2 py-0.5 text-[10px] font-bold uppercase tracking-widest text-bull">
      <Zap className="h-3 w-3" aria-hidden /> Trade
    </span>
  ) : (
    <span className="inline-flex items-center gap-1 rounded-md border border-warning/50 bg-warning/10 px-2 py-0.5 text-[10px] font-bold uppercase tracking-widest text-warning">
      <Hourglass className="h-3 w-3" aria-hidden /> Wait
    </span>
  );
}

function ConfidenceMeter({ pct, tradeable, threshold = 0.6 }: { pct: number; tradeable: boolean; threshold?: number }) {
  const TRADE_THRESHOLD = threshold;

  const clamped = Math.max(0, Math.min(100, pct));
  return (
    <div
      className="relative h-1.5 w-40 overflow-hidden rounded-full bg-muted/40"
      role="progressbar"
      aria-valuenow={clamped}
      aria-valuemin={0}
      aria-valuemax={100}
      aria-label="Signal confidence"
    >
      <div
        className={`h-full rounded-full transition-[width] ${tradeable ? "bg-bull" : "bg-warning"}`}
        style={{ width: `${clamped}%` }}
      />
      {/* Trade-threshold marker */}
      <div
        className="absolute inset-y-0 w-px bg-foreground/40"
        style={{ left: `${TRADE_THRESHOLD * 100}%` }}
        title={`Trade threshold ${Math.round(TRADE_THRESHOLD * 100)}%`}
        aria-hidden
      />
    </div>
  );
}

function SizeEditor({
  signalId,
  initialType,
  initialValue,
  override,
  onChange,
  disabled,
}: {
  signalId: string;
  initialType: "FIXED_AMOUNT" | "PERCENTAGE";
  initialValue: number;
  override?: { type: "FIXED_AMOUNT" | "PERCENTAGE"; value: number };
  onChange: (next: { type: "FIXED_AMOUNT" | "PERCENTAGE"; value: number }) => void;
  disabled?: boolean;
}) {
  const type = override?.type ?? initialType;
  const value = override?.value ?? initialValue;
  const isPct = type === "PERCENTAGE";
  const inputId = `size-${signalId}`;
  return (
    <div className="rounded-md border border-border bg-background/30 px-2 py-1.5">
      <div className="flex items-center justify-between gap-1">
        <label htmlFor={inputId} className="text-[9px] uppercase tracking-widest text-muted-foreground">
          Size
        </label>
        <div className="inline-flex overflow-hidden rounded border border-border/60" role="group" aria-label="Sizing type">
          <button
            type="button"
            disabled={disabled}
            aria-pressed={!isPct}
            onClick={() => onChange({ type: "FIXED_AMOUNT", value })}
            className={`px-1 text-[9px] uppercase tracking-widest transition ${!isPct ? "bg-primary/20 text-primary" : "text-muted-foreground hover:text-foreground"}`}
            title="Fixed USD amount"
          >
            $
          </button>
          <button
            type="button"
            disabled={disabled}
            aria-pressed={isPct}
            onClick={() => onChange({ type: "PERCENTAGE", value })}
            className={`px-1 text-[9px] uppercase tracking-widest transition ${isPct ? "bg-primary/20 text-primary" : "text-muted-foreground hover:text-foreground"}`}
            title="Percent of equity"
          >
            %
          </button>
        </div>
      </div>
      <div className="mt-0.5 flex items-baseline gap-1">
        <input
          id={inputId}
          type="number"
          inputMode="decimal"
          min={0}
          step={isPct ? 0.5 : 1}
          value={Number.isFinite(value) ? value : ""}
          disabled={disabled}
          onChange={(e) => {
            const v = Number(e.target.value);
            onChange({ type, value: Number.isFinite(v) ? v : 0 });
          }}
          aria-label={isPct ? "Percent of equity" : "Fixed amount in USD"}
          className="num h-6 w-full min-w-0 rounded bg-transparent px-1 text-xs font-semibold text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
        />
        <span className="text-[10px] text-muted-foreground">{isPct ? "% eq" : "USD"}</span>
      </div>
    </div>
  );
}

function LeverageEditor({
  signalId,
  initialValue,
  override,
  onChange,
  disabled,
}: {
  signalId: string;
  initialValue: number;
  override?: number;
  onChange: (n: number) => void;
  disabled?: boolean;
}) {
  const value = override ?? initialValue;
  const inputId = `lev-${signalId}`;
  return (
    <div className="rounded-md border border-border bg-background/30 px-2 py-1.5">
      <label htmlFor={inputId} className="block text-[9px] uppercase tracking-widest text-muted-foreground">
        Leverage
      </label>
      <div className="mt-0.5 flex items-baseline gap-1">
        <input
          id={inputId}
          type="number"
          inputMode="numeric"
          min={1}
          max={125}
          step={1}
          value={Number.isFinite(value) ? value : ""}
          disabled={disabled}
          onChange={(e) => {
            const v = Number(e.target.value);
            onChange(Number.isFinite(v) && v > 0 ? v : 1);
          }}
          aria-label="Leverage multiplier"
          className="num h-6 w-full min-w-0 rounded bg-transparent px-1 text-xs font-semibold text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
        />
        <span className="text-[10px] text-muted-foreground">x</span>
      </div>
    </div>
  );
}

function Kv({ label, value, tone }: { label: string; value: string; tone?: "bull" | "bear" }) {
  const c = tone === "bull" ? "text-bull" : tone === "bear" ? "text-bear" : "text-foreground";
  return (
    <div className="rounded-md border border-border bg-background/30 px-2 py-1.5">
      <div className="text-[9px] uppercase tracking-widest text-muted-foreground">{label}</div>
      <div className={`num text-xs font-semibold ${c}`}>{value}</div>
    </div>
  );
}

function ConnectionPill({
  connection,
}: {
  connection?: { state: PendingApprovalsConnectionState; attempt: number };
}) {
  const state = connection?.state ?? "idle";
  const attempt = connection?.attempt ?? 0;
  let icon = <Loader2 className="h-3 w-3 animate-spin" aria-hidden />;
  let label = "Idle";
  let cls = "border-border/60 text-muted-foreground bg-muted/30";
  if (state === "open") {
    icon = <Wifi className="h-3 w-3" aria-hidden />;
    label = "Connected";
    cls = "border-bull/40 text-bull bg-bull/10";
  } else if (state === "connecting") {
    icon = <Loader2 className="h-3 w-3 animate-spin" aria-hidden />;
    label = "Connecting…";
    cls = "border-warning/40 text-warning bg-warning/10";
  } else if (state === "reconnecting") {
    icon = <Loader2 className="h-3 w-3 animate-spin" aria-hidden />;
    label = attempt > 0 ? `Reconnecting · #${attempt}` : "Reconnecting…";
    cls = "border-warning/40 text-warning bg-warning/10";
  } else if (state === "closed") {
    icon = <WifiOff className="h-3 w-3" aria-hidden />;
    label = "Disconnected";
    cls = "border-bear/40 text-bear bg-bear/10";
  }
  return (
    <span
      role="status"
      aria-live="polite"
      title={`Backend WebSocket · ${label}`}
      className={`inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[10px] uppercase tracking-widest ${cls}`}
    >
      {icon}
      {label}
    </span>
  );
}
