import * as vscode from 'vscode';
import { logInfo } from '../../utils';
import { readStocksStore, addStock, removeStock, getStocksFilePath } from './stocksStore';
import { fetchAllPrices, fetchYahooPrice, fetchYahooHistory, evalMa224Recovery, evalDropRecovery, evalMaAlignment, evalRsi14, type Ma224RecoveryResult, type DropRecoveryResult, type MaAlignmentResult, type Rsi14Result } from './yahooClient';
import type { Fundamentals } from './naverFundamentals';
import { fetchFundamentalsRouted } from './fundamentalsRouter';
import { detectMarket, formatMoney, formatMarketCapKR, formatMarketCapUS, type StockMarket } from './marketUtils';
import { AIService } from '../../core/services';
import { classifyAll } from './signalClassifier';
import { writeStocksStore } from './stocksStore';
import { syncToSheets } from './sheetsSync';
import { judgeStock } from './llmJudge';
import { sendStocksReport, buildReportText } from './telegramReport';
import { runOnceNow } from './stocksWatcher';
import { discoverStocks } from './stockDiscovery';
import { resolveSectorQuery, listSectorKeys, type SectorGroup } from './stockSectors';
import { analyzeTopCandidates, renderTopFiveForChat, renderTopFiveForTelegram, sendTopFiveToTelegram } from './discoveryAnalyzer';
import type { ClassifiedStock, Stock } from './types';

/**
 * `/stocks <subcommand> [args]` 라우터 — slashRouter 의 단일 handler 로 등록되어
 * 첫 단어로 sub-routing.
 *
 * Subcommands:
 *   /stocks                       — 도움말
 *   /stocks list                  — 종목 + 신호 표시
 *   /stocks check                 — 현재가 갱신 (Yahoo)
 *   /stocks signal                — 매수사정권 종목만
 *   /stocks sync                  — Google Sheets 동기화
 *   /stocks add <심볼> <이름> [투자성향]
 *   /stocks remove <심볼>
 *   /stocks judge <심볼>           — LLM 4-criteria 평가
 *   /stocks discover [min] [max]  — Naver 크롤 발굴 (시총 범위 억 단위, default 1000-5000)
 *   /stocks discover sector <섹터> [min] [max] — 섹터별 발굴 / discover sectors — 섹터 목록
 *   /stocks report                — 텔레그램 보고서 즉시 발송
 *   /stocks run                   — watcher 한 사이클 즉시 실행 (현재가+sync+보고서)
 *   /stocks path                  — stocks.json 경로 표시
 */

interface Webview { postMessage(msg: any): Thenable<boolean> | boolean; }

function chunk(view: Webview | undefined, value: string) {
    view?.postMessage({ type: 'streamChunk', value });
}

function renderListLine(s: ClassifiedStock): string {
    const mkt: StockMarket = s.market ?? detectMarket(s.심볼);
    const cur = typeof s.현재가 === 'number' ? formatMoney(s.현재가, mkt) : '-';
    const rec = s.매수권장가 ?? '-';
    const flag = mkt === 'US' ? '🇺🇸 ' : '';
    return `  · ${flag}${s.이름} (${s.심볼}): ${cur} / 권장 ${rec} → ${s.signalText}`;
}

async function cmdList(view: Webview | undefined): Promise<void> {
    const store = readStocksStore();
    if (store.length === 0) {
        chunk(view, `\n종목 없음. \`/stocks add <심볼> <이름>\` 으로 추가하세요.\n경로: ${getStocksFilePath() ?? '(워크스페이스 없음)'}\n`);
        return;
    }
    const g = classifyAll(store);
    const lines: string[] = ['\n📋 **종목 목록 (분류별)**\n'];
    if (g.swing.length) {
        lines.push(`\n**스윙/중기** (${g.swing.length}개)`);
        g.swing.forEach(s => lines.push(renderListLine(s)));
    }
    if (g.long.length) {
        lines.push(`\n**장기투자** (${g.long.length}개)`);
        g.long.forEach(s => lines.push(renderListLine(s)));
    }
    if (g.ultraLow.length) {
        lines.push(`\n**저평가우량주** (${g.ultraLow.length}개)`);
        g.ultraLow.forEach(s => lines.push(renderListLine(s)));
    }
    chunk(view, lines.join('\n') + '\n');
}

async function cmdCheck(view: Webview | undefined): Promise<void> {
    const store = readStocksStore();
    if (store.length === 0) { chunk(view, '\n종목 없음.\n'); return; }
    chunk(view, `\n🔄 ${store.length}개 종목 현재가 갱신 중 (Yahoo, 1초/종목)...\n`);
    const symbols = store.map(s => s.심볼).filter(Boolean);
    const prices = await fetchAllPrices(symbols, (sym, p) => {
        const name = store.find(s => s.심볼 === sym)?.이름 ?? sym;
        chunk(view, `  · ${name}: ${p !== null ? formatMoney(p, detectMarket(sym)) : '조회 실패'}\n`);
    });
    for (const s of store) {
        const p = prices.get(s.심볼);
        if (typeof p === 'number') s.현재가 = p;
    }
    writeStocksStore(store);
    const updated = [...prices.values()].filter(p => p !== null).length;
    chunk(view, `\n✅ ${updated}/${store.length}개 종목 갱신 완료.\n`);
}

async function cmdSignal(view: Webview | undefined): Promise<void> {
    const store = readStocksStore();
    const g = classifyAll(store);
    const buyZone = g.all.filter(s => s.signal === 'BUY_ZONE');
    if (buyZone.length === 0) {
        chunk(view, '\n🚨 매수사정권 종목 없음.\n');
        return;
    }
    chunk(view, `\n🚨 **매수사정권 ${buyZone.length}개**\n\n`);
    for (const s of buyZone) {
        chunk(view, renderListLine(s) + '\n');
    }
}

async function cmdSync(view: Webview | undefined, context: vscode.ExtensionContext): Promise<void> {
    chunk(view, '\n📊 Google Sheets 동기화 중...\n');
    const r = await syncToSheets(context);
    if (r.ok) {
        chunk(view, `\n✅ 동기화 완료: ${r.updatedRanges.length}개 시트.\n${r.updatedRanges.map(x => `  · ${x}`).join('\n')}\n`);
    } else {
        chunk(view, `\n❌ 동기화 실패:\n${r.errors.map(e => `  · ${e}`).join('\n')}\n`);
    }
}

async function cmdAdd(arg: string, view: Webview | undefined): Promise<void> {
    const parts = arg.split(/\s+/);
    if (parts.length < 2) {
        chunk(view, '\n사용법: `/stocks add <심볼> <이름> [투자성향]`\n  투자성향: 스윙/중기 | 장기투자 | 저평가우량주 (기본: 스윙/중기)\n');
        return;
    }
    const [symbolRaw, name, profileRaw] = parts;
    // 미국 티커는 대문자 정규화(aapl→AAPL), 한국 코드는 그대로. 시장은 심볼로 자동 판별해 저장.
    const market = detectMarket(symbolRaw);
    const symbol = market === 'US' ? symbolRaw.toUpperCase() : symbolRaw;
    const profile = (profileRaw as Stock['투자성향']) || '스윙/중기';
    const r = addStock({ 이름: name, 심볼: symbol, market, 투자성향: profile });
    chunk(view, r.ok ? `\n✅ 추가: ${market === 'US' ? '🇺🇸 ' : ''}${name} (${symbol}, ${profile})\n` : `\n❌ ${r.reason}\n`);
}

async function cmdRemove(arg: string, view: Webview | undefined): Promise<void> {
    if (!arg.trim()) { chunk(view, '\n사용법: `/stocks remove <심볼>`\n'); return; }
    const r = removeStock(arg.trim());
    chunk(view, r.ok ? `\n✅ 제거: ${arg.trim()}\n` : `\n❌ ${r.reason}\n`);
}

async function cmdJudge(arg: string, view: Webview | undefined): Promise<void> {
    if (!arg.trim()) { chunk(view, '\n사용법: `/stocks judge <심볼>`\n'); return; }
    const symbol = arg.trim();
    const srcName = detectMarket(symbol) === 'US' ? 'Yahoo' : 'Naver';
    // 저장값은 분기 실적 이후 stale 할 수 있어 판정 전에 실시간 수치를 시도(한국=Naver, 미국=Yahoo).
    // 실패하면 stocks.json 저장값으로 폴백(결과에 데이터 출처 표기).
    chunk(view, `\n📡 ${srcName} 펀더멘털 갱신 중: ${symbol}...\n`);
    let fresh: Fundamentals | undefined;
    try {
        fresh = (await fetchFundamentalsRouted(symbol)) ?? undefined;
    } catch { /* 폴백 — 저장값 사용 */ }
    chunk(view, fresh ? '✅ 실시간 수치 확보\n' : '⚠️ 실시간 조회 실패 — 저장값으로 평가\n');
    chunk(view, `🤖 필터 평가 중 (수치 판정=코드, 근거 서술=LLM)...\n`);
    const r = await judgeStock(symbol, { fresh });
    if (!r.ok) {
        chunk(view, `\n❌ 평가 실패: ${r.error}\n`);
        return;
    }
    chunk(view, `\n✅ 평가 완료: ${r.filterText}\n📅 데이터: ${r.dataSource}\n`);
    if (r.rationale) chunk(view, `\n근거:\n${r.rationale}\n`);
}

// ─── /stocks analysis <심볼> — 단일 종목 심층 분석 (judge 보다 깊음) ───
// judge: 4-criteria 펀더멘털 평가만 (stocks.json 에 저장된 데이터 사용)
// analysis: Naver 펀더멘털 fresh fetch + Yahoo 1년 시세 기반 기술 지표(224회복/낙폭과대)
//           + LLM 종합 평가 (권장도/근거/리스크). stocks.json 에 종목이 없어도 동작.

const ANALYSIS_SYSTEM_PROMPT = [
    '당신은 한국·미국 주식 종합 평가 보조 도구다. 사용자가 제공한 한 종목의 *펀더멘털*',
    '(한국=Naver, 미국=Yahoo quoteSummary) + *Yahoo 1년 시세 기반 기술 지표* 를 보고 종합 평가를 내린다.',
    '외부 정보 추측 금지 — 주어진 데이터에서만 판단.',
    '',
    '**통화·시장 규칙:** 컨텍스트 첫 줄의 (시장/통화) 표기를 따른다. 한국=원, 미국=달러($).',
    '가격·타점을 쓸 때 그 통화 기호를 그대로 사용하고 단위를 섞지 말 것.',
    '미국 종목은 **유보율(retention ratio)을 보고하지 않는다 — "N/A"는 부정 신호가 아니다.**',
    '미국의 재무 안정성은 부채비율(D/E)·시가총액으로만 판단하고, 유보율 부재를 약점으로 적지 말 것.',
    '',
    '**평가 6차원:**',
    '  1. 가치 — PBR / PER (저평가 여부)',
    '  2. 수익성 — ROE / 영업이익률 절대 수준',
    '  3. 재무 안정성 — 유보율, 시가총액, **부채비율** (200% 이하 안전, 100% 이하 우량)',
    '  4. 추세 (기술) — 224일선 회복 + **MA 정배열/역배열** (5/20/60/120일)',
    '       · 정배열 = 추세 매수 안전',
    '       · 역배열 = 펀더 좋아도 대기 권장',
    '       · MA224 "N/A — 추세 확립" 상태는 **부정 신호 아님** (현재가가 MA224 한참 위라 회복 패턴 평가 무관). passed:false 와 혼동 금지.',
    '  5. 안전마진 — 낙폭과대 (1년 고점 대비 하락 + 최근 저점에서 반등). raw 데이터의 `failReason` 가 명시되면 그것을 그대로 인용 — 인과 거꾸로(예: "반등이 커서 미통과" 같은 오해석) 금지.',
    '  6. 진입 타이밍 — **RSI(14)**',
    '       · 과열 (≥70) = 매수 자제',
    '       · 침체 (≤30) = 단기 반등 여지',
    '       · 중립 (30~70) = 정상',
    '',
    '**판단 절제 규칙 — 과잉 해석 차단:**',
    '  a. **PBR/PER 절대값으로 "고평가/저평가" 단정 금지.** 업종 평균 데이터가 컨텍스트에 없으면 "PBR X.X (업종 평균 데이터 부재 — 절대값만 제시)" 식으로만. "부담" / "고평가" 같은 강한 단어는 업종 비교나 5년 역사적 평균이 있을 때만 사용.',
    '  b. **거래량 ±20% 미만 변동은 무의미.** 5일/60일 평균 차이가 약 -10% 정도면 "유의미한 변화 아님"으로 처리하고 "상승 동력 약화" 같은 강한 해석으로 끌고 가지 말 것. 진짜 약화 신호는 -20% 이상 또는 추세적 감소.',
    '  c. **우선주 (symbol 끝자리 5/7/9) 는 특이 항목 반영 필수.** 컨텍스트에 `[우선주 정보]` 블록이 있으면 보통주 대비 할인율을 매수 의견 근거에 한 줄 언급. 우선주는 일반적으로 보통주 대비 10~30% 할인이 정상 — 그 범위 내면 "정상", 좁으면 "프리미엄", 넓으면 "확대" 로 평가. 배당수익률 데이터는 현재 미수집이므로 추측 금지.',
    '  d. **오탈자 자기 점검.** 출력 직전 한국어 토큰 깨짐(예: "순위" → "순칭", "근거" → "근례")이 없는지 확인. 의심스러우면 다시 쓸 것.',
    '',
    '**출력 형식 (정확히 이대로, 다른 헤더 추가 금지):**',
    '## 종합 평가',
    '2-3 문장 핵심 의견.',
    '',
    '## 매수 의견',
    '- **권장도**: 매수권 / 관망 / 회피 중 하나',
    '- **근거**: 위 6차원 중 핵심 2-3개를 *수치 인용* 으로',
    '',
    '## 매매 타점',
    '*가격·% 모두 raw 데이터(MA값, 1년 최고가, 60일 저점, RSI, 부채비율 등)에서 직접 도출. 추측·외부 정보 금지. `__원` 같은 placeholder가 아니라 실제 수치로 채울 것.*',
    '',
    '### 매수 진입 타점',
    '권장도가 **회피**면 이 sub-section 통째로 한 줄: "회피 권장 — 매수 타점 산정 생략."',
    '권장도가 매수권/관망이면 *현재 기술 상태에 부합하는 시나리오만* 1-2개:',
    '- **1순위 — MA20 지지**: MA 정배열일 때만. MA20 ±1% 영역(가격 범위 X원~Y원). 거래량 감소 후 반등 캔들 확인.',
    '- **2순위 — MA60 눌림**: 단기 조정 심화 시. MA60 부근(Z원). 낙폭과대 동시 충족 시 적극.',
    '- **3순위 — 낙폭과대 + RSI ≤30**: RSI 30 이하 + 60일 저점(W원) 근접. 부채비율·유보율 이상 없을 것.',
    '현재 데이터로 어떤 시나리오도 부합 안 하면 한 줄: "현재 매수 타점 조건 부적합 — 추가 조정 대기."',
    '',
    '### 손절 기준',
    '**종가 기준 이탈**만 적용 (장중 터치는 손절 미해당). *위 매수 시나리오에 출력한 항목에 한해서만:*',
    '- MA20 진입 시 → MA20(X원) 종가 이탈, 최대 허용 약 -5%',
    '- MA60 진입 시 → MA60(X원) 종가 이탈, 최대 허용 약 -8%',
    '- 낙폭과대 진입 시 → 60일 저점(X원) 종가 이탈, 최대 허용 약 -10%',
    '',
    '### 익절 타점',
    '- **1차**: 1년 최고가 × 0.98 (가격 X원), 보유 비중 30~50% 축소',
    '- **2차**: 다음 심리적 라운드 넘버(한국: 현재가 위 가장 가까운 10,000/50,000/100,000원 단위 / 미국: $10/$50/$100/$1000 단위), 잔량 추가 축소',
    '- **3차**: 동종 업종 평균 PER 대비 20% 초과 시 전량 익절 고려. *업종 평균 PER 데이터는 현재 미수집 — 사용자가 수동 비교 필요.*',
    '',
    '### 관망 해제 트리거',
    '권장도가 **관망**일 때만 작성. 매수권/회피면 이 sub-section 통째로 생략(line drop).',
    '아래 *2개 이상 동시 충족* 시 진입 전환:',
    '1. MA20 또는 MA60 종가 지지 확인',
    '2. 거래량 60일 평균 (현재 약 X) 이하 감소 후 반등',
    '3. RSI 50 이하로 눌린 후 재반등 (현재 RSI Y)',
    '4. 1년 최고가 대비 -10% 이상 추가 조정',
    '',
    '## 리스크',
    '1-2줄, 주의해야 할 점.',
].join('\n');

/**
 * 한국 우선주 → 보통주 심볼 도출. 우선주 마지막 자리는 5/7/9 (1종/2종/3종 우선주).
 * 보통주는 마지막 자리 0. 예: 005935 → 005930, 005385 → 005380.
 * 패턴이 아니면 null (보통주거나 다른 형식).
 */
function deriveCommonStockSymbol(symbol: string): string | null {
    if (!/^[0-9]{6}$/.test(symbol)) return null;
    const last = symbol[symbol.length - 1];
    if (last === '5' || last === '7' || last === '9') {
        return symbol.slice(0, -1) + '0';
    }
    return null;
}

export interface PreferredStockInfo {
    /** 우선주 → 보통주로 환산한 6자리 심볼. */
    commonSymbol: string;
    /** Yahoo 에서 가져온 보통주 현재가. */
    commonPrice: number;
    /** 우선주 현재가. */
    preferredPrice: number;
    /** (보통주 - 우선주) / 보통주 × 100. 양수 = 우선주가 더 쌈(할인), 음수 = 프리미엄. */
    discountPct: number;
}

function buildAnalysisContext(
    symbol: string,
    f: Fundamentals,
    ma224: Ma224RecoveryResult | null,
    drop: DropRecoveryResult | null,
    align: MaAlignmentResult | null,
    rsi: Rsi14Result | null,
    preferred: PreferredStockInfo | null,
): string {
    const mkt: StockMarket = f.market ?? detectMarket(symbol);
    const money = (n?: number) => n !== undefined ? formatMoney(n, mkt) : '-';
    const capText = mkt === 'US' ? formatMarketCapUS(f.marketCapNative) : formatMarketCapKR(f.marketCapEok);
    const lines: string[] = [
        `종목 심볼: ${symbol}  (시장: ${mkt === 'US' ? '미국 🇺🇸' : '한국'}, 통화: ${f.currency ?? (mkt === 'US' ? 'USD' : 'KRW')})`,
        `섹터: ${f.sectorHint ?? '-'}`,
        '',
        `── ${f.source ?? (mkt === 'US' ? 'Yahoo quoteSummary' : 'Naver')} 펀더멘털 ──`,
        `시가총액: ${capText}`,
        `ROE: ${f.roe !== undefined ? f.roe.toFixed(2) + '%' : '-'}`,
        `영업이익률: ${f.operatingMargin !== undefined ? f.operatingMargin.toFixed(1) + '%' : '-'}`,
        // 유보율은 한국 전용 지표 — 미국은 미보고(부정 신호 아님). 부채비율(D/E)로 안정성 판단.
        mkt === 'US'
            ? '유보율: N/A (미국 미보고 — 부정 신호 아님. 안정성은 부채비율로 판단)'
            : `유보율: ${f.retentionRatio !== undefined ? Math.round(f.retentionRatio).toLocaleString() + '%' : '-'}`,
        `부채비율${mkt === 'US' ? '(D/E)' : ''}: ${f.debtRatio !== undefined ? f.debtRatio.toFixed(1) + '%' : '-'}`,
        `PER: ${f.per !== undefined ? f.per.toFixed(1) : '-'}`,
        `PBR: ${f.pbr !== undefined ? f.pbr.toFixed(2) : '-'}`,
        `현재가: ${money(f.currentPrice)}`,
        '',
        '── Yahoo 1년 기술 지표 ──',
    ];
    if (ma224) {
        const ma = money(ma224.ma224Today);
        const cp = money(ma224.currentPrice);
        if (ma224.notApplicable) {
            // N/A 상태 — passed:false 와 시각적으로 구분 (⚪ vs ❌). LLM 도 이걸 보고 음성 신호 아님으로 인지.
            lines.push(
                `224일선(MA224) 회복: ⚪ N/A — ${ma224.notApplicableReason ?? '추세 확립, 회복 패턴 평가 무관'}`,
                `  · 현재가 ${cp} vs MA224 ${ma}`,
                '  · ⚠️ 부정 신호가 아닙니다 — 회복 패턴은 *장기 하락 후 상향 돌파* 신호이므로, 이미 한참 위에 있는 종목엔 적용 무관.',
            );
        } else {
            lines.push(
                `224일선(MA224) 회복: ${ma224.passed ? '✅ 통과' : '❌ 미통과'}`,
                `  · 현재가 ${cp} vs MA224 ${ma}`,
                `  · 최근 30일 중 MA224 아래 일수: ${ma224.daysBelowLast30 ?? '-'}`,
                `  · 거래량 확인: 5일평균 ${ma224.vol5dAvg !== undefined ? Math.round(ma224.vol5dAvg).toLocaleString() : '-'} vs 60일평균 ${ma224.vol60dAvg !== undefined ? Math.round(ma224.vol60dAvg).toLocaleString() : '-'} → ${ma224.volumeConfirmed ? '✅' : '❌'}`,
            );
        }
    } else {
        lines.push('224일선 분석: 시세 데이터 부족 (224일 미만)');
    }
    if (drop) {
        const cp = money(drop.currentPrice);
        const hi = money(drop.high1y);
        const lo = money(drop.low60d);
        const fromHigh = drop.fromHighRatio !== undefined ? `${(drop.fromHighRatio * 100).toFixed(1)}% 수준` : '-';
        const fromLow = drop.fromLowRatio !== undefined ? `+${((drop.fromLowRatio - 1) * 100).toFixed(1)}% 반등` : '-';
        const verdict = drop.passed
            ? '✅ 통과'
            : `❌ 미통과${drop.failReason ? ` — ${drop.failReason}` : ''}`;
        lines.push(
            `낙폭과대: ${verdict}`,
            `  · 1년 최고가 ${hi} → 현재가 ${cp} (${fromHigh})`,
            `  · 60일 저점 ${lo} → 현재가 ${cp} (${fromLow})`,
        );
    } else {
        lines.push('낙폭과대 분석: 시세 데이터 부족 (60일 미만)');
    }
    if (align) {
        lines.push(
            `MA 배열: ${align.alignment === '정배열' ? '✅ 정배열' : align.alignment === '역배열' ? '❌ 역배열' : '⚠️ 혼조'}`,
            `  · MA5 ${money(align.ma5)} / MA20 ${money(align.ma20)} / MA60 ${money(align.ma60)} / MA120 ${money(align.ma120)}`,
        );
    } else {
        lines.push('MA 배열: 시세 데이터 부족 (120일 미만)');
    }
    if (rsi) {
        const tag = rsi.classification === '과열' ? '🔥 과열' : rsi.classification === '침체' ? '🧊 침체' : '🟢 중립';
        lines.push(`RSI(14): ${rsi.rsi.toFixed(1)} (${tag})`);
    } else {
        lines.push('RSI: 시세 데이터 부족 (15일 미만)');
    }
    if (preferred) {
        const sign = preferred.discountPct > 0 ? '할인' : '프리미엄';
        // 우선주 할인 범위 가이드: 통상 10~30% 정상, 좁으면 프리미엄, 넓으면 확대.
        const rangeNote = preferred.discountPct >= 30 ? '확대 (보통주 대비 매우 저평가)'
            : preferred.discountPct >= 10 ? '정상 범위 (10~30%)'
            : preferred.discountPct > 0 ? '좁음 (10% 미만 — 보통주 대비 프리미엄 수준)'
            : '역전 (우선주가 보통주보다 비쌈 — 극히 이례적)';
        lines.push(
            '',
            '── 우선주 특이정보 ──',
            `보통주 ${preferred.commonSymbol} 현재가: ${preferred.commonPrice.toLocaleString()}원`,
            `우선주 ${symbol} 현재가: ${preferred.preferredPrice.toLocaleString()}원`,
            `보통주 대비 ${sign}율: ${Math.abs(preferred.discountPct).toFixed(1)}% — ${rangeNote}`,
            '※ 우선주는 배당수익률 + 보통주 대비 할인율이 핵심 투자 포인트. 배당 데이터는 미수집(사용자 확인 필요).',
        );
    }
    return lines.join('\n');
}

async function cmdAnalysis(arg: string, view: Webview | undefined): Promise<void> {
    const symbol = (arg.trim().split(/\s+/)[0] || '').trim();
    if (!symbol) {
        chunk(view, '\n사용법: `/stocks analysis <심볼>` — 펀더멘털 + 1년 차트 패턴 종합 분석 (judge보다 깊음). stocks.json 미등록 종목도 가능.\n');
        return;
    }
    const mkt: StockMarket = detectMarket(symbol);
    const fundSrc = mkt === 'US' ? 'Yahoo quoteSummary' : 'Naver';
    chunk(view, `\n🔍 **${mkt === 'US' ? '🇺🇸 ' : ''}${symbol} 심층 분석** — ${fundSrc} 펀더멘털 + Yahoo 1년 시세 + LLM 종합...\n`);

    // 1. 펀더멘털 (한국=Naver, 미국=Yahoo quoteSummary)
    const f = await fetchFundamentalsRouted(symbol);
    if (!f) {
        chunk(view, mkt === 'US'
            ? '\n❌ Yahoo 에서 펀더멘털 데이터를 못 가져왔습니다. 티커를 확인해주세요(예: AAPL, MSFT). Yahoo crumb 일시 차단일 수도 있으니 잠시 후 재시도.\n'
            : '\n❌ Naver 에서 펀더멘털 데이터를 못 가져왔습니다. 심볼을 확인해주세요(코스피/코스닥 6자리).\n');
        return;
    }
    chunk(view, `  · ${fundSrc} 펀더멘털 OK\n`);

    // 2. Yahoo 1년 시세 + 기술 지표
    const history = await fetchYahooHistory(symbol);
    const ma224 = history ? evalMa224Recovery(history) : null;
    const drop = history ? evalDropRecovery(history) : null;
    const align = history ? evalMaAlignment(history) : null;
    const rsi = history ? evalRsi14(history) : null;
    chunk(view, history
        ? `  · Yahoo 1년 시세 OK (${history.closes.length} 거래일)\n`
        : '  ⚠️ Yahoo 시세 fetch 실패 — 펀더멘털만으로 분석\n');

    // 2-b. 우선주(symbol 끝자리 5/7/9) 면 보통주 현재가도 가져와 할인율 계산.
    //      배당 데이터까지는 아직 미수집 — 추후 Naver crawl 확장 필요.
    let preferred: PreferredStockInfo | null = null;
    const commonSymbol = deriveCommonStockSymbol(symbol);
    if (commonSymbol && f.currentPrice !== undefined && f.currentPrice > 0) {
        chunk(view, `  · 우선주 감지 — 보통주 ${commonSymbol} 현재가 fetch 중...\n`);
        const commonPrice = await fetchYahooPrice(commonSymbol);
        if (typeof commonPrice === 'number' && commonPrice > 0) {
            preferred = {
                commonSymbol,
                commonPrice,
                preferredPrice: f.currentPrice,
                discountPct: (commonPrice - f.currentPrice) / commonPrice * 100,
            };
            chunk(view, `  · 보통주 ${commonSymbol} 현재가 ${commonPrice.toLocaleString()}원 (할인율 ${preferred.discountPct.toFixed(1)}%)\n`);
        } else {
            chunk(view, `  ⚠️ 보통주 ${commonSymbol} 가격 fetch 실패 — 할인율 계산 skip\n`);
        }
    }

    // 3. 데이터 요약 표시 (모델에 보내는 것과 동일 — 투명성)
    const summary = buildAnalysisContext(symbol, f, ma224, drop, align, rsi, preferred);
    chunk(view, `\n\`\`\`\n${summary}\n\`\`\`\n`);

    // 4. LLM 종합 분석
    chunk(view, '\n🤖 LLM 종합 분석 중...\n');
    const ai = new AIService();
    try {
        const result = await ai.chat({
            system: ANALYSIS_SYSTEM_PROMPT,
            user: summary + '\n\n위 데이터로 종합 평가, 매수 의견, 리스크를 출력 형식 그대로 작성하시오.',
        });
        if (result.empty || !result.content.trim()) {
            chunk(view, '\n❌ LLM 빈 응답.\n');
            return;
        }
        chunk(view, `\n${result.content.trim()}\n`);
        logInfo('Stocks analysis 완료.', { symbol, model: result.model });
    } catch (e: any) {
        chunk(view, `\n❌ LLM 호출 실패: ${e?.message ?? String(e)}\n`);
    }
}

// ─── /stocks position [심볼] <총자산> <리스크%> <손절%> — 포지션 사이징 ───
// 공식: 권장 투자금 = 총자산 × (리스크%/100) ÷ (손절%/100)
// 심볼을 주면 Yahoo 현재가로 매수 가능 주수까지 계산.

async function cmdPosition(arg: string, view: Webview | undefined): Promise<void> {
    const parts = arg.trim().split(/\s+/).filter(Boolean);
    if (parts.length < 3) {
        chunk(view, [
            '\n사용법:',
            '  `/stocks position <총자산> <리스크%> <손절%>` — 단순 계산',
            '  `/stocks position <심볼> <총자산> <리스크%> <손절%>` — + 현재가로 매수 가능 주수',
            '',
            '예: `/stocks position 50000000 2 5` → 50M × 2% ÷ 5% = 20M',
            '예: `/stocks position 019010 50000000 2 5` → 20M / 현재가 = N주 (한국)',
            '예: `/stocks position AAPL 100000 2 5` → 미국 티커 ($ 표기)',
            '',
        ].join('\n'));
        return;
    }
    // 첫 인자가 종목 심볼이면(6자리 코드 또는 알파벳 티커) 분리, 아니면 곧바로 숫자 인자.
    let symbol: string | undefined;
    let nums: string[];
    if (/^[0-9]{6}$/.test(parts[0]) || /^[A-Za-z][A-Za-z.\-]*$/.test(parts[0])) {
        symbol = /^[A-Za-z]/.test(parts[0]) ? parts[0].toUpperCase() : parts[0];
        nums = parts.slice(1);
    } else {
        nums = parts;
    }
    const mkt: StockMarket = symbol ? detectMarket(symbol) : 'KR';
    if (nums.length < 3) {
        chunk(view, '\n❌ 인자 부족 — 총자산, 리스크%, 손절% 3개 필요.\n');
        return;
    }
    const total = Number(nums[0]);
    const riskPct = Number(nums[1]);
    const stopPct = Number(nums[2]);
    if (!Number.isFinite(total) || !Number.isFinite(riskPct) || !Number.isFinite(stopPct)
        || total <= 0 || riskPct <= 0 || stopPct <= 0) {
        chunk(view, '\n❌ 잘못된 입력 — 모두 양수여야 합니다.\n');
        return;
    }
    if (riskPct > 100 || stopPct > 100) {
        chunk(view, '\n❌ %는 100 이하로 입력하세요 (예: 2 = 2%).\n');
        return;
    }
    const positionWon = total * (riskPct / 100) / (stopPct / 100);
    const maxLoss = positionWon * (stopPct / 100);
    const positionRatio = positionWon / total * 100;

    chunk(view, '\n💰 **포지션 사이징 계산**\n');
    chunk(view, `  · 총 자산: ${formatMoney(total, mkt)}\n`);
    chunk(view, `  · 리스크 허용: ${riskPct}% (= 최대 손실 ${formatMoney(maxLoss, mkt)})\n`);
    chunk(view, `  · 손절폭: ${stopPct}%\n`);
    chunk(view, `\n  → **권장 투자금: ${formatMoney(positionWon, mkt)}** (총자산의 ${positionRatio.toFixed(1)}%)\n`);

    if (positionRatio > 50) {
        chunk(view, '\n  ⚠️ 권장 투자금이 총자산의 50%를 초과 — 손절폭이 너무 좁거나 리스크 허용이 너무 큽니다. 입력값 재검토 권장.\n');
    }

    if (symbol) {
        chunk(view, `\n📈 ${symbol} 현재가 fetch 중...\n`);
        const price = await fetchYahooPrice(symbol);
        if (typeof price === 'number') {
            const shares = Math.floor(positionWon / price);
            const actualWon = shares * price;
            chunk(view, `  · 현재가: ${formatMoney(price, mkt)}\n`);
            chunk(view, `  → **매수 가능 주수: ${shares.toLocaleString()}주** (실제 투자금 ${formatMoney(actualWon, mkt)})\n`);
        } else {
            chunk(view, '  ⚠️ Yahoo 현재가 fetch 실패 — 주수 계산 skip\n');
        }
    }
    chunk(view, '\n');
}

async function cmdReport(view: Webview | undefined, context: vscode.ExtensionContext): Promise<void> {
    chunk(view, '\n📨 텔레그램 보고서 발송 중...\n');
    const r = await sendStocksReport(context);
    chunk(view, r.ok ? '\n✅ 발송 완료.\n' : `\n❌ 발송 실패: ${r.reason}\n`);
    chunk(view, `\n*Preview:*\n${buildReportText()}\n`);
}

async function cmdRun(view: Webview | undefined, context: vscode.ExtensionContext): Promise<void> {
    chunk(view, '\n⚡ Watcher 1회 즉시 실행 (현재가 + Sheets + 텔레그램)...\n');
    await runOnceNow(context);
    chunk(view, '\n✅ 완료.\n');
}

async function cmdDiscover(rest: string, view: Webview | undefined, context?: vscode.ExtensionContext): Promise<void> {
    const parts = rest.split(/\s+/).filter(Boolean);

    // `sectors` — 사용 가능한 섹터 키 목록 안내.
    if (parts[0]?.toLowerCase() === 'sectors') {
        chunk(view, `\n🏷️ 발굴 가능한 섹터:\n  ${listSectorKeys().join(' · ')}\n\n사용법: \`/stocks discover sector <섹터> [min] [max]\`\n`);
        return;
    }

    // `sector <섹터명> [min] [max]` — 섹터 필터 발굴.
    let sectorGroup: SectorGroup | undefined;
    let argParts = parts;
    if (parts[0]?.toLowerCase() === 'sector') {
        const sectorName = parts[1];
        if (!sectorName) {
            chunk(view, `\n섹터명이 필요합니다. \`/stocks discover sector <섹터> [min] [max]\`\n사용 가능: ${listSectorKeys().join(' · ')}\n`);
            return;
        }
        const resolved = resolveSectorQuery(sectorName);
        if (!resolved) {
            chunk(view, `\n⚠️ '${sectorName}' 섹터를 못 찾았습니다. 사용 가능:\n  ${listSectorKeys().join(' · ')}\n`);
            return;
        }
        sectorGroup = resolved;
        argParts = parts.slice(2); // min/max 는 sector <name> 뒤에서 파싱
    }

    // 인자 파싱 — `min max` (둘 다 억 단위) / 안 주면 default.
    const minCap = argParts[0] ? parseInt(argParts[0], 10) : 1000;
    const maxCap = argParts[1] ? parseInt(argParts[1], 10) : 5000;
    if (Number.isNaN(minCap) || Number.isNaN(maxCap) || minCap >= maxCap) {
        chunk(view, '\n사용법: `/stocks discover [min] [max]` 또는 `/stocks discover sector <섹터> [min] [max]` (억 단위)\n');
        return;
    }

    const sectorLabel = sectorGroup ? ` · 섹터 '${sectorGroup.key}'` : '';
    chunk(view, `\n🔍 **Naver 발굴 시작** (시총 ${minCap.toLocaleString()}억 ~ ${maxCap.toLocaleString()}억${sectorLabel})\n`);

    const candidates = await discoverStocks({
        minMarketCapEok: minCap,
        maxMarketCapEok: maxCap,
        sector: sectorGroup,
        onProgress: (msg) => chunk(view, msg + '\n'),
    });

    if (candidates.length === 0) {
        chunk(view, '\n결과 없음.\n');
        return;
    }

    chunk(view, `\n📋 **발굴 후보 ${candidates.length}개** (멀티팩터 합성점수 내림차순 — Q퀄리티/V밸류/M모멘텀/S안정성)\n\n`);
    for (const c of candidates) {
        const price = c.fundamentals.currentPrice;
        const priceStr = typeof price === 'number' ? price.toLocaleString() + '원' : '-';
        const sectorStr = c.fundamentals.sectorHint ? ` · ${c.fundamentals.sectorHint}` : '';
        const weakMark = sectorGroup && c.passedKeywords.length < 3 ? ' ⚠️섹터내상대' : '';
        const vtMark = c.score.valueTrapGuarded ? ' · ⚠️VT가드' : '';
        chunk(view, `  · **${c.score.total}점** ${c.name} (${c.symbol}) [${c.market} · ${c.marketCapEok.toLocaleString()}억${sectorStr}]${weakMark}\n`);
        chunk(view, `      ${c.score.breakdown}${c.score.fScore ? ` · miniF ${c.score.fScore}` : ''}${vtMark} · 현재가 ${priceStr} · ROE ${c.fundamentals.roe?.toFixed(1) ?? '-'}% · 영업이익률 ${c.fundamentals.operatingMargin?.toFixed(1) ?? '-'}% · 유보율 ${c.fundamentals.retentionRatio?.toLocaleString() ?? '-'}%\n`);
        chunk(view, `      통과 (${c.passedKeywords.length}): ${c.passedKeywords.join(', ')}\n\n`);
    }

    // ── LLM 매력도 분석 + 텔레그램 전송 (자동 chain) ──
    // 사용자 의도: 발굴 목록이 나오면 *항상* 분석 + 텔레그램. 별도 명령 trigger 불필요.
    // 실패해도 (LLM timeout / 텔레그램 미설정) 위 발굴 목록은 화면에 그대로 남아 있음.
    chunk(view, '\n');
    const topFive = await analyzeTopCandidates(candidates, (msg) => chunk(view, msg + '\n'));
    chunk(view, renderTopFiveForChat(topFive));

    if (context) {
        const rangeLabel = `시총 ${minCap.toLocaleString()}-${maxCap.toLocaleString()}억${sectorGroup ? ` · ${sectorGroup.key}` : ''}`;
        const tgText = renderTopFiveForTelegram(topFive, rangeLabel);
        const tgResult = await sendTopFiveToTelegram(context, tgText);
        chunk(view, tgResult.ok
            ? '\n📨 텔레그램 발송 완료.\n'
            : `\n⚠️ 텔레그램 발송 skip: ${tgResult.reason}\n`);
    } else {
        chunk(view, '\n⚠️ 텔레그램 발송 skip: ExtensionContext 없음.\n');
    }

    chunk(view, '\n💡 종목을 stocks.json 에 추가하려면 `/stocks add <심볼> <이름>` 사용.\n');
}

function cmdPath(view: Webview | undefined): void {
    const p = getStocksFilePath();
    chunk(view, p ? `\n📂 stocks.json: \`${p}\`\n` : '\n⚠️ 워크스페이스 폴더 없음 — stocks 모듈 사용 불가.\n');
}

function cmdHelp(view: Webview | undefined): void {
    chunk(view, [
        '\n📈 **Stocks 명령**  (한국 6자리 코드 + 미국 티커 🇺🇸 지원)',
        '',
        '  `/stocks list`               — 종목 + 신호',
        '  `/stocks check`              — 현재가 갱신',
        '  `/stocks signal`             — 매수사정권 종목만',
        '  `/stocks sync`               — Google Sheets 동기화',
        '  `/stocks add <심볼> <이름>`   — 종목 추가 (예: `add 005930 삼성전자` / `add AAPL Apple`)',
        '  `/stocks remove <심볼>`       — 종목 제거',
        '  `/stocks judge <심볼>`        — LLM 4-criteria 평가 (stocks.json 등록 종목)',
        '  `/stocks analysis <심볼>`     — 심층 분석 (펀더멘털 + MA 정배열 + RSI + LLM 종합, 한·미)',
        '  `/stocks position [심볼] <총자산> <리스크%> <손절%>` — 포지션 사이징 (적정 투자금)',
        '  `/stocks discover [min] [max]` — Naver 크롤 발굴 (한국 전용, 시총 억 단위, default 1000-5000)',
        '  `/stocks discover sector <섹터> [min] [max]` — 섹터별 발굴 (예: `discover sector 반도체`)',
        '  `/stocks discover sectors`   — 발굴 가능한 섹터 목록',
        '  `/stocks report`             — 텔레그램 보고서 즉시 발송',
        '  `/stocks run`                — Watcher 1회 즉시 실행',
        '  `/stocks path`               — stocks.json 경로 표시',
        '',
        '자동 실행: VS Code 시작 시 활성화. KST 09:00 / 15:00 매일 자동.',
        '',
    ].join('\n'));
}

/** slashRouter 가 `/stocks` 로 들어오는 모든 입력을 이 함수 한 곳으로 위임. */
export async function handleStocksCommand(
    arg: string,
    view: Webview | undefined,
    context?: vscode.ExtensionContext,
): Promise<boolean> {
    const parts = arg.trim().split(/\s+/);
    const sub = (parts[0] || '').toLowerCase();
    const rest = parts.slice(1).join(' ').trim();

    logInfo(`Stocks slash: sub=${sub} rest="${rest.slice(0, 40)}"`);

    try {
        switch (sub) {
            case '': cmdHelp(view); return true;
            case 'list': await cmdList(view); return true;
            case 'check': await cmdCheck(view); return true;
            case 'signal': await cmdSignal(view); return true;
            case 'sync':
                if (!context) { chunk(view, '\n❌ ExtensionContext 없음 (sync 불가).\n'); return true; }
                await cmdSync(view, context); return true;
            case 'add': await cmdAdd(rest, view); return true;
            case 'remove': case 'rm': await cmdRemove(rest, view); return true;
            case 'judge': await cmdJudge(rest, view); return true;
            case 'analysis': case 'analyze': await cmdAnalysis(rest, view); return true;
            case 'position': case 'size': await cmdPosition(rest, view); return true;
            case 'discover': await cmdDiscover(rest, view, context); return true;
            case 'report':
                if (!context) { chunk(view, '\n❌ ExtensionContext 없음.\n'); return true; }
                await cmdReport(view, context); return true;
            case 'run':
                if (!context) { chunk(view, '\n❌ ExtensionContext 없음.\n'); return true; }
                await cmdRun(view, context); return true;
            case 'path': cmdPath(view); return true;
            default:
                chunk(view, `\n❌ 알 수 없는 sub-command: \`${sub}\`. \`/stocks\` 로 도움말 보기.\n`);
                return true;
        }
    } catch (e: any) {
        chunk(view, `\n❌ 에러: ${e?.message ?? String(e)}\n`);
        return true;
    }
}
