/* global React, ReactDOM, BriefingCard, PortfolioTable, CashBar, SoldRecords, SellModal,
   AddHoldingModal, EditHoldingModal, AlertsPage, SettingsModal,
   useTweaks, TweaksPanel, TweakSection, TweakRadio, TweakColor */
const { useState, useEffect, useRef, useCallback } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "#0064E0",
  "showBriefing": true
}/*EDITMODE-END*/;

// localStorage hook — initial value from storage if present, otherwise seed.
function usePersistentState(key, seed) {
  const [v, setV] = useState(() => {
    const stored = window.storage.load(key, undefined);
    return stored === undefined ? seed : stored;
  });
  useEffect(() => { window.storage.save(key, v); }, [key, v]);
  return [v, setV];
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [tab, setTab] = useState("dashboard");

  // 영속화되는 데이터 — 모두 빈 상태에서 시작 (사용자가 직접 입력)
  const [holdingsKR,  setHoldingsKR]  = usePersistentState("holdings.kr", []);
  const [holdingsUS,  setHoldingsUS]  = usePersistentState("holdings.us", []);
  const [soldRecords, setSoldRecords] = usePersistentState("soldRecords",  []);
  const [alerts,      setAlerts]      = usePersistentState("alerts",       []);
  const [briefing,    setBriefing]    = usePersistentState("briefing",     null);
  const [krwCash,     setKrwCash]     = usePersistentState("cash.krw", 0);
  const [usdCash,     setUsdCash]     = usePersistentState("cash.usd", 0);
  const [fxRate,      setFxRate]      = usePersistentState("fx.rate", 1372.50);
  const [fxAsOf,      setFxAsOf]      = usePersistentState("fx.asOf", null);
  const [pricesAsOf,  setPricesAsOf]  = usePersistentState("prices.asOf", null);
  const [analyzedAt,  setAnalyzedAt]  = usePersistentState("analysis.at", null);
  const [settings, setSettings] = usePersistentState("settings", {
    model: "claude-haiku-4-5-20251001",
    autoFx: true,
    autoPrices: true,
    autoRefreshMin: 5,
    investStyle: "장투",
    stopLossPct: -10,
    useWebSearch: true
  });

  // UI 상태
  const [expandedKR, setExpandedKR] = useState(null);
  const [expandedUS, setExpandedUS] = useState(null);
  const [historyLoading, setHistoryLoading] = useState({});
  const [reanalyzingIds, setReanalyzingIds] = useState({});
  const [sellTarget, setSellTarget] = useState(null);
  const [addMarket,  setAddMarket]  = useState(null);
  const [editTarget, setEditTarget] = useState(null);
  const [showSettings, setShowSettings] = useState(false);
  const [status, setStatus] = useState({ kind: "idle", msg: "" });
  const [loading, setLoading] = useState({ fx: false, prices: false, analyze: false, pfAnalyze: false });
  const [briefingLoading, setBriefingLoading] = useState(false);

  // 포트폴리오 전체 분석 (영속)
  const [portfolioAnalysis, setPortfolioAnalysis] = usePersistentState("portfolio.analysis", null);

  // accent → CSS 변수
  useEffect(() => {
    document.documentElement.style.setProperty("--primary", t.accent);
    document.documentElement.style.setProperty("--primary-deep",
      t.accent === "#0064E0" ? "#0143b5" :
      t.accent === "#1d9856" ? "#15703f" :
      t.accent === "#5e2cb8" ? "#3f1c80" :
      t.accent === "#0a1317" ? "#000000" : "#0143b5"
    );
  }, [t.accent]);

  // ────────────────────────────────────────────────────────────
  // 데이터 페치
  // ────────────────────────────────────────────────────────────
  const refreshFx = useCallback(async () => {
    setLoading((l) => ({ ...l, fx: true }));
    try {
      const { rate, asOf } = await window.api.fetchFxUsdKrw();
      setFxRate(rate);
      setFxAsOf(asOf);
      setStatus({ kind: "ok", msg: `환율 갱신 · ${rate.toFixed(2)} (${asOf})` });
    } catch (e) {
      setStatus({ kind: "err", msg: "환율 조회 실패: " + e.message });
    } finally {
      setLoading((l) => ({ ...l, fx: false }));
    }
  }, [setFxRate, setFxAsOf]);

  // 단일 종목 차트/지표 로드 (행 펼침 시 자동 호출)
  const loadHistory = useCallback(async (h, market) => {
    if (!h || !h.code) return;
    if (historyLoading[h.id]) return;
    setHistoryLoading((prev) => ({ ...prev, [h.id]: true }));
    try {
      const hist = await window.api.fetchHistory(h.code);
      const ind = window.api.computeIndicators(hist);
      const spark = hist.slice(-130).map((p) => p.close); // 최근 ~6개월
      const merge = (x) => x.id === h.id ? {
        ...x,
        indicators: ind,
        historySpark: spark,
        historyAsOf: new Date().toISOString()
      } : x;
      if (market === "kr") setHoldingsKR((prev) => prev.map(merge));
      else setHoldingsUS((prev) => prev.map(merge));
    } catch (e) {
      console.warn(`[history] fail for ${h.code}:`, e.message);
      setStatus({ kind: "warn", msg: `${h.name} 차트 데이터 실패: ${e.message}` });
    } finally {
      setHistoryLoading((prev) => {
        const next = { ...prev };
        delete next[h.id];
        return next;
      });
    }
  }, [historyLoading, setHoldingsKR, setHoldingsUS]);

  const refreshPrices = useCallback(async () => {
    setLoading((l) => ({ ...l, prices: true }));
    try {
      const allCodes = [
        ...holdingsKR.map((h) => h.code),
        ...holdingsUS.map((h) => h.code)
      ];
      const result = await window.api.fetchPrices(allCodes);
      let okCount = 0, failCount = 0;
      const apply = (h) => {
        const r = result[h.code];
        if (r && r.price && isFinite(r.price)) {
          okCount++;
          return { ...h, currentPrice: r.price, priceAsOf: r.asOf };
        }
        failCount++;
        return h;
      };
      setHoldingsKR(holdingsKR.map(apply));
      setHoldingsUS(holdingsUS.map(apply));
      setPricesAsOf(new Date().toISOString());
      setStatus({
        kind: failCount ? "warn" : "ok",
        msg: `주가 갱신 · 성공 ${okCount}건${failCount ? ` · 실패 ${failCount}건 (수동 입력 필요)` : ""}`
      });
    } catch (e) {
      setStatus({ kind: "err", msg: "주가 조회 실패: " + e.message });
    } finally {
      setLoading((l) => ({ ...l, prices: false }));
    }
  }, [holdingsKR, holdingsUS, setHoldingsKR, setHoldingsUS, setPricesAsOf]);

  // 마운트 시 자동 갱신 (설정에 따라)
  const didAutoFetch = useRef(false);
  useEffect(() => {
    if (didAutoFetch.current) return;
    didAutoFetch.current = true;
    if (settings.autoFx) refreshFx();
    if (settings.autoPrices) refreshPrices();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // 주기적 자동 갱신 (탭 활성 + 보유 종목 있을 때만)
  useEffect(() => {
    const min = Number(settings.autoRefreshMin) || 0;
    if (!settings.autoPrices || !min) return;
    const tick = () => {
      if (document.visibilityState !== "visible") return;
      if (loading.prices || loading.fx || loading.analyze) return;
      if (holdingsKR.length === 0 && holdingsUS.length === 0) return;
      refreshPrices();
      if (settings.autoFx) refreshFx();
    };
    const id = setInterval(tick, min * 60 * 1000);
    return () => clearInterval(id);
  }, [settings.autoPrices, settings.autoFx, settings.autoRefreshMin,
      holdingsKR.length, holdingsUS.length, loading.prices, loading.fx, loading.analyze,
      refreshPrices, refreshFx]);

  // 브리핑 전용 갱신 (서버 /api/briefing/refresh 호출)
  const refreshBriefingOnly = useCallback(async () => {
    setBriefingLoading(true);
    setStatus({ kind: "info", msg: "시장 브리핑 갱신 중... (약 10~20초 소요)" });
    try {
      const r = await fetch("/api/briefing/refresh", { method: "POST" });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || "HTTP " + r.status);
      setBriefing(j.briefing);
      setStatus({ kind: "ok", msg: "시장 브리핑 갱신 완료" });
    } catch (e) {
      setStatus({ kind: "err", msg: "브리핑 갱신 실패: " + e.message });
    } finally {
      setBriefingLoading(false);
    }
  }, [setBriefing]);

  // ────────────────────────────────────────────────────────────
  // 분석 실행 (Claude API)
  // ────────────────────────────────────────────────────────────
  const runAnalysis = useCallback(async () => {
    setLoading((l) => ({ ...l, analyze: true }));
    setStatus({ kind: "info", msg: "Claude 분석 진행 중... (web_search 활성 시 종목당 10~30초 소요)" });

    const newAlerts = [];
    const nowIso = new Date().toISOString();
    const stamp = nowIso.replace("T", " ").slice(0, 16);

    // 분석 시점의 포트폴리오 컨텍스트 스냅샷 (호출 중 변경 안 됨)
    const portfolioCtx = buildPortfolioContext();
    const userProfile = {
      style: settings.investStyle || "장투",
      stopLoss: settings.stopLossPct ?? -10
    };
    const useWebSearch = settings.useWebSearch !== false;

    const updateOne = async (h, market) => {
      // 1) 일봉 + 지표 갱신 (실패해도 LLM은 진행)
      let indicators = h.indicators;
      let historySpark = h.historySpark;
      try {
        const hist = await window.api.fetchHistory(h.code);
        indicators = window.api.computeIndicators(hist);
        historySpark = hist.slice(-130).map((p) => p.close);
      } catch (e) {
        console.warn(`[history] fail for ${h.code}:`, e.message);
      }
      // 2) Claude 분석 (지표 + 포트폴리오 + 시장 브리핑 + 사용자 프로파일 + 웹검색)
      try {
        const next = await window.api.analyzeHolding({
          holding: h, market, fxRate,
          model: settings.model,
          indicators,
          portfolioContext: portfolioCtx,
          marketBriefing: briefing?.[market],
          userProfile,
          useWebSearch
        });
        const diff = window.api.diffEval(h.eval, next.eval);
        if (diff) {
          newAlerts.push({
            id: "a" + Date.now() + "-" + h.id,
            type: diff,
            code: h.code, name: h.name, market,
            title: `판단 변경: ${capEval(h.eval)}${capEval(next.eval)}`,
            fromEval: h.eval, toEval: next.eval,
            detail: next.reason,
            time: stamp,
            related: "Claude 자동 분석",
            unread: true
          });
        }
        return { ...h, ...next, indicators, historySpark, historyAsOf: new Date().toISOString() };
      } catch (e) {
        setStatus({ kind: "err", msg: `${h.name} 분석 실패: ${e.message}` });
        return { ...h, indicators, historySpark, historyAsOf: new Date().toISOString() };
      }
    };

    try {
      // 보유 종목 분석 (순차 — 병렬 시 입력 토큰/분 한도 초과 방지)
      const total = holdingsKR.length + holdingsUS.length;
      let done = 0;
      const krNext = [];
      for (const h of holdingsKR) {
        setStatus({ kind: "info", msg: `분석 중 (${++done}/${total}) · ${h.name}` });
        krNext.push(await updateOne(h, "kr"));
      }
      const usNext = [];
      for (const h of holdingsUS) {
        setStatus({ kind: "info", msg: `분석 중 (${++done}/${total}) · ${h.name}` });
        usNext.push(await updateOne(h, "us"));
      }
      setHoldingsKR(krNext);
      setHoldingsUS(usNext);

      // 시장 브리핑 (순차)
      try {
        setStatus({ kind: "info", msg: "브리핑 생성 중 · 한국 시장..." });
        const krB = await window.api.analyzeBriefing({ market: "kr", fxRate, model: settings.model, useWebSearch });
        setStatus({ kind: "info", msg: "브리핑 생성 중 · 미국 시장..." });
        const usB = await window.api.analyzeBriefing({ market: "us", fxRate, model: settings.model, useWebSearch });
        setBriefing({ kr: krB, us: usB });
      } catch (e) {
        setStatus({ kind: "warn", msg: "브리핑 생성 실패: " + e.message });
      }

      if (newAlerts.length) setAlerts([...newAlerts, ...alerts]);
      setAnalyzedAt(nowIso);
      setStatus({
        kind: "ok",
        msg: `분석 완료 · 종목 ${holdingsKR.length + holdingsUS.length}건${newAlerts.length ? ` · 판단 변동 ${newAlerts.length}건` : ""}`
      });

      // 포트폴리오 종합 분석 (종목 분석 완료 후 순차 실행)
      if (holdingsKR.length + holdingsUS.length > 0) {
        setStatus({ kind: "info", msg: "포트폴리오 종합 분석 중..." });
        setLoading((l) => ({ ...l, pfAnalyze: true }));
        try {
          const pfResult = await window.api.analyzePortfolioOverall({
            model: settings.model,
            holdingsKR: krNext, holdingsUS: usNext,
            fxRate, krwCash, usdCash
          });
          setPortfolioAnalysis(pfResult);
          setStatus({ kind: "ok", msg: `전체 분석 완료 · 종목 ${krNext.length + usNext.length}건 + 포트폴리오 종합` });
        } catch (e) {
          setStatus({ kind: "warn", msg: "포트폴리오 종합 분석 실패: " + e.message });
        } finally {
          setLoading((l) => ({ ...l, pfAnalyze: false }));
        }
      }
    } catch (e) {
      setStatus({ kind: "err", msg: "분석 중 오류: " + e.message });
    } finally {
      setLoading((l) => ({ ...l, analyze: false }));
    }
  }, [settings, fxRate, holdingsKR, holdingsUS, alerts, krwCash, usdCash, setHoldingsKR, setHoldingsUS, setBriefing, setAlerts, setAnalyzedAt, setPortfolioAnalysis]);

  // 단일 종목 재분석
  const reanalyzeOne = useCallback(async (h, market) => {
    setReanalyzingIds((prev) => ({ ...prev, [h.id]: true }));
    setStatus({ kind: "info", msg: `${h.name} 분석 중...` });
    let indicators = h.indicators;
    let historySpark = h.historySpark;
    try {
      const hist = await window.api.fetchHistory(h.code);
      indicators = window.api.computeIndicators(hist);
      historySpark = hist.slice(-130).map((p) => p.close);
    } catch (e) {
      console.warn(`[history] fail for ${h.code}:`, e.message);
    }
    try {
      const next = await window.api.analyzeHolding({
        holding: h, market, fxRate, model: settings.model,
        indicators,
        portfolioContext: buildPortfolioContext(),
        marketBriefing: briefing?.[market],
        userProfile: { style: settings.investStyle || "장투", stopLoss: settings.stopLossPct ?? -10 },
        useWebSearch: settings.useWebSearch !== false
      });
      const diff = window.api.diffEval(h.eval, next.eval);
      const merged = { ...h, ...next, indicators, historySpark, historyAsOf: new Date().toISOString() };
      if (market === "kr") setHoldingsKR((prev) => prev.map((x) => x.id === h.id ? merged : x));
      else setHoldingsUS((prev) => prev.map((x) => x.id === h.id ? merged : x));
      if (diff) {
        const stamp = new Date().toISOString().replace("T", " ").slice(0, 16);
        setAlerts([{
          id: "a" + Date.now() + "-" + h.id,
          type: diff, code: h.code, name: h.name, market,
          title: `판단 변경: ${capEval(h.eval)}${capEval(next.eval)}`,
          fromEval: h.eval, toEval: next.eval,
          detail: next.reason, time: stamp,
          related: "Claude 자동 분석", unread: true
        }, ...alerts]);
      }
      setStatus({ kind: "ok", msg: `${h.name} 분석 완료 · ${capEval(next.eval)}` });
    } catch (e) {
      setStatus({ kind: "err", msg: `${h.name} 분석 실패: ${e.message}` });
    } finally {
      setReanalyzingIds((prev) => { const n = { ...prev }; delete n[h.id]; return n; });
    }
  }, [settings, fxRate, holdingsKR, holdingsUS, alerts, setHoldingsKR, setHoldingsUS, setAlerts]);

  // 포트폴리오 전체 종합 분석
  const runPortfolioAnalysis = useCallback(async () => {
    if (holdingsKR.length + holdingsUS.length === 0) { setStatus({ kind: "err", msg: "보유 종목이 없습니다." }); return; }
    setLoading((l) => ({ ...l, pfAnalyze: true }));
    setStatus({ kind: "info", msg: "포트폴리오 종합 분석 중..." });
    try {
      const result = await window.api.analyzePortfolioOverall({
        model: settings.model,
        holdingsKR, holdingsUS, fxRate, krwCash, usdCash
      });
      setPortfolioAnalysis(result);
      setStatus({ kind: "ok", msg: "포트폴리오 종합 분석 완료" });
    } catch (e) {
      setStatus({ kind: "err", msg: "포트폴리오 분석 실패: " + e.message });
    } finally {
      setLoading((l) => ({ ...l, pfAnalyze: false }));
    }
  }, [settings, holdingsKR, holdingsUS, fxRate, krwCash, usdCash, setPortfolioAnalysis]);

  // MD 추출 — 파일 2개 순차 다운로드
  const exportMarkdown = useCallback(() => {
    const now = new Date();
    const dateStr = now.toLocaleString("ko-KR");
    const dateKey = now.toISOString().slice(0, 10);
    const fmt = (n) => Math.round(n).toLocaleString("ko-KR");
    const fmtPctV = (n, t) => t ? ((n / t) * 100).toFixed(1) + "%" : "0.0%";

    const totalKRV    = holdingsKR.reduce((s, h) => s + h.currentPrice * h.qty, 0);
    const totalUSVUSD = holdingsUS.reduce((s, h) => s + h.currentPrice * h.qty, 0);
    const totalUSVKRW = totalUSVUSD * fxRate;
    const totalCashKRW = krwCash + usdCash * fxRate;
    const grand = totalKRV + totalUSVKRW + totalCashKRW;

    // ── 파일 1: 포트폴리오 현황 ──────────────────────────────
    const lines1 = [];
    lines1.push(`# 포트폴리오 현황`);
    lines1.push(`> 추출일: ${dateStr}  |  USD/KRW: ${fxRate.toLocaleString("ko-KR", { maximumFractionDigits: 2 })}`);
    lines1.push(``);
    lines1.push(`## 자산 개요`);
    lines1.push(`| 항목 | 금액 | 비중 |`);
    lines1.push(`|------|-----:|-----:|`);
    lines1.push(`| 총 자산 | ₩${fmt(grand)} | 100% |`);
    lines1.push(`| 국내 주식 | ₩${fmt(totalKRV)} | ${fmtPctV(totalKRV, grand)} |`);
    lines1.push(`| 미국 주식 | ₩${fmt(totalUSVKRW)} (≈$${totalUSVUSD.toLocaleString("en-US", { maximumFractionDigits: 0 })}) | ${fmtPctV(totalUSVKRW, grand)} |`);
    lines1.push(`| 현금 | ₩${fmt(totalCashKRW)} | ${fmtPctV(totalCashKRW, grand)} |`);
    lines1.push(``);
    lines1.push(`## 예수금`);
    lines1.push(`| 통화 | 금액 |`);
    lines1.push(`|------|-----:|`);
    lines1.push(`| 원화 | ₩${fmt(krwCash)} |`);
    lines1.push(`| 달러 | $${usdCash.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} |`);
    lines1.push(``);

    const holdingTable = (lines, holdings, isUS) => {
      const ccy = isUS ? "USD" : "KRW";
      lines.push(`| 종목 | 코드 | 매수일 | 매수가(${ccy}) | 수량 | 현재가(${ccy}) | 평가금액 | 손익 | 수익률 | 평가 |`);
      lines.push(`|------|------|--------|--------:|-----:|--------:|--------:|-----:|-------:|------|`);
      holdings.forEach((h) => {
        const buy = h.buyPrice * h.qty;
        const cur = h.currentPrice * h.qty;
        const pl = cur - buy;
        const plPct = buy ? ((pl / buy) * 100).toFixed(2) : "0.00";
        const curKRW = isUS ? cur * fxRate : cur;
        const plSign = pl >= 0 ? "+" : "";
        lines.push(`| ${h.name} | ${h.code} | ${h.buyDate} | ${h.buyPrice.toLocaleString()} | ${h.qty} | ${h.currentPrice.toLocaleString()} | ₩${fmt(curKRW)} | ${plSign}${pl.toLocaleString("en-US", { maximumFractionDigits: 2 })} | ${plSign}${plPct}% | ${h.eval || "-"} |`);
      });
    };

    lines1.push(`## 국내 보유종목 (${holdingsKR.length}개)`);
    if (holdingsKR.length > 0) holdingTable(lines1, holdingsKR, false);
    else lines1.push(`보유 종목 없음`);
    lines1.push(``);
    lines1.push(`## 미국 보유종목 (${holdingsUS.length}개)`);
    if (holdingsUS.length > 0) holdingTable(lines1, holdingsUS, true);
    else lines1.push(`보유 종목 없음`);

    // ── 파일 2: 종목 분석 + 포트폴리오 종합 분석 ────────────
    const lines2 = [];
    lines2.push(`# 포트폴리오 분석 리포트`);
    lines2.push(`> 추출일: ${dateStr}  |  분석 기준: ${analyzedAt ? new Date(analyzedAt).toLocaleString("ko-KR") : "미실행"}`);
    lines2.push(``);

    const holdingSection = (holdings, market) => {
      const isUS = market === "us";
      const ccy = isUS ? "USD" : "KRW";
      const EVAL_KR = { hold: "Hold (유지)", trim: "Trim (일부 매도)", sell: "Sell (매도)", add: "Add (추가 매수)" };

      holdings.forEach((h) => {
        const buy = h.buyPrice * h.qty;
        const cur = h.currentPrice * h.qty;
        const pl = cur - buy;
        const plPct = buy ? ((pl / buy) * 100).toFixed(2) : "0.00";
        const plSign = pl >= 0 ? "+" : "";

        lines2.push(`### ${h.name} (${h.code})`);
        lines2.push(``);
        lines2.push(`| 항목 | 값 |`);
        lines2.push(`|------|-----|`);
        lines2.push(`| 매수일 | ${h.buyDate} |`);
        lines2.push(`| 매수가 | ${h.buyPrice.toLocaleString()} ${ccy} |`);
        lines2.push(`| 수량 | ${h.qty}주 |`);
        lines2.push(`| 현재가 | ${h.currentPrice.toLocaleString()} ${ccy} |`);
        lines2.push(`| 평가손익 | ${plSign}${pl.toLocaleString("en-US", { maximumFractionDigits: 2 })} ${ccy} (${plSign}${plPct}%) |`);
        lines2.push(`| **판단** | **${EVAL_KR[h.eval] || h.eval || "-"}** |`);
        lines2.push(`| 신뢰도 | ${h.confidence || "-"} |`);
        if (h.analyzedAt) lines2.push(`| 분석 시각 | ${new Date(h.analyzedAt).toLocaleString("ko-KR")} |`);
        lines2.push(``);

        if (h.reason) {
          lines2.push(`**판단 근거**`);
          lines2.push(``);
          lines2.push(h.reason);
          lines2.push(``);
        }

        if (h.bullets && h.bullets.length > 0) {
          lines2.push(`**주요 포인트 (Bull)**`);
          lines2.push(``);
          h.bullets.forEach((b) => lines2.push(`- ${b}`));
          lines2.push(``);
        }

        if (h.risks && h.risks.length > 0) {
          lines2.push(`**리스크 / 모니터링**`);
          lines2.push(``);
          h.risks.forEach((r) => lines2.push(`- ${r}`));
          lines2.push(``);
        }

        if (h.portfolioNote) {
          lines2.push(`**포트폴리오 분산 관점**`);
          lines2.push(``);
          lines2.push(h.portfolioNote);
          lines2.push(``);
        }

        if (h.indicators) {
          const ind = h.indicators;
          const fmtI = (n, d = 1) => n == null || !isFinite(n) ? "N/A" : n.toFixed(d);
          const fmtP = (n) => n == null || !isFinite(n) ? "N/A" : (n >= 0 ? "+" : "") + n.toFixed(1) + "%";
          lines2.push(`**기술적 지표**`);
          lines2.push(``);
          lines2.push(`| 지표 | 값 |`);
          lines2.push(`|------|-----|`);
          lines2.push(`| RSI(14) | ${fmtI(ind.rsi)}${ind.rsi >= 70 ? " (과매수)" : ind.rsi <= 30 ? " (과매도)" : ""} |`);
          lines2.push(`| vs 20일선 | ${fmtP(ind.pctFromSma20)} |`);
          lines2.push(`| vs 60일선 | ${fmtP(ind.pctFromSma60)} |`);
          lines2.push(`| vs 200일선 | ${fmtP(ind.pctFromSma200)} |`);
          lines2.push(`| 52주 고점 대비 | ${fmtP(ind.pctFromMax52w)} |`);
          lines2.push(`| 52주 저점 대비 | ${fmtP(ind.pctFromMin52w)} |`);
          lines2.push(`| 거래량 5d/60d | ${ind.volRatio == null ? "N/A" : ind.volRatio.toFixed(2) + "배"} |`);
          lines2.push(``);
        }

        lines2.push(`---`);
        lines2.push(``);
      });
    };

    lines2.push(`## 국내 종목 분석`);
    lines2.push(``);
    if (holdingsKR.length > 0) holdingSection(holdingsKR, "kr");
    else lines2.push(`보유 종목 없음`);

    lines2.push(`## 미국 종목 분석`);
    lines2.push(``);
    if (holdingsUS.length > 0) holdingSection(holdingsUS, "us");
    else lines2.push(`보유 종목 없음`);

    // 포트폴리오 종합 분석
    lines2.push(`## 포트폴리오 종합 분석`);
    lines2.push(``);
    if (portfolioAnalysis) {
      const pa = portfolioAnalysis;
      const PRIORITY_KR = { "즉시": "🔴 즉시", "단기": "🟡 단기", "중기": "🟢 중기" };

      lines2.push(`**종합 점수: ${pa.score}/10** · ${pa.scoreComment || ""}`);
      lines2.push(``);
      lines2.push(pa.summary || "");
      lines2.push(``);

      if (pa.concentration) {
        const c = pa.concentration;
        lines2.push(`### 집중도 현황`);
        lines2.push(``);
        lines2.push(`| 항목 | 값 |`);
        lines2.push(`|------|-----|`);
        lines2.push(`| 최대 비중 종목 | ${c.topHolding} (${c.topHoldingPct?.toFixed(1)}%) |`);
        lines2.push(`| KR / US / 현금 | ${c.krPct?.toFixed(1)}% / ${c.usPct?.toFixed(1)}% / ${c.cashPct?.toFixed(1)}% |`);
        lines2.push(`| 집중 리스크 | ${c.risk} |`);
        lines2.push(``);
      }

      if (pa.sectorBreakdown && pa.sectorBreakdown.length > 0) {
        lines2.push(`### 섹터 배분`);
        lines2.push(``);
        lines2.push(`| 섹터 | 비중 | 평가 | 코멘트 |`);
        lines2.push(`|------|-----:|------|--------|`);
        pa.sectorBreakdown.forEach((s) => {
          lines2.push(`| ${s.sector} | ${(s.pct || 0).toFixed(1)}% | ${s.eval} | ${s.comment || ""} |`);
        });
        lines2.push(``);
      }

      if (pa.rebalancingActions && pa.rebalancingActions.length > 0) {
        lines2.push(`### 리밸런싱 우선순위`);
        lines2.push(``);
        lines2.push(`| 우선순위 | 액션 | 대상 | 이유 |`);
        lines2.push(`|----------|------|------|------|`);
        pa.rebalancingActions.forEach((a) => {
          lines2.push(`| ${PRIORITY_KR[a.priority] || a.priority} | ${a.action} | ${a.target} | ${a.reason} |`);
        });
        lines2.push(``);
      }

      if (pa.portfolioOptimization && pa.portfolioOptimization.length > 0) {
        lines2.push(`### 포트폴리오 최적화 제안`);
        lines2.push(``);
        pa.portfolioOptimization.forEach((opt) => lines2.push(`- ${opt}`));
        lines2.push(``);
      }

      if (pa.strengthsAndWeaknesses) {
        lines2.push(`### 강점 / 약점`);
        lines2.push(``);
        (pa.strengthsAndWeaknesses.strengths || []).forEach((s) => lines2.push(`- ✅ ${s}`));
        (pa.strengthsAndWeaknesses.weaknesses || []).forEach((w) => lines2.push(`- ⚠️ ${w}`));
        lines2.push(``);
      }

      if (pa.outlook) {
        lines2.push(`### 3~6개월 전망`);
        lines2.push(``);
        lines2.push(pa.outlook);
        lines2.push(``);
      }

      if (pa.analyzedAt) {
        lines2.push(`> 분석 시각: ${new Date(pa.analyzedAt).toLocaleString("ko-KR")}`);
      }
    } else {
      lines2.push(`> 포트폴리오 종합 분석이 실행되지 않았습니다. 분석 실행 후 다시 추출하세요.`);
    }

    // ── 다운로드 ────────────────────────────────────────────
    const download = (content, filename) => {
      const blob = new Blob([content], { type: "text/markdown; charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      a.click();
      URL.revokeObjectURL(url);
    };

    download(lines1.join("\n"), `portfolio-${dateKey}.md`);
    // 브라우저 다운로드 차단 방지를 위해 살짝 딜레이
    setTimeout(() => download(lines2.join("\n"), `portfolio-analysis-${dateKey}.md`), 300);
  }, [holdingsKR, holdingsUS, fxRate, krwCash, usdCash, analyzedAt, portfolioAnalysis]);

  // ────────────────────────────────────────────────────────────
  // 핸들러
  // ────────────────────────────────────────────────────────────
  const handleSellOpen = (h, market) => setSellTarget({ h, market });
  const handleAdd = (record) => {
    if (addMarket === "kr") setHoldingsKR([record, ...holdingsKR]);
    else setHoldingsUS([record, ...holdingsUS]);
    setAddMarket(null);
  };
  const handleRemove = (id, market) => {
    if (market === "kr") setHoldingsKR(holdingsKR.filter((h) => h.id !== id));
    else setHoldingsUS(holdingsUS.filter((h) => h.id !== id));
  };
  const handleEditOpen = (h, market) => setEditTarget({ h, market });
  const handleEditSubmit = (updated) => {
    const m = editTarget.market;
    if (m === "kr") setHoldingsKR(holdingsKR.map((h) => h.id === updated.id ? updated : h));
    else setHoldingsUS(holdingsUS.map((h) => h.id === updated.id ? updated : h));
    setEditTarget(null);
  };
  const handleSellSubmit = (record) => {
    setSoldRecords([record, ...soldRecords]);
    if (record.market === "kr") setHoldingsKR(holdingsKR.filter((h) => h.code !== record.code));
    else setHoldingsUS(holdingsUS.filter((h) => h.code !== record.code));
    setSellTarget(null);
  };
  const handleSoldDelete = (id) => setSoldRecords(soldRecords.filter((r) => r.id !== id));

  const markRead      = (id) => setAlerts(alerts.map((a) => a.id === id ? { ...a, unread: false } : a));
  const markAllRead   = ()   => setAlerts(alerts.map((a) => ({ ...a, unread: false })));
  const deleteAllAlerts = () => setAlerts([]);

  // ────────────────────────────────────────────────────────────
  // 집계
  // ────────────────────────────────────────────────────────────
  const totalKRValue    = holdingsKR.reduce((s, h) => s + h.currentPrice * h.qty, 0);
  const totalUSValueUSD = holdingsUS.reduce((s, h) => s + h.currentPrice * h.qty, 0);
  const totalUSValueKRW = totalUSValueUSD * fxRate;
  const totalCashKRW    = krwCash + usdCash * fxRate;
  const grandTotal      = totalKRValue + totalUSValueKRW + totalCashKRW;

  // 분석 호출에 주입할 포트폴리오 요약 — KR/US 종합 비중 기준 (원화 환산)
  const buildPortfolioContext = () => {
    const total = grandTotal;
    const items = [
      ...holdingsKR.map((h) => {
        const v = h.currentPrice * h.qty;
        return { id: h.id, name: h.name, code: h.code, market: "kr", valueKRW: v, pct: total ? (v / total) * 100 : 0, eval: h.eval };
      }),
      ...holdingsUS.map((h) => {
        const v = h.currentPrice * h.qty * fxRate;
        return { id: h.id, name: h.name, code: h.code, market: "us", valueKRW: v, pct: total ? (v / total) * 100 : 0, eval: h.eval };
      })
    ];
    return { total, totalKR: totalKRValue, totalUS: totalUSValueKRW, totalCash: totalCashKRW, holdings: items };
  };

  const today = new Date();
  const dateStr = today.toLocaleDateString("ko-KR", { year: "numeric", month: "long", day: "numeric", weekday: "long" });
  const timeStr = today.toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" });
  const unreadCount = alerts.filter((a) => a.unread).length;
  const analyzedLabel = analyzedAt
    ? new Date(analyzedAt).toLocaleString("ko-KR", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" })
    : "미실행";

  const TABS = [
    { id: "dashboard", label: "대시보드" },
    { id: "journal",   label: "매매 일지" },
    { id: "alerts",    label: "알림" }
  ];

  return (
    <React.Fragment>
      <div className={"promo-banner" + (status.kind === "err" ? " warning" : "")}>
        <span className="pulse" />
        <span>
          {status.kind === "idle"
            ? `오늘의 LLM 포트폴리오 리뷰 · 마지막 분석 ${analyzedLabel} · 환율 ${fxRate.toFixed(2)}${fxAsOf ? ` (${fxAsOf})` : ""}`
            : status.msg}
        </span>
      </div>
      <nav className="top-nav">
        <div className="logo"><span className="dot" />포트폴리오 분석</div>
        <div className="pill-tabs">
          {TABS.map((p) => (
            <button
              key={p.id}
              className={"pill-tab " + (tab === p.id ? "active" : "")}
              onClick={() => setTab(p.id)}
            >
              {p.label}
              {p.id === "alerts" && unreadCount > 0 && (
                <span style={{
                  marginLeft: 6, fontSize: 11,
                  background: tab === p.id ? "rgba(255,255,255,0.22)" : "var(--primary)",
                  color: "var(--canvas)",
                  borderRadius: 100, padding: "1px 7px", fontWeight: 700
                }}>{unreadCount}</span>
              )}
            </button>
          ))}
        </div>
        <div className="right">
          <button
            className="btn btn-buy btn-sm"
            onClick={runAnalysis}
            disabled={loading.analyze}
            title="Claude로 보유 전 종목 + 브리핑 재분석"
          >
            {loading.analyze ? "분석 중..." : "분석 실행"}
          </button>
          <button
            className="btn btn-ghost btn-sm"
            onClick={exportMarkdown}
            title="예수금 + 포트폴리오를 마크다운으로 다운로드"
          >
            추출
          </button>
          <button
            className="btn btn-ghost btn-sm"
            onClick={() => { refreshFx(); refreshPrices(); }}
            disabled={loading.fx || loading.prices}
            title="환율 + 주가 다시 가져오기"
          >
            {(loading.fx || loading.prices) ? "갱신 중..." : "↻ 새로고침"}
          </button>
          <button
            className="icon-btn"
            onClick={() => setShowSettings(true)}
            title="API 키 / 모델 설정"
            aria-label="설정"
          >
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="3" />
              <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
            </svg>
          </button>
        </div>
      </nav>

      <main className="page">
        <div className="page-header">
          <h1 className="page-title">
            {tab === "dashboard" && <>오늘의 포트폴리오 <span className="subdued">리뷰</span></>}
            {tab === "journal"   && <>매매 <span className="subdued">일지</span></>}
            {tab === "alerts"    && <>판단 변동 <span className="subdued">알림</span></>}
          </h1>
          <div className="page-meta">
            <span className="date">{dateStr}</span>
            <span className="ts">현재 {timeStr} · 분석 기준 {analyzedLabel}</span>
            <span style={{ color: "var(--success)", fontWeight: 700 }}>
              ● Claude {shortModel(settings.model)}
            </span>
          </div>
        </div>

        {tab === "dashboard" && (
          <React.Fragment>
            {t.showBriefing && (
              <section className="section">
                <div className="section-head">
                  <h2><strong>장 시작 전</strong> 시장 브리핑</h2>
                  <span className="meta">
                    {briefing?.kr?.generatedAt
                      ? `갱신 ${new Date(briefing.kr.generatedAt).toLocaleString("ko-KR", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" })} · 매일 08:00 자동`
                      : "매일 08:00 자동 갱신"}
                  </span>
                  <button
                    className="btn btn-ghost btn-sm"
                    onClick={refreshBriefingOnly}
                    disabled={briefingLoading || loading.analyze}
                    title="지수 실시간 조회 + Claude 서사 즉시 갱신"
                  >
                    {briefingLoading ? "갱신 중..." : "↻ 브리핑 갱신"}
                  </button>
                </div>
                {briefing && briefing.kr && briefing.us ? (
                  <div className="briefing-grid">
                    <BriefingCard data={briefing.kr} flag="kr" />
                    <BriefingCard data={briefing.us} flag="us" />
                  </div>
                ) : (
                  <div className="pfa-empty" style={{ marginTop: 0 }}>
                    <p>분석 실행 또는 <strong>↻ 브리핑 갱신</strong> 버튼으로 시장 브리핑을 불러오세요.</p>
                    <button className="btn btn-ghost" onClick={refreshBriefingOnly} disabled={briefingLoading}>
                      {briefingLoading ? "갱신 중..." : "↻ 브리핑 갱신"}
                    </button>
                  </div>
                )}
              </section>
            )}

            <section className="section">
              <div className="section-head">
                <h2><strong>예수금</strong> 현황</h2>
                <span className="meta">총 자산 ₩{Math.round(grandTotal).toLocaleString("ko-KR")}</span>
              </div>
              <CashBar krw={krwCash} usd={usdCash} fxRate={fxRate} setKrw={setKrwCash} setUsd={setUsdCash} fxAsOf={fxAsOf} />
            </section>

            <section className="section">
              <div className="section-head">
                <h2><strong>국내</strong> 포트폴리오</h2>
                <span className="meta">{pricesAsOf ? `주가 갱신 ${new Date(pricesAsOf).toLocaleString("ko-KR", { hour: "2-digit", minute: "2-digit" })}` : "행을 클릭하면 LLM 분석 근거를 펼쳐볼 수 있습니다"}</span>
              </div>
              <PortfolioTable
                title="🇰🇷 국내 보유 종목" market="kr"
                holdings={holdingsKR}
                expandedId={expandedKR} setExpandedId={setExpandedKR}
                onSell={handleSellOpen}
                onAdd={() => setAddMarket("kr")}
                onRemove={handleRemove}
                onEdit={handleEditOpen}
                onReanalyze={reanalyzeOne}
                onLoadHistory={loadHistory}
                historyLoading={historyLoading}
                reanalyzingIds={reanalyzingIds}
              />
            </section>

            <section className="section">
              <div className="section-head">
                <h2><strong>미국</strong> 포트폴리오</h2>
                <span className="meta">USD 기준 · 환산 시 환율 ₩{fxRate.toLocaleString("ko-KR")} 적용</span>
              </div>
              <PortfolioTable
                title="🇺🇸 미국 보유 종목" market="us"
                holdings={holdingsUS}
                expandedId={expandedUS} setExpandedId={setExpandedUS}
                onSell={handleSellOpen}
                onAdd={() => setAddMarket("us")}
                onRemove={handleRemove}
                onEdit={handleEditOpen}
                onReanalyze={reanalyzeOne}
                onLoadHistory={loadHistory}
                historyLoading={historyLoading}
                reanalyzingIds={reanalyzingIds}
              />
            </section>

            <section className="section">
              <div className="section-head">
                <h2><strong>포트폴리오</strong> 종합 분석</h2>
                <span className="meta"></span>
              </div>
              <PortfolioAnalysisCard
                analysis={portfolioAnalysis}
                onRun={runPortfolioAnalysis}
                loading={loading.pfAnalyze}
              />
            </section>
          </React.Fragment>
        )}

        {tab === "journal" && (
          <section className="section">
            <div className="section-head">
              <h2><strong>매매</strong> 일지</h2>
              <span className="meta">실현 손익 + 매도 사유 아카이브 · 총 {soldRecords.length}건</span>
            </div>
            <SoldRecords records={soldRecords} onDelete={handleSoldDelete} />
          </section>
        )}

        {tab === "alerts" && (
          <AlertsPage alerts={alerts} onMarkRead={markRead} onMarkAllRead={markAllRead} onDeleteAll={deleteAllAlerts} />
        )}

        <footer className="footer">
          본 페이지는 LLM이 자동 생성하는 분석 템플릿입니다 · 투자 조언이 아닙니다 · 최종 투자 판단은 본인 책임
        </footer>
      </main>

      {sellTarget && (
        <SellModal
          holding={sellTarget.h} market={sellTarget.market}
          onClose={() => setSellTarget(null)} onSubmit={handleSellSubmit}
        />
      )}
      {addMarket && (
        <AddHoldingModal
          market={addMarket}
          onClose={() => setAddMarket(null)} onSubmit={handleAdd}
        />
      )}
      {editTarget && (
        <EditHoldingModal
          market={editTarget.market}
          initial={editTarget.h}
          onClose={() => setEditTarget(null)}
          onSubmit={handleEditSubmit}
        />
      )}
      {showSettings && (
        <SettingsModal
          settings={settings}
          onClose={() => setShowSettings(false)}
          onSave={(next) => { setSettings(next); setShowSettings(false); }}
          onClear={() => {
            if (confirm("저장된 모든 데이터(보유·예수금·매매일지·알림·설정)를 초기화하시겠습니까?")) {
              ["holdings.kr", "holdings.us", "soldRecords", "alerts", "briefing",
               "cash.krw", "cash.usd", "fx.rate", "fx.asOf", "prices.asOf",
               "analysis.at", "settings", "portfolio.analysis"].forEach((k) => window.storage.remove(k));
              location.reload();
            }
          }}
        />
      )}

      {/* 모바일 하단 탭바 */}
      <nav className="mobile-bottom-bar">
        <div className="mob-tabs">
          {TABS.map((p) => (
            <button key={p.id} className={"mobile-tab-btn " + (tab === p.id ? "active" : "")} onClick={() => setTab(p.id)}>
              {p.id === "alerts" && unreadCount > 0 && <span className="mob-badge">{unreadCount}</span>}
              <span className="mobile-tab-label">{p.label}</span>
            </button>
          ))}
          <button className="mobile-tab-btn" onClick={runAnalysis} disabled={loading.analyze}>
            <span className="mobile-tab-label">{loading.analyze ? "분석중" : "분석"}</span>
          </button>
          <button className="mobile-tab-btn" onClick={() => setShowSettings(true)}>
            <span className="mobile-tab-label">설정</span>
          </button>
        </div>
      </nav>

      <TweaksPanel title="Tweaks">
        <TweakSection title="강조 색상" subtitle="commerce-flow 강조에 사용되는 cobalt 변경">
          <TweakColor
            label="Accent" value={t.accent}
            options={["#0064E0", "#1d9856", "#5e2cb8", "#0a1317"]}
            onChange={(v) => setTweak("accent", v)}
          />
        </TweakSection>
        <TweakSection title="레이아웃">
          <TweakRadio
            label="브리핑 표시"
            value={t.showBriefing ? "on" : "off"}
            options={[{ label: "표시", value: "on" }, { label: "숨김", value: "off" }]}
            onChange={(v) => setTweak("showBriefing", v === "on")}
          />
        </TweakSection>
      </TweaksPanel>
    </React.Fragment>
  );
}

function capEval(v) {
  return ({ hold: "Hold", trim: "Trim", sell: "Sell", add: "Add" })[v] || v;
}
function shortModel(m) {
  if (!m) return "Haiku";
  if (m.includes("haiku")) return "Haiku 4.5";
  if (m.includes("sonnet")) return "Sonnet";
  if (m.includes("opus")) return "Opus";
  return m;
}

window.storageReady.then(() => {
  ReactDOM.createRoot(document.getElementById("root")).render(<App />);
});
