import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import CandlestickChart from "../components/CandlestickChart";
import CompanyIntelPanel from "../components/CompanyIntelPanel";
import FlowPanel from "../components/FlowPanel";
import AiSummaryCard from "../components/AiSummaryCard";
import FundAnalysisPanel from "../components/FundAnalysisPanel";
import { TermHint } from "../components/TermHint";
import EquityChart from "../components/EquityChart";
import OscillatorChart from "../components/OscillatorChart";
import { AppIcon } from "../components/PixelIcons";
import { Card, Tag, Loading, RefreshButton } from "../components/ui";
import { setFreshQueryData } from "../lib/refresh";
import {
  api,
  type BacktestPayload,
  type FinCheck,
  type FinQuarter,
  type PatternSignal,
  type WatchlistGroup,
  type WatchlistPayload,
  type WatchlistQuote,
} from "../lib/api";
import {
  analyzeGranville,
  analyzeGuppy,
  buildChartMarkers,
  pickDisplaySignals,
} from "../lib/patternRules";
import { computeAllIndicators } from "../lib/technicalIndicators";
import {
  buildTradePlan,
  describeVolumeRatio,
  priceLevelClass,
  projectPatternSignalToNow,
  projectSignalToNow,
  scanTradeSignals,
  signalsToChartMarkers,
  tradeLevelLabels,
  type TradeSignal,
} from "../lib/tradeSignals";
import { fmtMoneyShort, fmtNum, fmtPct, priceStats, technicalStats } from "../lib/stockTechnical";

const SYM_RE = /^[A-Z0-9.\-]{1,12}$/;

const MA_OPTIONS = [
  { period: 5, color: "#f0c040", label: "MA5" },
  { period: 10, color: "#ff9f5a", label: "MA10" },
  { period: 20, color: "#6fe0d0", label: "MA20" },
  { period: 60, color: "#c97bff", label: "MA60" },
  { period: 120, color: "#7eb8ff", label: "MA120" },
  { period: 200, color: "#8898bc", label: "MA200" },
] as const;

import {
  CHART_INTERVAL_OPTS,
  chartRangeLabel,
  normalizeChartRange,
  rangesForInterval,
  type ChartInterval,
  type ChartRange,
} from "../lib/chartIntervals";

type DetailTab = "chart" | "backtest" | "financials" | "flow" | "company";

function fmtShares(n: number | null | undefined) {
  if (n == null || Number.isNaN(n)) return "—";
  const a = Math.abs(n);
  const s = n < 0 ? "-" : "+";
  if (a >= 1e6) return `${s}${(a / 1e6).toFixed(2)}M`;
  if (a >= 1e3) return `${s}${(a / 1e3).toFixed(1)}K`;
  return `${s}${a.toLocaleString()}`;
}

function fmtMoneyTable(v: number | null | undefined) {
  if (v == null || Number.isNaN(v)) return "—";
  const a = Math.abs(v);
  if (a >= 1e9) return `$${(v / 1e9).toFixed(2)}B`;
  if (a >= 1e6) return `$${(v / 1e6).toFixed(1)}M`;
  return `$${v.toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
}

function verdictTone(c?: string): "up" | "down" | "gold" {
  if (c === "good") return "up";
  if (c === "bad") return "down";
  return "gold";
}

function FinRow({ check }: { check: FinCheck }) {
  return (
    <div className={`fin-row fin-${check.status}`}>
      <span className={`fin-light ${check.status}`} aria-hidden />
      <div className="fin-body">
        <div className="fin-label">
          <TermHint term={check.label} />
        </div>
        <div className="fin-value">{check.value}</div>
        {check.note ? <div className="fin-note">{check.note}</div> : null}
      </div>
    </div>
  );
}

function FinTable({ rows, cols }: { rows: FinQuarter[]; cols: { key: keyof FinQuarter; label: string; fmt?: (v: number | null | undefined) => string }[] }) {
  if (!rows.length) return <p className="muted">尚無資料</p>;
  return (
    <div className="fin-table-wrap fund-table-scroll">
      <table className={`fin-table${cols.length >= 5 ? " fund-table-wide" : ""}`}>
        <thead>
          <tr>
            <th>期間</th>
            {cols.map((c) => (
              <th key={String(c.key)}>
                <TermHint term={c.label} />
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map((r) => (
            <tr key={r.label || r.end}>
              <td className="fin-period">{r.label || r.end}</td>
              {cols.map((c) => {
                const v = r[c.key];
                const text =
                  typeof v === "number"
                    ? c.fmt
                      ? c.fmt(v)
                      : fmtNum(v, 2)
                    : v == null
                      ? "—"
                      : String(v);
                return <td key={String(c.key)}>{text}</td>;
              })}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default function Research() {
  const qc = useQueryClient();
  const [searchParams, setSearchParams] = useSearchParams();
  const [activeGroupId, setActiveGroupId] = useState<string | null>(null);
  const [selected, setSelected] = useState(() => searchParams.get("sym")?.toUpperCase() || "");
  const [addInput, setAddInput] = useState("");
  const [detailTab, setDetailTab] = useState<DetailTab>("chart");
  const [interval, setInterval] = useState<ChartInterval>("1d");
  const [range, setRange] = useState<ChartRange>("2y");
  const [msg, setMsg] = useState("");
  const [scanOn, setScanOn] = useState(false);
  const [showGuppy, setShowGuppy] = useState(false);
  const [maOn, setMaOn] = useState<Record<number, boolean>>({ 5: true, 20: true, 60: true });
  const [indOn, setIndOn] = useState({ boll: true, rsi: false, macd: false, kdj: false });
  const [showTechMarkers, setShowTechMarkers] = useState(true);
  const [selectedPat, setSelectedPat] = useState<{ date: string; patternId?: string } | null>(null);
  const [selectedTechSig, setSelectedTechSig] = useState<{ date: string; ruleId: string } | null>(null);
  const [btRange, setBtRange] = useState("5y");
  const [btStrategyId, setBtStrategyId] = useState("");
  const [btParams, setBtParams] = useState<Record<string, string>>({});
  const [btResult, setBtResult] = useState<BacktestPayload | null>(null);
  const [btRunning, setBtRunning] = useState(false);
  const [importJson, setImportJson] = useState("");
  const [pendingPatternBt, setPendingPatternBt] = useState<string | null>(null);
  const [symbolFilter, setSymbolFilter] = useState("");
  const [inventoryView, setInventoryView] = useState<"grid" | "list" | null>(null);
  const symRefs = useRef<Record<string, HTMLDivElement | null>>({});

  const wlQ = useQuery({ queryKey: ["watchlist"], queryFn: api.watchlist, staleTime: 30_000 });

  const groups = wlQ.data?.groups ?? [];
  const activeGroup = groups.find((g) => g.id === activeGroupId) || groups[0] || null;

  useEffect(() => {
    if (activeGroup && !activeGroupId) setActiveGroupId(activeGroup.id);
  }, [activeGroup, activeGroupId]);

  const symbols = activeGroup?.symbols ?? [];
  const inventoryLayout = inventoryView ?? (symbols.length > 12 ? "list" : "grid");

  const quotesQ = useQuery({
    queryKey: ["watchlist-quotes", symbols.join(",")],
    queryFn: () => api.watchlistQuotes(symbols),
    enabled: symbols.length > 0,
    staleTime: 45_000,
    refetchInterval: 60_000,
  });

  const quoteMap = useMemo(() => {
    const m: Record<string, WatchlistQuote> = {};
    for (const q of quotesQ.data?.quotes ?? []) m[q.symbol] = q;
    return m;
  }, [quotesQ.data]);

  const filteredSymbols = useMemo(() => {
    const q = symbolFilter.trim().toUpperCase();
    if (!q) return symbols;
    return symbols.filter((sym) => {
      const name = quoteMap[sym]?.name?.toUpperCase() || "";
      return sym.includes(q) || name.includes(q);
    });
  }, [symbols, symbolFilter, quoteMap]);

  useEffect(() => {
    setSymbolFilter("");
  }, [activeGroup?.id]);

  useEffect(() => {
    if (!selected) return;
    const el = symRefs.current[selected];
    el?.scrollIntoView({ block: "nearest", behavior: "smooth" });
  }, [selected, filteredSymbols, inventoryLayout]);

  const symbol = selected.trim().toUpperCase();
  const symOk = SYM_RE.test(symbol);

  useEffect(() => {
    if (!symOk) return;
    const next = new URLSearchParams(searchParams);
    if (next.get("sym") !== symbol) {
      next.set("sym", symbol);
      setSearchParams(next, { replace: true });
    }
  }, [symbol, symOk, searchParams, setSearchParams]);

  const quoteQ = useQuery({ queryKey: ["quote", symbol], queryFn: () => api.quote(symbol), enabled: symOk, staleTime: 60_000 });
  const fundQ = useQuery({ queryKey: ["fundamentals", symbol], queryFn: () => api.fundamentals(symbol), enabled: symOk, staleTime: 300_000 });
  const priceQ = useQuery({
    queryKey: ["price", symbol, interval, range],
    queryFn: () => api.price(symbol, range, interval),
    enabled: symOk && detailTab === "chart",
    staleTime: interval === "5m" ? 5 * 60_000 : interval === "15m" ? 10 * 60_000 : interval === "1h" ? 15 * 60_000 : 120_000,
  });
  const scanQ = useQuery({
    queryKey: ["pattern-scan", symbol, range],
    queryFn: () => api.patternScan(symbol, range),
    enabled: symOk && detailTab === "chart" && scanOn && interval === "1d",
    staleTime: 180_000,
  });
  const catalogQ = useQuery({ queryKey: ["pattern-catalog"], queryFn: api.patternCatalog, staleTime: 3600_000 });
  const strategiesQ = useQuery({
    queryKey: ["strategies"],
    queryFn: api.strategies,
    staleTime: 300_000,
  });
  const flowQ = useQuery({ queryKey: ["flow", symbol], queryFn: () => api.institutionalFlow(symbol, 12), enabled: symOk && detailTab === "flow", staleTime: 300_000 });
  const holdersQ = useQuery({ queryKey: ["holders", symbol], queryFn: () => api.stockHolders(symbol), enabled: symOk && detailTab === "flow", staleTime: 300_000 });
  const notableQ = useQuery({ queryKey: ["notable-holders", symbol], queryFn: () => api.notableHolders(symbol), enabled: symOk && detailTab === "flow", staleTime: 300_000 });
  const queryClient = useQueryClient();
  const [chipRefreshing, setChipRefreshing] = useState(false);
  const [quotesRefreshing, setQuotesRefreshing] = useState(false);
  const [quoteRefreshing, setQuoteRefreshing] = useState(false);
  const [priceRefreshing, setPriceRefreshing] = useState(false);
  const [fundRefreshing, setFundRefreshing] = useState(false);
  const [intelRefreshing, setIntelRefreshing] = useState(false);
  const [scanRefreshing, setScanRefreshing] = useState(false);

  const refreshQuotes = useCallback(async () => {
    if (!symbols.length) return;
    setQuotesRefreshing(true);
    try {
      await setFreshQueryData(
        queryClient,
        ["watchlist-quotes", symbols.join(",")],
        () => api.watchlistQuotes(symbols, true),
      );
    } finally {
      setQuotesRefreshing(false);
    }
  }, [queryClient, symbols]);

  const refreshQuote = useCallback(async () => {
    if (!symOk) return;
    setQuoteRefreshing(true);
    try {
      await setFreshQueryData(queryClient, ["quote", symbol], () => api.quote(symbol, true));
    } finally {
      setQuoteRefreshing(false);
    }
  }, [queryClient, symOk, symbol]);

  const refreshPrice = useCallback(async () => {
    if (!symOk) return;
    setPriceRefreshing(true);
    try {
      await setFreshQueryData(queryClient, ["price", symbol, interval, range], () => api.price(symbol, range, interval, true));
      if (scanOn && interval === "1d") {
        await setFreshQueryData(
          queryClient,
          ["pattern-scan", symbol, range],
          () => api.patternScan(symbol, range, 120, true),
        );
      }
    } finally {
      setPriceRefreshing(false);
    }
  }, [queryClient, symOk, symbol, interval, range, scanOn]);

  const refreshPatternScan = useCallback(async () => {
    if (!symOk) return;
    setScanRefreshing(true);
    try {
      setScanOn(true);
      await setFreshQueryData(
        queryClient,
        ["pattern-scan", symbol, range],
        () => api.patternScan(symbol, range, 120, true),
      );
    } finally {
      setScanRefreshing(false);
    }
  }, [queryClient, symOk, symbol, range]);

  const refreshFundamentals = useCallback(async () => {
    if (!symOk) return;
    setFundRefreshing(true);
    try {
      await setFreshQueryData(queryClient, ["fundamentals", symbol], () => api.fundamentals(symbol, true));
    } finally {
      setFundRefreshing(false);
    }
  }, [queryClient, symOk, symbol]);

  const refreshIntel = useCallback(async () => {
    if (!symOk) return;
    setIntelRefreshing(true);
    try {
      await setFreshQueryData(queryClient, ["intel", symbol], () => api.companyIntel(symbol, { fresh: true }));
      const holders = await api.stockHolders(symbol, true);
      queryClient.setQueryData(["holders", symbol, "company"], holders);
    } finally {
      setIntelRefreshing(false);
    }
  }, [queryClient, symOk, symbol]);

  const refreshChipData = useCallback(async () => {
    if (!symOk) return;
    setChipRefreshing(true);
    try {
      const [holders, notable, flow] = await Promise.all([
        api.stockHolders(symbol, true),
        api.notableHolders(symbol, true),
        api.institutionalFlow(symbol, 12, true),
      ]);
      queryClient.setQueryData(["holders", symbol], holders);
      queryClient.setQueryData(["notable-holders", symbol], notable);
      queryClient.setQueryData(["flow", symbol], flow);
    } finally {
      setChipRefreshing(false);
    }
  }, [symOk, symbol, queryClient]);
  const intelQ = useQuery({
    queryKey: ["intel", symbol],
    queryFn: () => api.companyIntel(symbol),
    enabled: symOk && (detailTab === "company" || detailTab === "chart" || detailTab === "financials"),
    staleTime: 300_000,
  });
  const secArchiveQ = useQuery({
    queryKey: ["sec-archive", symbol],
    queryFn: () => api.secArchive(symbol),
    enabled: symOk && detailTab === "company",
    staleTime: 600_000,
  });
  const companyHoldersQ = useQuery({
    queryKey: ["holders", symbol, "company"],
    queryFn: () => api.stockHolders(symbol),
    enabled: symOk && detailTab === "company",
    staleTime: 300_000,
  });

  const intelSyncMut = useMutation({
    mutationFn: (force?: boolean) => api.syncCompanyIntel(symbol, !!force),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["intel", symbol] });
    },
  });
  const secSyncMut = useMutation({
    mutationFn: (sync?: boolean) => (sync ? api.syncSecArchive(symbol, true) : api.secArchive(symbol)),
    onSuccess: (data) => {
      qc.setQueryData(["sec-archive", symbol], data);
    },
  });

  const points = priceQ.data?.points ?? [];
  const ohlc = useMemo(() => {
    const byDate = new Map<string, { date: string; open: number; high: number; low: number; close: number; volume?: number }>();
    for (const p of points) {
      if (p.close == null || Number.isNaN(p.close)) continue;
      const c = p.close;
      const o = p.open ?? c;
      const h = p.high ?? Math.max(o, c);
      const l = p.low ?? Math.min(o, c);
      byDate.set(p.date, { date: p.date, open: o, high: h, low: l, close: c, volume: p.volume });
    }
    return [...byDate.values()].sort((a, b) => a.date.localeCompare(b.date));
  }, [points]);
  const rangeOpts = useMemo(() => rangesForInterval(interval), [interval]);
  const onIntervalChange = useCallback((next: ChartInterval) => {
    setInterval(next);
    setRange((r) => normalizeChartRange(next, r));
  }, []);
  const tech = useMemo(() => technicalStats(points, quoteQ.data ?? {}), [points, quoteQ.data]);
  const hist = useMemo(() => priceStats(points), [points]);
  const report = fundQ.data?.report;

  const activeMas = useMemo(
    () => MA_OPTIONS.filter((m) => maOn[m.period]).map((m) => ({ period: m.period, color: m.color, label: m.label })),
    [maOn],
  );
  const indicators = useMemo(
    () => (ohlc.length >= 30 ? computeAllIndicators(ohlc, MA_OPTIONS.map((m) => m.period)) : null),
    [ohlc],
  );
  const techSignals = useMemo(() => (ohlc.length >= 30 ? scanTradeSignals(ohlc) : []), [ohlc]);
  const tradePlan = useMemo(
    () => (ohlc.length >= 30 ? buildTradePlan(ohlc, techSignals) : null),
    [ohlc, techSignals],
  );
  const selectedTechSignal = useMemo(() => {
    if (!techSignals.length) return null;
    if (selectedTechSig) {
      return techSignals.find((s) => s.date === selectedTechSig.date && s.ruleId === selectedTechSig.ruleId) || null;
    }
    return techSignals[0] ?? null;
  }, [techSignals, selectedTechSig]);

  const projectedTech = useMemo(
    () => (selectedTechSignal && ohlc.length >= 30 ? projectSignalToNow(selectedTechSignal, ohlc) : null),
    [selectedTechSignal, ohlc],
  );

  const displaySignals = useMemo(() => pickDisplaySignals(scanQ.data), [scanQ.data]);
  const selectedSignal = useMemo(() => {
    if (!displaySignals.length) return null;
    if (selectedPat) {
      return (
        displaySignals.find(
          (s) => s.date === selectedPat.date && (s.patternId || s.id) === selectedPat.patternId,
        ) || null
      );
    }
    return displaySignals[0] ?? null;
  }, [displaySignals, selectedPat]);

  const projectedPattern = useMemo(
    () => (selectedSignal && ohlc.length >= 30 ? projectPatternSignalToNow(selectedSignal, ohlc) : null),
    [selectedSignal, ohlc],
  );

  useEffect(() => {
    setScanOn(false);
    setSelectedPat(null);
    setSelectedTechSig(null);
  }, [symbol]);

  useEffect(() => {
    if (!scanOn || !displaySignals.length || selectedPat) return;
    const s = displaySignals[0];
    setSelectedPat({ date: s.date, patternId: s.patternId || s.id });
  }, [scanOn, displaySignals, selectedPat]);

  const patternMarkers = useMemo(
    () =>
      scanOn
        ? buildChartMarkers(
            displaySignals,
            selectedPat ||
              (selectedSignal && !selectedTechSig
                ? { date: selectedSignal.date, patternId: selectedSignal.patternId || selectedSignal.id }
                : null),
          )
        : [],
    [scanOn, displaySignals, selectedPat, selectedSignal, selectedTechSig],
  );
  const techMarkers = useMemo(
    () =>
      showTechMarkers
        ? signalsToChartMarkers(techSignals, selectedTechSig || (selectedTechSignal && !selectedPat ? { date: selectedTechSignal.date, ruleId: selectedTechSignal.ruleId } : null))
        : [],
    [showTechMarkers, techSignals, selectedTechSig, selectedTechSignal, selectedPat],
  );
  const chartMarkers = useMemo(() => {
    if (interval !== "1d") return [];
    const seen = new Set<string>();
    const out = [...patternMarkers, ...techMarkers].filter((m) => {
      const k = `${m.date}-${m.type}-${m.label}`;
      if (seen.has(k)) return false;
      seen.add(k);
      return true;
    });
    return out.slice(0, 48);
  }, [interval, patternMarkers, techMarkers]);

  const granville = useMemo(() => (ohlc.length >= 25 ? analyzeGranville(ohlc) : null), [ohlc]);
  const guppy = useMemo(() => (ohlc.length >= 65 ? analyzeGuppy(ohlc) : null), [ohlc]);

  const catalogPat = useMemo(() => {
    if (!selectedSignal?.patternId || !catalogQ.data?.patterns) return null;
    return catalogQ.data.patterns.find((p) => p.id === selectedSignal.patternId) || null;
  }, [selectedSignal, catalogQ.data]);

  const selectPattern = (s: PatternSignal) => {
    setSelectedPat({ date: s.date, patternId: s.patternId || s.id });
    setSelectedTechSig(null);
  };

  const selectTechSignal = (s: TradeSignal) => {
    setSelectedTechSig({ date: s.date, ruleId: s.ruleId });
    setSelectedPat(null);
  };

  const toggleMa = (period: number) => setMaOn((prev) => ({ ...prev, [period]: !prev[period] }));
  const toggleInd = (key: keyof typeof indOn) => setIndOn((prev) => ({ ...prev, [key]: !prev[key] }));

  const patTone = (s: PatternSignal): "up" | "down" | "gold" => {
    if (s.action === "buy" || s.bias === "bullish") return "up";
    if (s.action === "sell" || s.bias === "bearish") return "down";
    return "gold";
  };

  const strategies = strategiesQ.data?.strategies ?? [];
  const activeBtStrategy = useMemo(
    () => strategies.find((s) => s.id === btStrategyId) || strategies[0] || null,
    [strategies, btStrategyId],
  );

  useEffect(() => {
    if (!strategies.length) return;
    if (!btStrategyId || !strategies.some((s) => s.id === btStrategyId)) {
      setBtStrategyId(strategies[0].id);
    }
  }, [strategies, btStrategyId]);

  useEffect(() => {
    if (!activeBtStrategy) return;
    const next: Record<string, string> = {};
    for (const p of activeBtStrategy.paramDefs || []) {
      next[p.key] = String(activeBtStrategy.params?.[p.key] ?? p.def);
    }
    setBtParams(next);
  }, [activeBtStrategy?.id]);

  useEffect(() => {
    if (!pendingPatternBt || !strategies.length) return;
    const hit = strategies.find((s) => s.patternId === pendingPatternBt || s.slug === pendingPatternBt);
    if (hit) {
      setBtStrategyId(hit.id);
      setDetailTab("backtest");
    }
    setPendingPatternBt(null);
  }, [pendingPatternBt, strategies]);

  const runBacktest = async () => {
    if (!symOk || !activeBtStrategy) return;
    setBtRunning(true);
    try {
      const params: Record<string, number> = {};
      for (const [k, v] of Object.entries(btParams)) {
        if (v !== "") params[k] = Number(v);
      }
      const res = await api.backtest(symbol, { strategyId: activeBtStrategy.id, range: btRange, params });
      setBtResult(res);
    } catch (e: any) {
      setMsg(e?.message || "回測失敗");
    } finally {
      setBtRunning(false);
    }
  };

  const importStrategy = async () => {
    try {
      const raw = JSON.parse(importJson.trim());
      if (!raw.name || !raw.engine) throw new Error("需含 name 與 engine");
      await api.createStrategy({
        name: raw.name,
        engine: raw.engine,
        params: raw.params || {},
        description: raw.description,
        formula: raw.formula,
        source: raw.source || "custom",
        tags: raw.tags,
        patternId: raw.patternId,
        bookRef: raw.bookRef,
        bookPage: raw.bookPage,
      });
      setImportJson("");
      setMsg(`已加入策略：${raw.name}`);
      void qc.invalidateQueries({ queryKey: ["strategies"] });
    } catch (e: any) {
      setMsg(e?.message || "匯入失敗");
    }
  };

  const fmtMoney = (v: number | null | undefined) =>
    v == null || Number.isNaN(v) ? "—" : `$${Math.round(v).toLocaleString()}`;

  const saveMut = useMutation({
    mutationFn: (payload: WatchlistPayload) => api.saveWatchlist(payload),
    onSuccess: (d) => {
      qc.setQueryData(["watchlist"], d);
      void qc.invalidateQueries({ queryKey: ["calendar"] });
      setMsg("已儲存");
      setTimeout(() => setMsg(""), 2000);
    },
    onError: () => setMsg("儲存失敗"),
  });

  const persist = useCallback(
    (next: WatchlistPayload) => {
      saveMut.mutate(next);
    },
    [saveMut],
  );

  const updateGroups = (fn: (gs: WatchlistGroup[]) => WatchlistGroup[]) => {
    if (!wlQ.data) return;
    persist({ ...wlQ.data, groups: fn([...wlQ.data.groups]) });
  };

  const addGroup = () => {
    const name = window.prompt("新分群名稱", "新分群");
    if (!name?.trim()) return;
    const id = `g_${Date.now().toString(36)}`;
    updateGroups((gs) => [...gs, { id, name: name.trim().slice(0, 40), symbols: [], order: gs.length }]);
    setActiveGroupId(id);
  };

  const renameGroup = () => {
    if (!activeGroup) return;
    const name = window.prompt("分群名稱", activeGroup.name);
    if (!name?.trim()) return;
    updateGroups((gs) => gs.map((g) => (g.id === activeGroup.id ? { ...g, name: name.trim().slice(0, 40) } : g)));
  };

  const deleteGroup = () => {
    if (!activeGroup || groups.length <= 1) {
      setMsg("至少保留一個分群");
      return;
    }
    if (!window.confirm(`刪除「${activeGroup.name}」？`)) return;
    updateGroups((gs) => gs.filter((g) => g.id !== activeGroup.id));
    setActiveGroupId(groups.find((g) => g.id !== activeGroup.id)?.id || null);
  };

  const addSymbol = (e: React.FormEvent) => {
    e.preventDefault();
    const sym = addInput.trim().toUpperCase();
    if (!SYM_RE.test(sym)) {
      setMsg("代號格式不正確");
      return;
    }
    if (!activeGroup) return;
    const all = new Set(groups.flatMap((g) => g.symbols));
    if (all.has(sym)) {
      setMsg(`${sym} 已在其他分群`);
      return;
    }
    updateGroups((gs) =>
      gs.map((g) => (g.id === activeGroup.id ? { ...g, symbols: [...g.symbols, sym] } : g)),
    );
    setAddInput("");
    setSelected(sym);
    setMsg(`${sym} 已加入`);
  };

  const removeSymbol = (sym: string) => {
    if (!activeGroup) return;
    updateGroups((gs) =>
      gs.map((g) => (g.id === activeGroup.id ? { ...g, symbols: g.symbols.filter((s) => s !== sym) } : g)),
    );
    if (selected === sym) setSelected("");
  };

  const aiBase = { symbol, subPage: detailTab, label: symbol ? `${symbol} · 背包` : "個股背包" };
  const DETAIL_TABS: { key: DetailTab; label: string }[] = [
    { key: "chart", label: "走勢" },
    { key: "backtest", label: "回測" },
    { key: "financials", label: "財報" },
    { key: "flow", label: "籌碼" },
    { key: "company", label: "公司" },
  ];

  return (
    <>
      <div className="page-head">
        <div className="eyebrow">追蹤清單 · 分群管理</div>
        <h1 className="page-title-row">
          <AppIcon name="folder" size={38} framed glow variant="hero" />
          個股背包
        </h1>
        <p>
          左側選背包分群，上方格子點選個股；下方詳情區固定不動（像楓之谷開背包）。標的多時可搜尋、切換列表模式，格子區獨立捲動。
        </p>
      </div>

      {msg ? <div className="backpack-msg">{msg}</div> : null}

      <div className="backpack-layout">
        <aside className="backpack-groups" aria-label="背包分群">
          <div className="backpack-groups-head">
            <b>分群</b>
            <button type="button" className="btn-primary sm" onClick={addGroup}>
              ＋
            </button>
          </div>
          {wlQ.isLoading ? (
            <Loading label="載入分群…" />
          ) : (
            <ul className="backpack-group-list">
              {groups.map((g) => (
                <li key={g.id}>
                  <button
                    type="button"
                    className={`backpack-group-btn ${g.id === activeGroup?.id ? "active" : ""}`}
                    onClick={() => setActiveGroupId(g.id)}
                  >
                    <span>{g.name}</span>
                    <span className="backpack-group-count">{g.symbols.length}</span>
                  </button>
                </li>
              ))}
            </ul>
          )}
          {activeGroup ? (
            <div className="backpack-group-actions">
              <button type="button" className="btn-ghost sm" onClick={renameGroup}>
                重新命名
              </button>
              {groups.length > 1 ? (
                <button type="button" className="btn-ghost sm danger" onClick={deleteGroup}>
                  刪除分群
                </button>
              ) : null}
            </div>
          ) : null}
        </aside>

        <main className="backpack-main">
          <div className="backpack-inventory-zone" aria-label="背包格子">
            <div className="backpack-main-head">
              <div>
                <h2>{activeGroup?.name || "我的追蹤"}</h2>
                <span className="backpack-meta">
                  {symbols.length} 檔
                  {symbolFilter && filteredSymbols.length !== symbols.length ? ` · 篩選 ${filteredSymbols.length}` : ""}
                  {wlQ.data?.updatedAt
                    ? ` · 更新 ${new Date(wlQ.data.updatedAt).toLocaleString("zh-TW", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" })}`
                    : ""}
                </span>
              </div>
              {symbols.length ? (
                <RefreshButton onClick={() => void refreshQuotes()} loading={quotesRefreshing} label="重新整理報價" />
              ) : null}
            </div>

            <form className="backpack-add" onSubmit={addSymbol}>
              <input
                value={addInput}
                onChange={(e) => setAddInput(e.target.value.toUpperCase())}
                placeholder="加入代號（NVDA、2330）"
                maxLength={12}
                spellCheck={false}
              />
              <button type="submit" className="btn-primary">
                加入
              </button>
            </form>

            {symbols.length > 6 ? (
              <div className="backpack-inventory-toolbar">
                <input
                  className="backpack-filter"
                  value={symbolFilter}
                  onChange={(e) => setSymbolFilter(e.target.value)}
                  placeholder="搜尋代號或名稱…"
                  spellCheck={false}
                />
                <div className="backpack-view-toggle">
                  <button
                    type="button"
                    className={inventoryLayout === "grid" ? "active" : ""}
                    onClick={() => setInventoryView("grid")}
                    title="格狀"
                  >
                    格狀
                  </button>
                  <button
                    type="button"
                    className={inventoryLayout === "list" ? "active" : ""}
                    onClick={() => setInventoryView("list")}
                    title="列表（標的多時較好找）"
                  >
                    列表
                  </button>
                </div>
              </div>
            ) : null}

            <div
              className={`backpack-symbols backpack-symbols-scroll${inventoryLayout === "list" ? " backpack-symbols--list" : ""}`}
            >
              {symbols.length ? (
                filteredSymbols.length ? (
                  filteredSymbols.map((sym) => {
                    const q = quoteMap[sym];
                    const chg = q?.changePercent;
                    const on = sym === selected;
                    if (inventoryLayout === "list") {
                      return (
                        <div
                          key={sym}
                          ref={(el) => {
                            symRefs.current[sym] = el;
                          }}
                          className={`backpack-sym-row ${on ? "active" : ""}`}
                        >
                          <button type="button" className="backpack-sym-row-open" onClick={() => setSelected(sym)}>
                            <span className="backpack-sym-ticker">{sym}</span>
                            <span className="backpack-sym-name">{q?.name && q.name !== sym ? q.name : ""}</span>
                            <span className="backpack-sym-price">{q?.price != null ? `$${fmtNum(q.price)}` : "…"}</span>
                            <span className={`backpack-sym-chg ${chg == null ? "" : chg >= 0 ? "up" : "down"}`}>
                              {chg != null ? fmtPct(chg) : "—"}
                            </span>
                          </button>
                          <button
                            type="button"
                            className="backpack-sym-rm"
                            onClick={() => removeSymbol(sym)}
                            aria-label={`移除 ${sym}`}
                          >
                            ×
                          </button>
                        </div>
                      );
                    }
                    return (
                      <div
                        key={sym}
                        ref={(el) => {
                          symRefs.current[sym] = el;
                        }}
                        className={`backpack-sym-card ${on ? "active" : ""}`}
                      >
                        <button type="button" className="backpack-sym-open" onClick={() => setSelected(sym)}>
                          <span className="backpack-sym-ticker">{sym}</span>
                          <span className="backpack-sym-name">{q?.name && q.name !== sym ? q.name : ""}</span>
                          <span className="backpack-sym-price">{q?.price != null ? `$${fmtNum(q.price)}` : "…"}</span>
                          <span className={`backpack-sym-chg ${chg == null ? "" : chg >= 0 ? "up" : "down"}`}>
                            {chg != null ? fmtPct(chg) : "—"}
                          </span>
                        </button>
                        <button
                          type="button"
                          className="backpack-sym-rm"
                          onClick={() => removeSymbol(sym)}
                          aria-label={`移除 ${sym}`}
                        >
                          ×
                        </button>
                      </div>
                    );
                  })
                ) : (
                  <div className="backpack-empty">沒有符合「{symbolFilter}」的標的。</div>
                )
              ) : (
                <div className="backpack-empty">此分群尚無標的。輸入代號加入，美股如 NVDA、台股如 2330。</div>
              )}
            </div>
            {symbols.length > 24 ? (
              <p className="backpack-inventory-hint small muted">
                此分群已有 {symbols.length} 檔，建議拆成多個背包分群，或用搜尋快速定位。
              </p>
            ) : null}
          </div>

          <div className="backpack-detail-zone">
          {symOk ? (
            <section className="backpack-detail">
              <header className="backpack-detail-head">
                <div>
                  <div className="backpack-detail-sym">{symbol}</div>
                  <div className="backpack-detail-name">{fundQ.data?.name || quoteQ.data?.name || ""}</div>
                </div>
                <div className="backpack-detail-quote">
                  <span className="backpack-detail-price">
                    {quoteQ.data?.price != null ? `$${fmtNum(quoteQ.data.price)}` : "—"}
                  </span>
                  <span className={quoteQ.data?.changePercent != null && quoteQ.data.changePercent >= 0 ? "up" : "down"}>
                    {quoteQ.data?.changePercent != null ? fmtPct(quoteQ.data.changePercent) : ""}
                  </span>
                  {report ? (
                    <Tag tone={verdictTone(report.summary.verdictColor)}>{report.summary.verdict}</Tag>
                  ) : null}
                  <RefreshButton onClick={() => void refreshQuote()} loading={quoteRefreshing} />
                </div>
              </header>

              <nav className="backpack-detail-tabs">
                {DETAIL_TABS.map((t) => (
                  <button
                    key={t.key}
                    type="button"
                    className={`backpack-tab ${detailTab === t.key ? "active" : ""}`}
                    onClick={() => setDetailTab(t.key)}
                  >
                    {t.label}
                  </button>
                ))}
              </nav>

              {detailTab === "chart" ? (
                <div className="backpack-panel">
                  <div className="chart-scan-toolbar">
                    <RefreshButton onClick={() => void refreshPrice()} loading={priceRefreshing} label="重新整理 K 線" />
                    <RefreshButton
                      onClick={() => void refreshPatternScan()}
                      loading={scanRefreshing || scanQ.isFetching}
                      label={scanOn ? "重新掃描線型" : "線型掃描"}
                    />
                    <button
                      type="button"
                      className={`btn-ghost ${showTechMarkers ? "active" : ""}`}
                      onClick={() => setShowTechMarkers((v) => !v)}
                    >
                      {showTechMarkers ? "買賣點 ON" : "買賣點"}
                    </button>
                    <button
                      type="button"
                      className={`btn-ghost ${showGuppy ? "active" : ""}`}
                      onClick={() => setShowGuppy((v) => !v)}
                    >
                      {showGuppy ? "顧比 ON" : "顧比"}
                    </button>
                    <span className="small muted">
                      技術訊號 {techSignals.length} 筆
                      {scanQ.data ? ` · 線型 ${scanQ.data.signalCount}` : ""}
                    </span>
                  </div>

                  <div className="chart-ind-toolbar">
                    <div className="chart-ind-group">
                      <span className="chart-ind-label">均線</span>
                      {MA_OPTIONS.map((m) => (
                        <button
                          key={m.period}
                          type="button"
                          className={`ind-chip ${maOn[m.period] ? "on" : ""}`}
                          style={{ "--chip-color": m.color } as CSSProperties}
                          onClick={() => toggleMa(m.period)}
                        >
                          {m.label}
                        </button>
                      ))}
                    </div>
                    <div className="chart-ind-group">
                      <span className="chart-ind-label">指標</span>
                      {(
                        [
                          ["boll", "布林"],
                          ["rsi", "RSI"],
                          ["macd", "MACD"],
                          ["kdj", "KD"],
                        ] as const
                      ).map(([k, label]) => (
                        <button
                          key={k}
                          type="button"
                          className={`ind-chip ${indOn[k] ? "on" : ""}`}
                          onClick={() => toggleInd(k)}
                        >
                          {label}
                        </button>
                      ))}
                    </div>
                  </div>

                  <div className="chart-pattern-layout">
                    <Card
                      title="K 線走勢"
                      ico={<AppIcon name="chart" size={22} framed variant="hero" />}
                      className="chart-main-card"
                      ai
                      aiFocus={{ ...aiBase, cardTitle: "K 線走勢" }}
                      onRefresh={() => void refreshPrice()}
                      refreshing={priceRefreshing}
                      refreshLabel="重新整理 K 線"
                    >
                      <div className="range-chips-wrap">
                        <div className="small muted" style={{ marginBottom: 6 }}>
                          K 線週期（OHLC）
                        </div>
                        <div className="range-chips">
                          {CHART_INTERVAL_OPTS.map((o) => (
                            <button
                              key={o.key}
                              type="button"
                              className={`range-chip ${interval === o.key ? "active" : ""}`}
                              onClick={() => onIntervalChange(o.key)}
                            >
                              {o.label}
                            </button>
                          ))}
                        </div>
                        <div className="small muted" style={{ margin: "10px 0 6px" }}>
                          回看區間
                        </div>
                        <div className="range-chips">
                          {rangeOpts.map((r) => (
                            <button
                              key={r}
                              type="button"
                              className={`range-chip ${range === r ? "active" : ""}`}
                              onClick={() => setRange(r)}
                            >
                              {chartRangeLabel(r)}
                            </button>
                          ))}
                        </div>
                        <span className="range-chips-hint small muted">
                          {ohlc.length ? `${ohlc.length} 根K` : ""}
                          {priceQ.data?.interval ? ` · ${CHART_INTERVAL_OPTS.find((o) => o.key === interval)?.label || interval}` : ""}
                          {priceQ.data?.source ? ` · ${priceQ.data.source}` : ""}
                          {interval !== "1d" ? " · 盤中資料來自 Yahoo（免費，約數分鐘更新）" : ""}
                        </span>
                      </div>
                      {priceQ.isLoading ? (
                        <Loading />
                      ) : ohlc.length ? (
                        <>
                          <CandlestickChart
                            data={ohlc}
                            mas={activeMas}
                            showGuppy={showGuppy}
                            showBollinger={indOn.boll}
                            bollinger={indicators?.boll}
                            markers={chartMarkers}
                            heightClass="candle-chart candle-chart-tall"
                          />
                          {indOn.rsi && indicators ? (
                            <OscillatorChart
                              title="RSI(14)"
                              lines={[{ name: "RSI", color: "#6fe0d0", points: indicators.rsi }]}
                              refLines={[
                                { value: 30, color: "#3dd68c", label: "30" },
                                { value: 70, color: "#ff6b6b", label: "70" },
                              ]}
                            />
                          ) : null}
                          {indOn.macd && indicators ? (
                            <OscillatorChart
                              title="MACD"
                              lines={[
                                { name: "MACD", color: "#4da3ff", points: indicators.macd.macd },
                                { name: "訊號", color: "#f0a040", points: indicators.macd.signal },
                              ]}
                              hist={indicators.macd.hist}
                            />
                          ) : null}
                          {indOn.kdj && indicators ? (
                            <OscillatorChart
                              title="KD(9,3,3)"
                              lines={[
                                { name: "K", color: "#f0c040", points: indicators.kdj.k },
                                { name: "D", color: "#6fe0d0", points: indicators.kdj.d },
                                { name: "J", color: "#c97bff", points: indicators.kdj.j },
                              ]}
                              refLines={[
                                { value: 20, color: "#3dd68c", label: "20" },
                                { value: 80, color: "#ff6b6b", label: "80" },
                              ]}
                            />
                          ) : null}
                        </>
                      ) : (
                        <p className="muted">無 K 線資料</p>
                      )}
                    </Card>

                    <aside className="pattern-side">
                      {tradePlan ? (
                        <div className="pattern-side-block trade-plan-box">
                          <h4>綜合進退場建議</h4>
                          <p className="small muted" style={{ margin: "0 0 8px" }}>
                            以下價位皆以<b>今日收盤 {fmtNum(tradePlan.currentPrice)}</b>重算；歷史訊號價不會直接套用。
                          </p>
                          <div className="trade-plan-head">
                            <Tag tone={tradePlan.bias === "bullish" ? "up" : tradePlan.bias === "bearish" ? "down" : "gold"}>
                              {tradePlan.actionLabel}
                            </Tag>
                          </div>
                          {(() => {
                            const lv = tradeLevelLabels(tradePlan.action);
                            const px = tradePlan.currentPrice;
                            return (
                              <div className="pat-levels pat-levels--stacked">
                                <div className="pat-level-row">
                                  <span>{lv.entry}</span>
                                  <b className={priceLevelClass(tradePlan.entry, px)}>{fmtNum(tradePlan.entry)}</b>
                                  <em className="pat-level-note">{tradePlan.entryNote}</em>
                                </div>
                                <div className="pat-level-row">
                                  <span>{lv.stop}</span>
                                  <b className={priceLevelClass(tradePlan.stop, px)}>{fmtNum(tradePlan.stop)}</b>
                                  <em className="pat-level-note">{tradePlan.stopNote}</em>
                                </div>
                                <div className="pat-level-row">
                                  <span>{lv.target}</span>
                                  <b className={priceLevelClass(tradePlan.target, px)}>{fmtNum(tradePlan.target)}</b>
                                  <em className="pat-level-note">{tradePlan.targetNote}</em>
                                </div>
                                <div className="pat-level-row">
                                  <span>量能比</span>
                                  <b>{tradePlan.volRatio != null ? `${tradePlan.volRatio.toFixed(1)}×` : "—"}</b>
                                  <em className="pat-level-note">{tradePlan.volNote}</em>
                                </div>
                              </div>
                            );
                          })()}
                          <ul className="trade-plan-reasons">
                            {tradePlan.reasons.map((r) => (
                              <li key={r}>{r}</li>
                            ))}
                          </ul>
                          {tradePlan.cautions.length ? (
                            <ul className="trade-plan-cautions">
                              {tradePlan.cautions.map((c) => (
                                <li key={c}>{c}</li>
                              ))}
                            </ul>
                          ) : null}
                          {indicators ? (
                            <p className="small muted">
                              RSI {fmtNum(indicators.snapshot.rsi14, 0)} · K {fmtNum(indicators.snapshot.k, 0)} D {fmtNum(indicators.snapshot.d, 0)} · 布林位 {fmtNum(indicators.snapshot.bollPos, 0)}%
                            </p>
                          ) : null}
                        </div>
                      ) : null}

                      {selectedTechSignal && projectedTech ? (
                        <div className="pattern-side-block pat-detail-box">
                          <h4>選中技術訊號</h4>
                          <div className="pat-detail-head">
                            <strong>{selectedTechSignal.rule}</strong>
                            <Tag tone={projectedTech.side === "buy" ? "up" : projectedTech.side === "sell" ? "down" : "gold"}>
                              {projectedTech.side === "buy" ? "買" : projectedTech.side === "sell" ? "賣" : "觀察"}
                            </Tag>
                          </div>
                          <p className="small muted">{selectedTechSignal.date} · {selectedTechSignal.reason}</p>
                          <p className="small muted">{projectedTech.contextNote}</p>
                          {(() => {
                            const lv = tradeLevelLabels(projectedTech.side);
                            const px = projectedTech.currentPrice;
                            return (
                              <div className="pat-levels pat-levels--stacked">
                                <div className="pat-level-row">
                                  <span>現價（基準）</span>
                                  <b>{fmtNum(px)}</b>
                                </div>
                                <div className="pat-level-row">
                                  <span>{lv.entry}</span>
                                  <b className={priceLevelClass(projectedTech.entry, px)}>{fmtNum(projectedTech.entry)}</b>
                                  <em className="pat-level-note">{projectedTech.entryNote}</em>
                                </div>
                                <div className="pat-level-row">
                                  <span>{lv.stop}</span>
                                  <b className={priceLevelClass(projectedTech.stop, px)}>{fmtNum(projectedTech.stop)}</b>
                                  <em className="pat-level-note">{projectedTech.stopNote}</em>
                                </div>
                                <div className="pat-level-row">
                                  <span>{lv.target}</span>
                                  <b className={priceLevelClass(projectedTech.target, px)}>{fmtNum(projectedTech.target)}</b>
                                  <em className="pat-level-note">{projectedTech.targetNote}</em>
                                </div>
                                <div className="pat-level-row">
                                  <span>觸發日收盤</span>
                                  <b className={priceLevelClass(projectedTech.signalDayPrice, px)}>{fmtNum(projectedTech.signalDayPrice)}</b>
                                  <em className="pat-level-note">僅供對照，不作為今日進場價</em>
                                </div>
                              </div>
                            );
                          })()}
                        </div>
                      ) : null}

                      <div className="pattern-side-block">
                        <h4>技術買賣點（{techSignals.length}）</h4>
                        <p className="small muted" style={{ margin: "0 0 8px" }}>
                          點選訊號可在上方查看進場／停損／目標，圖上僅標記箭頭
                        </p>
                        {techSignals.length ? (
                          <ul className="pat-signal-list">
                            {techSignals.slice(0, 20).map((s, i) => {
                              const on = selectedTechSig?.date === s.date && selectedTechSig?.ruleId === s.ruleId;
                              return (
                                <li key={`${s.ruleId}-${s.date}-${i}`}>
                                  <button
                                    type="button"
                                    className={`pat-signal-btn ${on ? "active" : ""}`}
                                    onClick={() => selectTechSignal(s)}
                                  >
                                    <span className="pat-signal-name">{s.rule}</span>
                                    <Tag tone={s.type === "buy" ? "up" : s.type === "sell" ? "down" : "gold"}>
                                      {s.type === "buy" ? "買" : s.type === "sell" ? "賣" : "觀察"}
                                    </Tag>
                                    <span className="pat-signal-meta">{s.date} · {s.reason}</span>
                                  </button>
                                </li>
                              );
                            })}
                          </ul>
                        ) : (
                          <p className="small muted">拉長區間（建議 2y+）以搜尋更多買賣點。</p>
                        )}
                      </div>

                      <div className="pattern-side-block">
                        <h4>書本法則 · 即時對照</h4>
                        {[granville, guppy].filter(Boolean).map((r) => (
                          <div key={r!.id} className={`rule-hit rule-${r!.bias}`}>
                            <Tag tone={r!.bias === "bullish" ? "up" : r!.bias === "bearish" ? "down" : "gold"}>
                              {r!.category}
                            </Tag>
                            <div className="rule-name">{r!.name}</div>
                            <p className="rule-summary">{r!.summary}</p>
                            <p className="rule-hint">{r!.hint}</p>
                          </div>
                        ))}
                        {!granville && !guppy ? (
                          <p className="small muted">資料不足，無法判斷葛蘭碧／顧比狀態。</p>
                        ) : null}
                      </div>

                      <div className="pattern-side-block">
                        <h4>線型掃描結果</h4>
                        {!scanOn ? (
                          <p className="small muted">按「線型掃描」對照書中 18 招可偵測型態。</p>
                        ) : scanQ.isLoading ? (
                          <Loading label="掃描線型…" />
                        ) : displaySignals.length ? (
                          <ul className="pat-signal-list">
                            {displaySignals.map((s, i) => {
                              const on =
                                selectedPat?.date === s.date &&
                                selectedPat?.patternId === (s.patternId || s.id);
                              return (
                                <li key={`${s.patternId}-${s.date}-${i}`}>
                                  <button
                                    type="button"
                                    className={`pat-signal-btn ${on ? "active" : ""}`}
                                    onClick={() => selectPattern(s)}
                                  >
                                    <span className="pat-signal-name">{s.patternName || s.name}</span>
                                    <Tag tone={patTone(s)}>{s.actionLabel || s.action || "訊號"}</Tag>
                                    <span className="pat-signal-meta">
                                      {s.date}
                                      {s.barsAgo != null ? ` · ${s.barsAgo}根前` : ""}
                                    </span>
                                  </button>
                                </li>
                              );
                            })}
                          </ul>
                        ) : (
                          <p className="small muted">尚無訊號。</p>
                        )}
                      </div>

                      {selectedSignal && projectedPattern ? (
                        <div className="pattern-side-block pat-detail-box">
                          <h4>選中線型訊號</h4>
                          <div className="pat-detail-head">
                            <strong>{selectedSignal.patternName || selectedSignal.name}</strong>
                            <Tag tone={patTone(selectedSignal)}>
                              {selectedSignal.actionLabel || selectedSignal.action}
                            </Tag>
                          </div>
                          <p>{selectedSignal.teachSummary || catalogPat?.teach?.summary || selectedSignal.hint}</p>
                          <p className="small muted">{projectedPattern.contextNote}</p>
                          {(() => {
                            const lv = tradeLevelLabels(projectedPattern.side);
                            const px = projectedPattern.currentPrice;
                            return (
                              <div className="pat-levels pat-levels--stacked">
                                <div className="pat-level-row">
                                  <span>現價（基準）</span>
                                  <b>{fmtNum(px)}</b>
                                </div>
                                <div className="pat-level-row">
                                  <span>{lv.entry}</span>
                                  <b className={priceLevelClass(projectedPattern.entry, px)}>{fmtNum(projectedPattern.entry)}</b>
                                  <em className="pat-level-note">{projectedPattern.entryNote}</em>
                                </div>
                                <div className="pat-level-row">
                                  <span>{lv.stop}</span>
                                  <b className={priceLevelClass(projectedPattern.stop, px)}>{fmtNum(projectedPattern.stop)}</b>
                                  <em className="pat-level-note">{projectedPattern.stopNote}</em>
                                </div>
                                <div className="pat-level-row">
                                  <span>{lv.target}</span>
                                  <b className={priceLevelClass(projectedPattern.target, px)}>{fmtNum(projectedPattern.target)}</b>
                                  <em className="pat-level-note">{projectedPattern.targetNote}</em>
                                </div>
                                <div className="pat-level-row">
                                  <span>觸發日收盤</span>
                                  <b className={priceLevelClass(projectedPattern.signalDayPrice, px)}>{fmtNum(projectedPattern.signalDayPrice)}</b>
                                </div>
                                {selectedSignal.riskReward != null ? (
                                  <div className="pat-level-row">
                                    <span>書本風報比</span>
                                    <b>{selectedSignal.riskReward}x</b>
                                    <em className="pat-level-note">線型觸發當日估算，現價已變請自行重算</em>
                                  </div>
                                ) : null}
                              </div>
                            );
                          })()}
                          {selectedSignal.hint ? <p className="pat-hint"><b>操作</b> {selectedSignal.hint}</p> : null}
                          {selectedSignal.exitHint ? <p className="pat-hint"><b>出場</b> {selectedSignal.exitHint}</p> : null}
                          {catalogPat?.teach?.entryHint ? <p className="pat-hint"><b>書本進場</b> {catalogPat.teach.entryHint.slice(0, 160)}…</p> : null}
                          {catalogPat?.bookPage ? (
                            <a
                              className="pat-book-link"
                              href={`/book-patterns/${catalogPat.teach?.bookHtml || ""}`}
                              target="_blank"
                              rel="noopener noreferrer"
                            >
                              開啟書本教學 · 第 {catalogPat.bookPage} 頁
                            </a>
                          ) : null}
                          {catalogPat?.id ? (
                            <button
                              type="button"
                              className="btn-ghost sm mt-s"
                              onClick={() => setPendingPatternBt(catalogPat.id)}
                            >
                              回測此線型策略 →
                            </button>
                          ) : null}
                          <p className="small muted">{selectedSignal.caution || catalogPat?.teach?.caution}</p>
                        </div>
                      ) : null}
                    </aside>
                  </div>

                  <div className="grid g2 mt">
                    <Card
                      title="技術指標"
                      ico={<AppIcon name="cards" size={22} framed variant="hero" />}
                      ai
                      aiFocus={{ ...aiBase, cardTitle: "技術指標" }}
                      onRefresh={() => void refreshPrice()}
                      refreshing={priceRefreshing}
                    >
                      <div className="tech-grid">
                        <div><span>MA20</span><strong>{tech.ma20 != null ? fmtNum(tech.ma20) : "—"}</strong><em>{fmtPct(tech.dist20)}</em></div>
                        <div><span>MA60</span><strong>{indicators?.sma[60]?.slice(-1)[0]?.value != null ? fmtNum(indicators.sma[60].slice(-1)[0].value) : "—"}</strong></div>
                        <div><span>布林上</span><strong>{fmtNum(indicators?.snapshot.bollUpper ?? tech.ma20)}</strong></div>
                        <div><span>布林下</span><strong>{fmtNum(indicators?.snapshot.bollLower)}</strong></div>
                        <div><span>RSI(14)</span><strong>{fmtNum(indicators?.snapshot.rsi14 ?? tech.rsi14, 1)}</strong></div>
                        <div><span>K / D / J</span><strong>{fmtNum(indicators?.snapshot.k, 0)} / {fmtNum(indicators?.snapshot.d, 0)} / {fmtNum(indicators?.snapshot.j, 0)}</strong></div>
                        <div><span>MACD</span><strong>{fmtNum(indicators?.snapshot.macd, 2)}</strong><em>{fmtNum(indicators?.snapshot.macdHist, 2)}</em></div>
                        <div>
                          <span>量能比</span>
                          <strong>
                            {indicators?.snapshot.volRatio != null
                              ? `${fmtNum(indicators.snapshot.volRatio, 2)}×`
                              : tech.volumeRatio != null
                                ? `${fmtNum(tech.volumeRatio, 2)}×`
                                : "—"}
                          </strong>
                          <em className="sub">{describeVolumeRatio(indicators?.snapshot.volRatio ?? tech.volumeRatio)}</em>
                        </div>
                        <div><span>52週位</span><strong>{tech.pos52 != null ? `${fmtNum(tech.pos52, 0)}%` : "—"}</strong></div>
                      </div>
                    </Card>
                    <Card
                      title="歷史報酬"
                      ico={<AppIcon name="compass" size={22} framed variant="hero" />}
                      ai
                      aiFocus={{ ...aiBase, cardTitle: "歷史報酬" }}
                      onRefresh={() => void refreshPrice()}
                      refreshing={priceRefreshing}
                    >
                      <div className="tech-grid">
                        <div><span>1月</span><strong className={hist.ret1m != null && hist.ret1m >= 0 ? "up" : "down"}>{fmtPct(hist.ret1m)}</strong></div>
                        <div><span>3月</span><strong>{fmtPct(hist.ret3m)}</strong></div>
                        <div><span>6月</span><strong>{fmtPct(hist.ret6m)}</strong></div>
                        <div><span>1年</span><strong>{fmtPct(hist.ret1y)}</strong></div>
                        <div><span>波動</span><strong>{hist.volatility != null ? `${fmtNum(hist.volatility, 1)}%` : "—"}</strong></div>
                        <div><span>最大回撤</span><strong className="down">{fmtPct(hist.maxDrawdown)}</strong></div>
                      </div>
                    </Card>
                  </div>

                  {intelQ.data?.management?.longBusinessSummary ? (
                    <Card
                      className="mt"
                      title="公司簡介"
                      ico={<AppIcon name="world" size={22} framed variant="hero" />}
                      ai
                      aiFocus={{ ...aiBase, cardTitle: "公司簡介" }}
                      onRefresh={() => void refreshIntel()}
                      refreshing={intelRefreshing}
                    >
                      <p className="company-blurb">{intelQ.data.management.longBusinessSummary}</p>
                    </Card>
                  ) : null}
                </div>
              ) : null}

              {detailTab === "backtest" ? (
                <div className="backpack-panel">
                  <Card
                    title="策略回測"
                    ico={<AppIcon name="compass" size={22} framed variant="hero" />}
                    ai
                    aiFocus={{ ...aiBase, cardTitle: "策略回測", backtest: btResult }}
                  >
                    <p className="small muted mb-s">
                      以資料庫累積的還原股價（Yahoo/Nasdaq，同 TradingView 日線來源）模擬機械策略。書本線型與葛蘭碧／顧比規則已存入策略庫，也可請貓頭鷹協助新增。
                    </p>

                    <div className="bt-layout">
                      <aside className="bt-strat-list">
                        <div className="bt-strat-head">
                          <h4>策略庫</h4>
                          <span className="small muted">{strategies.length} 筆</span>
                        </div>
                        {strategiesQ.isLoading ? (
                          <Loading label="載入策略…" />
                        ) : (
                          <ul>
                            {strategies.map((s) => (
                              <li key={s.id}>
                                <button
                                  type="button"
                                  className={`bt-strat-btn ${activeBtStrategy?.id === s.id ? "active" : ""}`}
                                  onClick={() => setBtStrategyId(s.id)}
                                >
                                  <span className="bt-strat-name">{s.name}</span>
                                  <span className="bt-strat-src">{s.source === "book" ? "書本" : s.source === "owl" ? "貓頭鷹" : "自訂"}</span>
                                  {s.formula ? <span className="bt-strat-formula">{s.formula}</span> : null}
                                </button>
                              </li>
                            ))}
                          </ul>
                        )}
                      </aside>

                      <div className="bt-main">
                        {activeBtStrategy ? (
                          <>
                            <div className="bt-controls">
                              <div>
                                <label className="bt-label">期間</label>
                                <div className="range-chips">
                                  {["1y", "2y", "5y", "max"].map((r) => (
                                    <button
                                      key={r}
                                      type="button"
                                      className={`range-chip ${btRange === r ? "active" : ""}`}
                                      onClick={() => setBtRange(r)}
                                    >
                                      {r}
                                    </button>
                                  ))}
                                </div>
                              </div>
                              {(activeBtStrategy.paramDefs || []).map((p) => (
                                <div key={p.key}>
                                  <label className="bt-label">{p.label}</label>
                                  <input
                                    type="number"
                                    className="bt-input"
                                    value={btParams[p.key] ?? String(p.def)}
                                    onChange={(e) => setBtParams((prev) => ({ ...prev, [p.key]: e.target.value }))}
                                  />
                                </div>
                              ))}
                              <button type="button" className="btn-primary" disabled={btRunning || !symOk} onClick={() => void runBacktest()}>
                                {btRunning ? "回測中…" : "跑回測"}
                              </button>
                            </div>
                            {activeBtStrategy.description ? (
                              <p className="bt-desc">{activeBtStrategy.description}</p>
                            ) : null}
                          </>
                        ) : (
                          <p className="muted">尚無策略，請重新整理或從下方匯入。</p>
                        )}

                        {btResult ? (
                          <div className="bt-result">
                            <div className="bt-result-head">
                              <strong>{btResult.strategyMeta?.name || btResult.strategyLabel}</strong>
                              <span className="small muted">
                                {btResult.from} ~ {btResult.to} · 初始 ${btResult.capital.toLocaleString()}
                              </span>
                            </div>
                            <EquityChart
                              series={[
                                { name: btResult.strategyLabel, color: "#4da3ff", points: btResult.equity },
                                ...(btResult.benchmark
                                  ? [{ name: "買進持有", color: "#8898bc", points: btResult.benchmark }]
                                  : []),
                              ]}
                            />
                            <div className="bt-stats-grid">
                              {[
                                { title: btResult.strategyLabel, s: btResult.stats, color: "var(--teal)" },
                                { title: "買進持有", s: btResult.benchStats, color: "var(--gold-soft)" },
                              ]
                                .filter((x) => x.s)
                                .map((x) => (
                                  <div key={x.title} className="bt-stat-card" style={{ borderTopColor: x.color }}>
                                    <h5>{x.title}</h5>
                                    <div className="bt-stat-rows">
                                      <div><span>期末</span><b>{fmtMoney(x.s!.finalValue)}</b></div>
                                      <div><span>總報酬</span><b className={x.s!.totalReturn >= 0 ? "up" : "down"}>{fmtPct(x.s!.totalReturn)}</b></div>
                                      <div><span>年化</span><b>{fmtPct(x.s!.cagr)}</b></div>
                                      <div><span>最大回撤</span><b className="down">-{x.s!.maxDrawdown.toFixed(1)}%</b></div>
                                      <div><span>在場</span><b>{x.s!.exposure.toFixed(0)}%</b></div>
                                      <div>
                                        <span>{x.s!.winRate != null ? "勝率" : "交易次數"}</span>
                                        <b>
                                          {x.s!.winRate != null
                                            ? `${x.s!.winRate.toFixed(0)}%（${x.s!.trades}次）`
                                            : `${x.s!.trades} 次`}
                                        </b>
                                      </div>
                                    </div>
                                  </div>
                                ))}
                            </div>
                            {btResult.note ? <p className="bt-note">{btResult.note}</p> : null}
                            <p className="small muted">
                              資料來源：{btResult.dataSource || "db"}。過去績效不代表未來，僅供學習對照。
                            </p>
                          </div>
                        ) : (
                          <p className="muted mt">選策略後按「跑回測」查看權益曲線。</p>
                        )}
                      </div>
                    </div>

                    <details className="bt-import mt">
                      <summary>匯入新策略（JSON / 貓頭鷹對話貼上）</summary>
                      <p className="small muted">
                        跟貓頭鷹說「新增策略」會得到 ```backtest-strategy``` 區塊；貼上後即可存入資料庫。
                      </p>
                      <textarea
                        className="bt-import-area"
                        rows={4}
                        placeholder='{"name":"我的策略","engine":"golden_cross","params":{"fast":5,"slow":25},"description":"…","source":"owl"}'
                        value={importJson}
                        onChange={(e) => setImportJson(e.target.value)}
                      />
                      <button type="button" className="btn-ghost" onClick={() => void importStrategy()}>
                        存入策略庫
                      </button>
                    </details>
                  </Card>
                </div>
              ) : null}

              {detailTab === "financials" ? (
                <div className="backpack-panel">
                  <Card
                    title="財報鑑定"
                    ico={<AppIcon name="scroll" size={22} framed variant="hero" />}
                    ai
                    aiFocus={{ ...aiBase, cardTitle: "財報鑑定" }}
                    onRefresh={() => void refreshFundamentals()}
                    refreshing={fundRefreshing}
                  >
                    {report ? (
                      <div className="fin-summary bright">
                        <Tag tone="up">{report.summary.good} 通過</Tag>
                        <Tag tone="gold">{report.summary.warn} 注意</Tag>
                        <Tag tone="down">{report.summary.bad} 警示</Tag>
                        <Tag tone={verdictTone(report.summary.verdictColor)}>{report.summary.verdict}</Tag>
                      </div>
                    ) : null}
                    {fundQ.isLoading ? (
                      <Loading label="讀取財報…" />
                    ) : report?.steps?.length ? (
                      <div className="fin-steps">
                        {report.steps.map((st) => (
                          <div key={st.num} className="fin-step-block">
                            <h3>
                              <span className="fin-step-num">{st.num}</span>
                              {st.title}
                            </h3>
                            <div className="fin-rows">
                              {st.checks.map((c) => (
                                <FinRow key={c.id} check={c} />
                              ))}
                            </div>
                          </div>
                        ))}
                      </div>
                    ) : (
                      <p className="muted">尚無鑑定資料</p>
                    )}
                  </Card>

                  <AiSummaryCard symbol={symbol} scope="fund" title="AI 財報總結（MCP + 今日快取）" aiFocus={aiBase} />

                  <FundAnalysisPanel
                    data={fundQ.data}
                    aiFocus={aiBase}
                    onRefresh={() => void refreshFundamentals()}
                    refreshing={fundRefreshing}
                  />

                  <div className="fund-layout-stack mt">
                    <Card
                      className="fund-panel-compact"
                      title="季報損益"
                      ico={<AppIcon name="chart" size={22} framed variant="hero" />}
                      ai
                      aiFocus={{ ...aiBase, cardTitle: "季報損益" }}
                      onRefresh={() => void refreshFundamentals()}
                      refreshing={fundRefreshing}
                    >
                      <FinTable
                        rows={fundQ.data?.quarters ?? []}
                        cols={[
                          { key: "revenue", label: "營收", fmt: fmtMoneyTable },
                          { key: "grossMargin", label: "毛利率", fmt: (v) => (v != null ? `${fmtNum(v, 1)}%` : "—") },
                          { key: "operatingMargin", label: "營益率", fmt: (v) => (v != null ? `${fmtNum(v, 1)}%` : "—") },
                          { key: "netMargin", label: "淨利率", fmt: (v) => (v != null ? `${fmtNum(v, 1)}%` : "—") },
                          { key: "eps", label: "EPS", fmt: (v) => fmtNum(v, 2) },
                          { key: "ocf", label: "營業現金流", fmt: fmtMoneyTable },
                        ]}
                      />
                    </Card>

                    <Card
                      className="fund-panel-compact"
                      title="年報損益"
                      ico={<AppIcon name="macro" size={22} framed variant="hero" />}
                      ai
                      aiFocus={{ ...aiBase, cardTitle: "年報損益" }}
                      onRefresh={() => void refreshFundamentals()}
                      refreshing={fundRefreshing}
                    >
                      <FinTable
                        rows={fundQ.data?.annual ?? []}
                        cols={[
                          { key: "revenue", label: "營收", fmt: fmtMoneyTable },
                          { key: "grossProfit", label: "毛利", fmt: fmtMoneyTable },
                          { key: "netIncome", label: "淨利", fmt: fmtMoneyTable },
                          { key: "ocf", label: "營業現金流", fmt: fmtMoneyTable },
                          { key: "capex", label: "資本支出 Capex", fmt: fmtMoneyTable },
                        ]}
                      />
                    </Card>
                  </div>

                  <div className="fund-layout-stack mt">
                    <Card
                      className="fund-panel-compact"
                      title="現金流量表"
                      ico={<AppIcon name="flow" size={22} framed variant="hero" />}
                      ai
                      aiFocus={{ ...aiBase, cardTitle: "現金流量表" }}
                      onRefresh={() => void refreshFundamentals()}
                      refreshing={fundRefreshing}
                    >
                      <FinTable
                        rows={fundQ.data?.annual ?? []}
                        cols={[
                          { key: "ocf", label: "營業現金流", fmt: fmtMoneyTable },
                          { key: "capex", label: "資本支出 Capex", fmt: fmtMoneyTable },
                          { key: "fcfFree", label: "自由現金流 FCF", fmt: fmtMoneyTable },
                          { key: "icf", label: "投資活動", fmt: fmtMoneyTable },
                          { key: "fcf", label: "融資活動", fmt: fmtMoneyTable },
                          { key: "dividends", label: "股利", fmt: fmtMoneyTable },
                          { key: "buybacks", label: "庫藏股", fmt: fmtMoneyTable },
                        ]}
                      />
                    </Card>

                    {fundQ.data?.balance ? (
                      <Card
                        className="fund-panel-compact"
                        title="資產負債表（最近一季）"
                        ico={<AppIcon name="coin" size={22} framed variant="hero" />}
                        ai
                        aiFocus={{ ...aiBase, cardTitle: "資產負債表" }}
                        onRefresh={() => void refreshFundamentals()}
                        refreshing={fundRefreshing}
                      >
                        <div className="fin-table-wrap fund-table-scroll">
                          <table className="fin-table fund-table-wide">
                            <thead>
                              <tr>
                                <th>項目</th>
                                <th>金額</th>
                              </tr>
                            </thead>
                            <tbody>
                              {(
                                [
                                  ["總資產", fundQ.data.balance.totalAssets, false],
                                  ["總負債", fundQ.data.balance.totalLiabilities, false],
                                  ["股東權益", fundQ.data.balance.totalEquity, false],
                                  ["現金餘額", fundQ.data.balance.cash, false],
                                  ["廠房設備 PPE", fundQ.data.balance.ppe, false],
                                  ["商譽 Goodwill", fundQ.data.balance.goodwill, false],
                                  ["存貨", fundQ.data.balance.inventory, false],
                                  ["應收帳款", fundQ.data.balance.receivables, false],
                                  ["流動資產", fundQ.data.balance.currentAssets, false],
                                  ["流動負債", fundQ.data.balance.currentLiabilities, false],
                                  ["負債比", fundQ.data.balance.debtToAssets, true],
                                ] as const
                              ).map(([k, v, pct]) => (
                                <tr key={k}>
                                  <td className="fin-period">
                                    <TermHint term={k} />
                                  </td>
                                  <td>
                                    {pct && typeof v === "number" ? `${fmtNum(v, 1)}%` : fmtMoneyTable(v as number)}
                                  </td>
                                </tr>
                              ))}
                            </tbody>
                          </table>
                        </div>
                        <p className="small muted mt-s">
                          來源：{fundQ.data.source} · 截至 {fundQ.data.asOf || "—"} · 市值 {fmtMoneyShort(fundQ.data.marketCap)} · PE {fmtNum(fundQ.data.peTrailing, 1)}
                        </p>
                      </Card>
                    ) : (
                      <Card className="fund-panel-compact" title="資產負債表（最近一季）" ico={<AppIcon name="coin" size={22} framed variant="hero" />}>
                        <p className="muted">尚無資產負債資料</p>
                      </Card>
                    )}
                  </div>
                </div>
              ) : null}

              {detailTab === "flow" ? (
                <FlowPanel
                  flow={flowQ.data}
                  holders={holdersQ.data}
                  notable={notableQ.data}
                  flowLoading={flowQ.isLoading}
                  holdersLoading={holdersQ.isLoading}
                  notableLoading={notableQ.isLoading}
                  flowError={flowQ.isError}
                  holdersError={holdersQ.isError}
                  notableError={notableQ.isError}
                  chipRefreshing={chipRefreshing}
                  onRefreshChip={() => void refreshChipData()}
                  aiBase={aiBase}
                />
              ) : null}

              {detailTab === "company" ? (
                <CompanyIntelPanel
                  symbol={symbol}
                  intel={intelQ.data}
                  intelLoading={intelQ.isLoading}
                  secArchive={secArchiveQ.data}
                  secLoading={secArchiveQ.isLoading || secArchiveQ.isFetching}
                  holders={companyHoldersQ.data}
                  syncingIntel={intelSyncMut.isPending}
                  syncingSec={secSyncMut.isPending}
                  onSyncIntel={(force) => intelSyncMut.mutate(force)}
                  onReloadIntel={() => void refreshIntel()}
                  intelReloading={intelRefreshing}
                  onSyncSec={(sync) => secSyncMut.mutate(sync)}
                  aiFocus={aiBase}
                />
              ) : null}

            </section>
          ) : selected ? (
            <p className="muted mt">代號格式不正確</p>
          ) : symbols.length ? (
            <p className="muted mt backpack-hint">點選上方背包格子，下方會固定顯示 K 線、財報、籌碼與公司資訊。</p>
          ) : null}
          </div>
        </main>
      </div>
    </>
  );
}