/**
 * ============================================================
 * /email — Gmail + 클래식 Outlook 통합 이메일 명령 (v2.2.269)
 *
 *   /email                       — 도움말 + 연결 상태
 *   /email list [gmail|outlook] [일수]   — 최근 메일 목록 (기본 둘 다, 7일)
 *   /email read G3 | O2          — 목록 번호로 내용 읽기
 *   /email wikify G3 | O2        — 지식 문서로 위키화 → brain 반영 (wiki.save)
 *   /email reply O2 [지시사항]    — Outlook 회신 초안 → Drafts 저장 (발송 안 함)
 *
 * 아키텍처 결정 (2026-07-02): Gmail 은 ASTRA 자체 Google OAuth(gmail.readonly
 * 스코프 추가)로 직접 읽는다 — NAS bridge 의 gmail 라우트는 headless OAuth 가
 * 비현실적이라 미사용. Outlook 은 로컬 COM(PowerShell). LLM 합성은 PC 로컬,
 * 위키 저장만 bridge(wiki.save) — /meet 와 동일 경로로 brain 에 반영된다.
 *
 * import 만으로 등록 — extension.ts 에서 `import './features/email/handlers'`.
 * ============================================================
 */

import * as vscode from 'vscode';
import { registerSlashCommand, chunk, progress } from '../datacollect/slashRouter';
import { persistWikiDoc, describeSave } from '../datacollect/wikiSave';
import { callLmSynthesis } from '../datacollect/llm';
import { listMessageIds, getMessage, getThreadMessages, type ParsedMessage } from './gmailApi';
import { listInbox, readMail, saveReplyDraft, openMailInOutlook, type OutlookMail } from './outlookApi';
import { buildEmailTranscript, buildReplyDraftPrompt, buildInboxBriefPrompt, injectTimeHeader, polishBriefLabels, type EmailForPrompt } from './emailPrompts';
import { buildEmailCanonicalWikifyPrompt } from '../datacollect/prompts/wikifyPrompt';
import { getCanonicalFormat } from '../datacollect/wikiFormat';
import { groupThreads, normalizeSubject } from './emailBrief';
import { collectRecentMails, emailProviders, type GmailThreadItem } from './emailCollect';
import { readEmailState, writeEmailState, syncThreads, setThreadState, waitingHours, readWikifiedTopics, markWikified, clearWikified, type ThreadState } from './emailState';
import type { ThreadGroup } from './emailBrief';
import { emailWatcherFireOnce } from './emailWatcher';

type Webview = Parameters<typeof chunk>[0];

// ── 목록 캐시 — read/wikify/reply 가 번호로 참조 ─────────────────────────────
const lastList: { gmail: GmailThreadItem[]; outlook: OutlookMail[] } = { gmail: [], outlook: [] };

/** "G3" / "o2" → { provider, idx(0-based) }. 형식 오류면 null. (테스트 export) */
export function parseRef(token: string): { provider: 'gmail' | 'outlook'; idx: number } | null {
    const m = (token || '').trim().match(/^([GgOo])\s*(\d{1,2})$/);
    if (!m) return null;
    const idx = parseInt(m[2], 10) - 1;
    if (idx < 0) return null;
    return { provider: /^[Gg]$/.test(m[1]) ? 'gmail' : 'outlook', idx };
}

const fmtDate = (ms: number) => new Date(ms).toISOString().slice(0, 16).replace('T', ' ');
const trunc = (s: string, n: number) => (s.length > n ? s.slice(0, n - 1) + '…' : s);

const WIKIFY_SYSTEM = '당신은 지식 큐레이터입니다. 제공된 이메일 스레드를 P-Reinforce 규격의 고밀도 위키 문서로 정리합니다. 스레드에 없는 내용은 절대 지어내지 않으며, 모든 문서는 한국어로 작성합니다.';

/** 메일 목록 → 프롬프트 메타 (참여자·기간·최신 메일 날짜). */
function mailsMeta(mails: EmailForPrompt[]): { participants: string[]; dateRange: string; latestYmd: string } {
    const participants = [...new Set(mails.map(m => (m.from || '').trim()).filter(f => f && f !== '나'))].slice(0, 6);
    const dates = mails.map(m => m.date).sort();
    const latest = dates[dates.length - 1] || '';
    return {
        participants,
        dateRange: dates.length ? `${dates[0]} ~ ${latest}` : '',
        // 문서 제목용 — 위키화한 날이 아니라 **이메일의 최신 날짜** (시점 오인 방지).
        latestYmd: latest.slice(0, 10) || new Date().toISOString().slice(0, 10),
    };
}

// ── list ─────────────────────────────────────────────────────────────────────
async function listGmail(view: Webview, context: vscode.ExtensionContext, days: number): Promise<void> {
    chunk(view, `\n📥 **Gmail** — 최근 ${days}일 스레드 조회 중…\n`);
    const ids = await listMessageIds(context, { query: `newer_than:${days}d -in:spam -in:trash -category:promotions -category:social`, maxResults: 60 });
    if (!ids.ok) {
        chunk(view, `  ⚠️ ${ids.error}\n`);
        lastList.gmail = [];
        return;
    }
    // 스레드 단위 dedupe (최신 메시지 우선 순서 유지), 상위 15개만 메타 조회.
    const seen = new Set<string>();
    const threadFirstMsg: { threadId: string; msgId: string }[] = [];
    for (const m of ids.data) {
        if (!m.threadId || seen.has(m.threadId)) continue;
        seen.add(m.threadId);
        threadFirstMsg.push({ threadId: m.threadId, msgId: m.id });
        if (threadFirstMsg.length >= 15) break;
    }
    const items: GmailThreadItem[] = [];
    for (const t of threadFirstMsg) {
        const r = await getMessage(context, t.msgId);
        if (r.ok) items.push({ threadId: t.threadId, subject: r.data.subject || '(제목 없음)', from: r.data.from, date: r.data.date });
    }
    lastList.gmail = items;
    if (items.length === 0) {
        chunk(view, '  (해당 기간 메일 없음)\n');
        return;
    }
    for (let i = 0; i < items.length; i++) {
        const it = items[i];
        chunk(view, `- **G${i + 1}** ${fmtDate(it.date)} · ${trunc(it.from, 30)} — ${trunc(it.subject, 60)}\n`);
    }
    chunk(view, '\n');
}

async function listOutlook(view: Webview, days: number): Promise<void> {
    chunk(view, `\n📥 **Outlook (클래식)** — 최근 ${days}일 받은편지함 조회 중…\n`);
    const r = await listInbox(days, 25);
    if (!r.ok) {
        chunk(view, `  ⚠️ ${r.error}\n`);
        lastList.outlook = [];
        return;
    }
    lastList.outlook = r.data;
    if (r.data.length === 0) {
        chunk(view, '  (해당 기간 메일 없음)\n');
        return;
    }
    for (let i = 0; i < r.data.length; i++) {
        const m = r.data[i];
        chunk(view, `- **O${i + 1}** ${m.received} · ${trunc(m.from, 30)} — ${m.unread ? '🔵 ' : ''}${trunc(m.subject, 60)}${m.folder ? ` · 📁${trunc(m.folder, 16)}` : ''}\n`);
    }
    chunk(view, '\n');
}

// ── read 대상 로드 (provider 공통 셰이프로) ───────────────────────────────────
async function loadMails(
    ref: { provider: 'gmail' | 'outlook'; idx: number },
    context: vscode.ExtensionContext | undefined,
): Promise<{ ok: true; subject: string; mails: EmailForPrompt[]; entryId?: string } | { ok: false; error: string }> {
    if (ref.provider === 'gmail') {
        const it = lastList.gmail[ref.idx];
        if (!it) return { ok: false, error: `G${ref.idx + 1} 없음 — 먼저 \`/email list gmail\` 로 목록을 불러오세요.` };
        if (!context) return { ok: false, error: 'ExtensionContext 없음' };
        const r = await getThreadMessages(context, it.threadId);
        if (!r.ok) return { ok: false, error: r.error };
        const mails: EmailForPrompt[] = r.data.map((m: ParsedMessage) => ({
            subject: m.subject, from: m.from, to: m.to, date: fmtDate(m.date), body: m.bodyText || m.snippet,
        }));
        return { ok: true, subject: it.subject, mails };
    }
    const it = lastList.outlook[ref.idx];
    if (!it) return { ok: false, error: `O${ref.idx + 1} 없음 — 먼저 \`/email list outlook\` 으로 목록을 불러오세요.` };
    const r = await readMail(it.entryId);
    if (!r.ok) return { ok: false, error: r.error };
    return {
        ok: true,
        subject: r.data.subject || '(제목 없음)',
        entryId: it.entryId,
        mails: [{ subject: r.data.subject, from: r.data.from, to: r.data.to, date: r.data.received, body: r.data.body || '' }],
    };
}

// ── 브리핑 (기본 동작) — 영속 상태(email_state.json) 기반 현황 ─────────────────
// v2.2.271: 매번 새로 긁는 스냅샷이 아니라, 워처가 누적한 상태에 이번 수집을
// 병합해 보여준다 — 수집 창(7일) 밖으로 벗어난 오래된 미회신도 계속 표시.

const fmtWait = (t: ThreadState, now: Date) => {
    const h = waitingHours(t, now);
    if (h <= 0) return '';
    return h >= 48 ? ` · **${(h / 24).toFixed(1)}일째 대기**` : ` · ${Math.round(h)}시간째 대기`;
};

/** LLM 실패 시에도 항상 나오는 결정론 렌더 — 상태 저장소 기반 (대시보드 + 상태 이모지). */
function renderStateBrief(threads: ThreadState[], now: Date): string {
    const lines: string[] = [];
    const section = (title: string, items: ThreadState[], fmt: (t: ThreadState) => string) => {
        if (items.length === 0) return;
        lines.push(`\n**${title}** (${items.length})\n`);
        for (const t of items) lines.push(fmt(t));
    };
    const label = (t: ThreadState) =>
        `- ${t.hasUnread ? '🔵 ' : ''}**${trunc(t.subject, 60)}** — ${t.participants.slice(0, 3).join(', ') || '-'}${fmtWait(t, now)}${t.refs.length ? ` · [${t.refs.slice(0, 3).join(' ')}]` : ''}`;

    const waiting = threads.filter(t => t.state === 'waiting' && !t.automated)
        .sort((a, b) => (a.waitingSince || '').localeCompare(b.waitingSince || ''));
    const answeredAll = threads.filter(t => t.state === 'answered' && !t.automated);
    // 상대 회신 대기는 7일+ 장기 건만 강조 — 단기 대기는 건수만 (놓친 업무 발굴 목적).
    const daysSince = (t: ThreadState) => {
        const ms = new Date(t.lastActivity.replace(' ', 'T')).getTime();
        return Number.isFinite(ms) ? Math.floor((now.getTime() - ms) / 86_400_000) : 0;
    };
    const longWait = answeredAll.filter(t => daysSince(t) >= 7)
        .sort((a, b) => daysSince(b) - daysSince(a)).slice(0, 10);
    const auto = threads.filter(t => t.automated && t.state !== 'muted')
        .sort((a, b) => b.lastActivity.localeCompare(a.lastActivity)).slice(0, 8);

    // 상단 대시보드 — 코드 판정 기반 (LLM 우선순위 분류는 폴백에선 생략).
    const oldCount = waiting.filter(t => waitingHours(t, now) >= 48).length;
    lines.push('■ **메일 현황**\n');
    lines.push(`- 🔴 회신 필요 ${waiting.length}건 (48h+ ${oldCount}건) · ⏳ 상대 대기 ${answeredAll.length}건 (7일+ ${longWait.length}건) · 📢 자동발신 ${auto.length}건\n`);

    section('🔴 회신 필요 — 오래된 순', waiting, label);
    section(`⏳ 장기 대기 — 7일 이상 (전체 ${answeredAll.length}건 중)`, longWait,
        t => `- **${trunc(t.subject, 50)}** — ${t.participants[0] || '-'} 대기 · **${daysSince(t)}일째**${t.refs.length ? ` · [${t.refs.slice(0, 2).join(' ')}]` : ''}`);
    section('📢 자동발신 · 공지 (회신 불필요)', auto, t => `- ${trunc(t.subject, 60)} — ${t.participants[0] || '-'} · ${t.lastActivity}`);
    return lines.join('\n');
}

async function cmdBrief(rest: string[], view: Webview, context?: vscode.ExtensionContext): Promise<void> {
    // 기간을 명시하면(`/email 1`) 수집뿐 아니라 **표시도 그 범위로 제한** — 기본(무인자)일 때만
    // 누적 상태 전체(오래된 미회신 보존)를 보여준다.
    const explicitDays = /^\d+$/.test((rest[0] || '').trim());
    const days = Math.max(1, Math.min(90, parseInt(rest[0] || '7', 10) || 7));
    const now = new Date();
    chunk(view, `\n📬 **이메일 브리핑** — 최근 ${days}일 수집 + 누적 상태 병합 중…\n\n`);

    // (1) 수집 (워처와 동일 경로) + 참조번호 캐시 갱신
    const col = await collectRecentMails(days, context, { gmailThreads: 12 });
    if (col.outlookError) chunk(view, `- ⚠️ Outlook: ${col.outlookError}\n`);
    if (col.gmailError) chunk(view, `- ℹ️ Gmail 제외: ${trunc(col.gmailError, 120)}\n`);
    lastList.outlook = col.outlookMails;
    lastList.gmail = col.gmailItems;

    // (2) 영속 상태에 병합 — 이번 관측 + 과거 누적 (오래된 미회신 보존)
    const observed = groupThreads(col.inbox, col.sent);
    const store = syncThreads(readEmailState(), observed, now);
    writeEmailState(store);
    let active = store.threads.filter(t => t.state !== 'muted' && t.state !== 'done');
    if (explicitDays) {
        const cutoff = new Date(now.getTime() - days * 86_400_000).toISOString().slice(0, 10);
        const before = active.length;
        active = active.filter(t => t.lastActivity.slice(0, 10) >= cutoff);
        chunk(view, `📅 표시 범위: 최근 ${days}일 (그 외 추적 중 ${before - active.length}건은 숨김 — 전체는 \`/email\`)\n\n`);
    }
    if (active.length === 0) {
        chunk(view, '- 추적 중인 메일이 없습니다.\n');
        return;
    }
    const waitingCount = active.filter(t => t.state === 'waiting' && !t.automated).length;
    chunk(view, `수집 ${col.inbox.length}건 → 추적 주제 ${active.length}개 (회신 대기 ${waitingCount}건). 정리 중…\n\n`);

    // 오늘 일정 (v2.2.287 — 결정론, 캘린더 캐시) — 메일은 결국 일정으로 이어지므로 함께 표시.
    if (context) {
        try {
            const { readCalendarEventsCache } = await import('../calendar/calendarCache');
            const ymd = now.toISOString().slice(0, 10);
            const events = readCalendarEventsCache(context)
                .filter(e => (e.startIso || '').slice(0, 10) === ymd)
                .sort((a, b) => a.startIso.localeCompare(b.startIso));
            if (events.length) {
                chunk(view, `🗓 **오늘 일정** (${events.length})\n`);
                for (const e of events.slice(0, 8)) {
                    chunk(view, `- ${e.allDay ? '종일' : e.startIso.slice(11, 16)} — ${trunc(e.summary || '(제목 없음)', 40)}\n`);
                }
                chunk(view, '\n');
            }
        } catch { /* 캘린더 미연결/캐시 없음 — 조용히 생략 */ }
    }

    // (3) 회신 대기 상위 8개는 본문 심화 (Outlook 한정 — '내용/할 일' 품질의 핵심)
    const deep = new Map<string, string>();
    const needDeep = active.filter(t => t.state === 'waiting' && !t.automated && t.latestEntryId)
        .sort((a, b) => (a.waitingSince || '').localeCompare(b.waitingSince || '')).slice(0, 8);
    for (const t of needDeep) {
        const r = await readMail(t.latestEntryId!);
        if (r.ok && r.data.body) deep.set(t.topic, r.data.body.slice(0, 1500).replace(/\s+/g, ' '));
    }

    // (4) LLM 서술 (분류·대기시간 판정은 코드 — 프롬프트에 명시).
    //     타임라인(이번 관측에서 확보한 메시지 흐름)을 '경과' 재료로 공급.
    const timelineByTopic = new Map(observed.map(g => [g.topic, g.timeline]));
    const block = active.map((t, i) => {
        const status = t.automated ? '자동발신' : t.state === 'waiting' ? '회신대기' : '공넘김';
        const ccMark = t.ccOnly ? ' | 수신=CC(회신불요)' : '';
        // 회신대기 = 내 대기시간, 공넘김 = 상대가 답 안 준 경과일 (장기 대기 7일+ 판정 재료)
        const sinceMs = new Date(t.lastActivity.replace(' ', 'T')).getTime();
        const elapsedDays = Number.isFinite(sinceMs) ? Math.floor((now.getTime() - sinceMs) / 86_400_000) : 0;
        const wh = waitingHours(t, now);
        const wait = t.state === 'waiting'
            ? ` | 대기=${wh >= 48 ? `${Math.round(wh / 24)}일` : `${Math.round(wh)}시간`}`
            : t.state === 'answered' && !t.automated ? ` | 상대대기경과=${elapsedDays}일` : '';
        const lines = [
            `[${i + 1}] 상태=${status}${wait} | 주제="${t.subject}" | 참여자=${t.participants.join(',') || '-'} | 수신${t.inCount}/발신${t.outCount} | 최근=${t.lastActivity} | 읽지않음=${t.hasUnread ? 'Y' : 'N'}${ccMark} | 참조=[${t.refs.slice(0, 3).join(' ')}]`,
        ];
        const tl = timelineByTopic.get(t.topic);
        if (tl && tl.length && !t.automated) {
            lines.push(`    타임라인: ${tl.map(e => `${e.date.slice(5, 16)} ${e.from}${e.snippet ? `("${e.snippet.slice(0, 60)}")` : ''}`).join(' → ')}`);
        }
        const body = deep.get(t.topic);
        if (body) lines.push(`    본문발췌="${body.slice(0, 800)}"`);
        else if (t.snippet) lines.push(`    스니펫="${t.snippet.slice(0, 200)}"`);
        return lines.join('\n');
    }).join('\n');

    try {
        const brief = await callLmSynthesis(
            buildInboxBriefPrompt(block),
            '당신은 받은편지함 현황을 정리하는 비서다. 제공된 데이터에 있는 내용만 사용하고, 분류·순서·대기시간 판정을 바꾸지 않는다. 한국어 마크다운으로 출력한다.',
            { maxTokens: 4500 },
        );
        chunk(view, (brief && brief.trim() ? polishBriefLabels(brief.trim()) : renderStateBrief(active, now)) + '\n');
    } catch {
        chunk(view, renderStateBrief(active, now) + '\n');
    }
    chunk(view, `\n💡 \`/email open O216\` **Outlook에서 열기** · \`/email read O2\` 읽기 · \`/email done O2\` 완료 · \`/email mute O3\` 무시 · \`/email wikify O2\` 지식화 · \`/email reply O2\` 회신초안\n`);
}

// ── 전체 스캔 + 자동 위키화 (v2.2.275) ────────────────────────────────────────
// `/email scan [all|일수] [최대건수]` — 7일 창을 넘어 메일함 전체(기본)를 수집해
// 상태에 병합하고, 사람 발신 스레드를 자동으로 지식 문서화(Email_Wiki)한다.
// LLM 위키화는 건당 수십 초라 회차당 상한(기본 15건) — 이미 위키화된 스레드는
// 레지스트리로 스킵되므로, 다시 실행하면 나머지를 이어서 처리한다.

/** 스레드의 위키화 재료 로드 — Gmail 은 스레드 전체, Outlook 은 최신 메일 본문+타임라인. */
async function loadThreadForWikify(
    g: ThreadGroup,
    context?: vscode.ExtensionContext,
): Promise<EmailForPrompt[] | null> {
    const gmailRef = g.refs.find(r => r.startsWith('G'));
    if (gmailRef && context) {
        const parsed = parseRef(gmailRef);
        const item = parsed ? lastList.gmail[parsed.idx] : undefined;
        if (item) {
            const r = await getThreadMessages(context, item.threadId);
            if (r.ok && r.data.length) {
                return r.data.map(m => ({ subject: m.subject, from: m.from, to: m.to, date: fmtDate(m.date), body: m.bodyText || m.snippet }));
            }
        }
    }
    if (g.latestEntryId) {
        const r = await readMail(g.latestEntryId);
        if (r.ok) {
            // 타임라인 스니펫으로 과거 흐름 보강 (Outlook 은 스레드 일괄 조회가 없어 최신 본문 중심).
            const history = g.timeline.slice(0, -1)
                .map(e => `[${e.date}] ${e.from}: ${e.snippet || '(내용 요약 없음)'}`).join('\n');
            const body = (history ? `--- 이전 흐름 요약 ---\n${history}\n\n--- 최신 메일 ---\n` : '') + (r.data.body || '');
            return [{ subject: r.data.subject, from: r.data.from, to: r.data.to, date: r.data.received, body }];
        }
    }
    return null;
}

async function cmdScan(rest: string[], view: Webview, context?: vscode.ExtensionContext): Promise<void> {
    // `/email scan reset` — 위키화 완료 기록 초기화 → 다음 스캔이 처음부터 다시.
    if ((rest[0] || '').toLowerCase() === 'reset') {
        const ok = clearWikified();
        chunk(view, ok
            ? '\n🧹 **위키화 완료 기록 초기화** — 이제 `/email scan` 이 모든 스레드를 처음부터 다시 위키화합니다.\n'
            : '\n⚠️ 초기화 실패 — 워크스페이스가 열려 있는지 확인하세요.\n');
        return;
    }
    const a0 = (rest[0] || 'all').toLowerCase();
    const days = a0 === 'all' ? 3650 : Math.max(1, Math.min(3650, parseInt(a0, 10) || 3650));
    const maxWikify = Math.max(1, Math.min(50, parseInt(rest[1] || '', 10) || 15));
    const rangeLabel = a0 === 'all' ? '전체(최대 10년·300건)' : `최근 ${days}일`;

    chunk(view, `\n🔎 **이메일 전체 스캔** — ${rangeLabel} 수집 중… (받은편지함 최대 300건 + 보낸편지함 200건)\n\n`);
    const col = await collectRecentMails(days, context, { gmailThreads: 25, outlookMax: 300, sentMax: 200 });
    if (col.outlookError) chunk(view, `- ⚠️ Outlook: ${col.outlookError}\n`);
    if (col.gmailError) chunk(view, `- ℹ️ Gmail 제외: ${trunc(col.gmailError, 120)}\n`);
    lastList.outlook = col.outlookMails;
    lastList.gmail = col.gmailItems;
    if (col.inbox.length === 0) { chunk(view, '- 수집된 메일이 없습니다.\n'); return; }

    // 상태 병합 — 스캔 이후 /email 브리핑이 전체 이력을 반영.
    const observed = groupThreads(col.inbox, col.sent);
    writeEmailState(syncThreads(readEmailState(), observed));

    // 자동 위키화 대상 선별 — 사람 발신 + 미위키화.
    const wikified = readWikifiedTopics(true); // 파일 삭제 감지 — 지운 문서는 다시 위키화
    const humans = observed.filter(t => !t.automated);
    const targets = humans.filter(t => !wikified.has(t.topic));
    const batch = targets.slice(0, maxWikify);
    chunk(view, `수집 ${col.inbox.length}건 → 주제 ${observed.length}개 (사람 ${humans.length} · 자동발신 ${observed.length - humans.length})\n`);
    chunk(view, `위키화 대상 ${targets.length}건 중 이번 회차 ${batch.length}건 처리 (이미 완료 ${humans.length - targets.length}건 스킵)\n\n`);
    if (batch.length === 0) {
        chunk(view, '✅ 모든 사람 발신 스레드가 이미 위키화되어 있습니다.\n');
        return;
    }

    // 항목별 로그 대신 제자리 갱신 진행 표시 한 줄 (v2.2.279) — 완료 후 요약만 남는다.
    let ok = 0;
    const failures: string[] = [];
    let savedDir = '';
    const today = new Date().toISOString().slice(0, 10);
    const canonical = await getCanonicalFormat();
    for (let i = 0; i < batch.length; i++) {
        const t = batch[i];
        progress(view, `\n⏳ **이메일 위키화 중 (${i + 1}/${batch.length})** — ${trunc(t.subject, 45)}\n`);
        try {
            const mails = await loadThreadForWikify(t, context);
            if (!mails) throw new Error('본문 로드 실패');
            const meta = mailsMeta(mails);
            const doc = await callLmSynthesis(
                buildEmailCanonicalWikifyPrompt(t.subject, buildEmailTranscript(mails), meta, canonical),
                WIKIFY_SYSTEM,
                { maxTokens: 5000 },
            );
            if (!doc || !doc.trim()) throw new Error('LLM 응답 비어 있음');
            // 제목 날짜 = 이메일 최신 날짜 (위키화한 날 아님) + 기준 시점 헤더 주입 — 시점 오인 방지.
            const stamped = injectTimeHeader(doc.trim(), meta.dateRange, today);
            const saved = await persistWikiDoc(`이메일 ${trunc(t.subject, 60)} ${meta.latestYmd}`, stamped, { store: 'email', docType: 'email' });
            if (!saved.ok) throw new Error(saved.error || '저장 실패');
            if (saved.path) savedDir = saved.path.replace(/[\\/][^\\/]+$/, '');
            markWikified(t.topic, saved.path);
            ok++;
        } catch (e: any) {
            failures.push(`${trunc(t.subject, 40)} — ${trunc(e?.message || String(e), 60)}`);
        }
    }
    // 마지막 chunk 가 진행 줄을 걷어내고 요약으로 대체한다.
    const remaining = targets.length - batch.length;
    chunk(view, `\n📚 **스캔 완료** — 위키화 ${ok}건${failures.length ? ` · 실패 ${failures.length}건` : ''}${remaining > 0 ? ` · 남은 ${remaining}건 (\`/email scan\` 재실행으로 이어서)` : ' · 전부 완료'}\n`);
    if (savedDir) chunk(view, `- 저장 폴더: \`${savedDir}\`\n`);
    if (canonical) chunk(view, `- 포맷 정본 v${canonical.version} (브리지)\n`);
    for (const f of failures.slice(0, 5)) chunk(view, `- ⚠️ ${f}\n`);
    if (failures.length > 5) chunk(view, `- ⚠️ … 외 실패 ${failures.length - 5}건\n`);
    chunk(view, `상태 저장소에도 병합됨 — \`/email\` 브리핑이 이제 이 범위를 반영합니다.\n`);
}

// ── 원본 메일 열기 (v2.2.289) — 브리핑 [O번호] → 실제 메일로 점프 ───────────────
async function cmdOpen(rest: string[], view: Webview): Promise<void> {
    const ref = parseRef(rest[0] || '');
    if (!ref) { chunk(view, '\n사용법: `/email open O216` — 해당 메일을 Outlook 창으로 엽니다 (Gmail 은 브라우저).\n'); return; }
    if (ref.provider === 'gmail') {
        const it = lastList.gmail[ref.idx];
        if (!it) { chunk(view, `\n⚠️ G${ref.idx + 1} 없음 — 먼저 \`/email\` 로 목록을 로드하세요.\n`); return; }
        await vscode.env.openExternal(vscode.Uri.parse(`https://mail.google.com/mail/u/0/#all/${it.threadId}`));
        chunk(view, `\n🌐 브라우저에서 Gmail 스레드를 열었습니다: **${trunc(it.subject, 50)}**\n`);
        return;
    }
    const it = lastList.outlook[ref.idx];
    if (!it) { chunk(view, `\n⚠️ O${ref.idx + 1} 없음 — 먼저 \`/email\` 로 목록을 로드하세요.\n`); return; }
    const r = await openMailInOutlook(it.entryId);
    chunk(view, r.ok
        ? `\n📬 **Outlook 창으로 열었습니다**: ${trunc(it.subject, 50)} (${it.received} · ${trunc(it.from, 20)})\n`
        : `\n⚠️ 열기 실패: ${r.error}\n`);
}

// ── 트리아지 — done(완료) / mute(무시) / unmute ───────────────────────────────
function refToTopic(ref: { provider: 'gmail' | 'outlook'; idx: number }): string | null {
    const subject = ref.provider === 'gmail'
        ? lastList.gmail[ref.idx]?.subject
        : lastList.outlook[ref.idx]?.subject;
    return subject ? normalizeSubject(subject) : null;
}

async function cmdTriage(kind: 'done' | 'muted' | 'waiting', rest: string[], view: Webview): Promise<void> {
    // `/email done old 30` — 30일 이상 미회신을 일괄 완료 (스캔이 편입한 과거 백로그 정리용).
    // done 은 새 수신이 오면 자동으로 waiting 재개되므로 안전하다.
    if (kind === 'done' && (rest[0] || '').toLowerCase() === 'old') {
        const days = Math.max(1, Math.min(365, parseInt(rest[1] || '30', 10) || 30));
        const store = readEmailState();
        const now = new Date();
        let n = 0;
        for (const t of store.threads) {
            if (t.state === 'waiting' && waitingHours(t, now) >= days * 24) {
                t.state = 'done';
                t.waitingSince = undefined;
                t.alertedFor = undefined;
                n++;
            }
        }
        writeEmailState(store);
        chunk(view, `\n🧹 **${days}일 이상 미회신 ${n}건을 완료 처리**했습니다 — 해당 주제에 새 메일이 오면 자동으로 다시 추적됩니다.\n`);
        return;
    }
    const ref = parseRef(rest[0] || '');
    if (!ref) { chunk(view, `\n사용법: \`/email ${kind === 'muted' ? 'mute' : kind === 'done' ? 'done' : 'unmute'} O2\` (먼저 \`/email\` 로 목록 로드)\n`); return; }
    const topic = refToTopic(ref);
    if (!topic) { chunk(view, `\n⚠️ ${rest[0]} 참조를 찾지 못했습니다 — 먼저 \`/email\` 을 실행하세요.\n`); return; }
    const store = readEmailState();
    const t = setThreadState(store, topic, kind);
    if (!t) { chunk(view, '\n⚠️ 추적 중인 주제가 아닙니다 — `/email` 로 상태를 먼저 갱신하세요.\n'); return; }
    writeEmailState(store);
    const label = kind === 'done' ? '✅ 완료 처리 (새 수신이 오면 자동 재개)' : kind === 'muted' ? '🔇 무시 처리 (이후 조용)' : '🔔 다시 추적';
    chunk(view, `\n${label}: **${t.subject}**\n`);
}

// ── 서브커맨드 구현 ───────────────────────────────────────────────────────────
async function cmdList(rest: string[], view: Webview, context?: vscode.ExtensionContext): Promise<void> {
    const provider = /^(gmail|outlook)$/i.test(rest[0] || '') ? rest[0].toLowerCase() : 'both';
    const daysTok = provider === 'both' ? rest[0] : rest[1];
    const days = Math.max(1, Math.min(90, parseInt(daysTok || '7', 10) || 7));
    const enabled = emailProviders();

    if (provider !== 'outlook') {
        if (!enabled.gmail) {
            // 명시적으로 gmail 을 요청했을 때만 안내 — 기본(both)이면 조용히 스킵 (깔끔한 출력).
            if (provider === 'gmail') chunk(view, '\nℹ️ Gmail 이 설정에서 비활성화돼 있습니다 (Settings → 연동 → 이메일).\n');
        } else if (context) await listGmail(view, context, days);
        else chunk(view, '\n⚠️ Gmail: ExtensionContext 없음.\n');
    }
    if (provider !== 'gmail') {
        if (!enabled.outlook) {
            if (provider === 'outlook') chunk(view, '\nℹ️ Outlook 이 설정에서 비활성화돼 있습니다 (Settings → 연동 → 이메일).\n');
        } else await listOutlook(view, days);
    }
    chunk(view, `\nℹ️ 이건 **원시 목록**입니다 — 내용·경과·할 일이 정리된 **비서 브리핑은 \`/email\`** 을 사용하세요.\n💡 읽기 \`/email read G1\` · 위키화 \`/email wikify O2\` · 회신초안 \`/email reply O2 [지시사항]\`\n`);
}

async function cmdRead(rest: string[], view: Webview, context?: vscode.ExtensionContext): Promise<void> {
    const ref = parseRef(rest[0] || '');
    if (!ref) { chunk(view, '\n사용법: `/email read G3` 또는 `/email read O2`\n'); return; }
    const loaded = await loadMails(ref, context);
    if (!loaded.ok) { chunk(view, `\n⚠️ ${loaded.error}\n`); return; }
    chunk(view, `\n📧 **${loaded.subject}** (메시지 ${loaded.mails.length}건)\n\n`);
    for (const m of loaded.mails) {
        chunk(view, `> **${m.date}** · ${m.from}${m.to ? ` → ${m.to}` : ''}\n\n${trunc(m.body, 3000)}\n\n`);
    }
}

async function cmdWikify(rest: string[], view: Webview, context?: vscode.ExtensionContext): Promise<void> {
    const ref = parseRef(rest[0] || '');
    if (!ref) { chunk(view, '\n사용법: `/email wikify G3` 또는 `/email wikify O2`\n'); return; }
    const loaded = await loadMails(ref, context);
    if (!loaded.ok) { chunk(view, `\n⚠️ ${loaded.error}\n`); return; }

    chunk(view, `\n🧪 **위키화** — "${trunc(loaded.subject, 50)}" P-Reinforce 정본 규격으로 합성 중…`);
    const transcript = buildEmailTranscript(loaded.mails);
    const meta = mailsMeta(loaded.mails);
    const canonical = await getCanonicalFormat();
    let doc: string;
    try {
        doc = await callLmSynthesis(
            buildEmailCanonicalWikifyPrompt(loaded.subject, transcript, meta, canonical),
            WIKIFY_SYSTEM,
            { maxTokens: 5000 },
        );
    } catch (e: any) {
        chunk(view, ` ⚠️ LLM 합성 실패: ${e?.message || String(e)}\n`);
        return;
    }
    if (!doc || !doc.trim()) { chunk(view, ' ⚠️ LLM 응답이 비어 있습니다.\n'); return; }

    try {
        const today = new Date().toISOString().slice(0, 10);
        // 제목 날짜 = 이메일 최신 날짜 + 기준 시점 헤더 (시점 오인 방지, v2.2.282).
        const stamped = injectTimeHeader(doc.trim(), meta.dateRange, today);
        chunk(view, ' ✓\n\n' + stamped + '\n\n');
        const saved = await persistWikiDoc(`이메일 ${trunc(loaded.subject, 60)} ${meta.latestYmd}`, stamped, { store: 'email', docType: 'email' });
        chunk(view, describeSave(saved, '지식 문서').replace('\n', ' — brain 검색에 반영됩니다.\n'));
    } catch (e: any) {
        chunk(view, `⚠️ 저장 실패(합성 결과는 위에 유지): ${e?.message || String(e)}\n`);
    }
}

async function cmdReply(rest: string[], view: Webview, context?: vscode.ExtensionContext): Promise<void> {
    const ref = parseRef(rest[0] || '');
    if (!ref) { chunk(view, '\n사용법: `/email reply O2 [지시사항]` (Outlook 전용)\n'); return; }
    if (ref.provider === 'gmail') {
        chunk(view, '\nℹ️ Gmail 은 **읽기 전용 스코프**라 초안 작성을 지원하지 않습니다 (Outlook 만 가능 — Drafts 저장, 발송 없음).\n');
        return;
    }
    const loaded = await loadMails(ref, context);
    if (!loaded.ok || !loaded.entryId) { chunk(view, `\n⚠️ ${loaded.ok ? 'EntryID 없음' : loaded.error}\n`); return; }
    const instruction = rest.slice(1).join(' ');

    chunk(view, `\n✍️ **회신 초안 작성** — "${trunc(loaded.subject, 50)}"…`);
    let draft: string;
    try {
        draft = await callLmSynthesis(
            buildReplyDraftPrompt(loaded.mails[0], instruction),
            '당신은 한국어 비즈니스 이메일 초안 작성 보조 도구다. 원문에 없는 사실을 만들지 않는다. 회신 본문 텍스트만 출력한다.',
            { maxTokens: 2048 },
        );
    } catch (e: any) {
        chunk(view, ` ⚠️ LLM 실패: ${e?.message || String(e)}\n`);
        return;
    }
    if (!draft || !draft.trim()) { chunk(view, ' ⚠️ LLM 응답이 비어 있습니다.\n'); return; }
    chunk(view, ' ✓\n\n' + draft.trim() + '\n\n');

    const r = await saveReplyDraft(loaded.entryId, draft.trim());
    chunk(view, r.ok
        ? `📝 **Outlook ${r.data.savedTo}에 저장 완료** — 발송은 하지 않았습니다. Outlook 에서 검토·수정 후 직접 보내세요.\n`
        : `⚠️ 초안 저장 실패: ${r.error}\n`);
}

function cmdHelp(view: Webview): void {
    chunk(view, [
        '',
        '📧 **/email — Gmail + Outlook(클래식) 이메일**',
        '',
        '- `/email` — **받은편지함 현황** (누적 추적 — 오래된 미회신도 유지 · 대기시간 표시)',
        '- `/email brief 14` — 14일 범위로 수집해 병합',
        '- `/email scan [all|일수] [최대건수]` — **전체 스캔 + 자동 위키화** (사람 발신 스레드 → Email_Wiki, 회차당 기본 15건 · 재실행 시 이어서 · **문서를 지우면 자동 재위키화**)',
        '- `/email scan reset` — 위키화 완료 기록 초기화 (처음부터 다시)',
        '- `/email done O2` / `mute O3` / `unmute O3` — 완료·무시·재추적 (트리아지)',
        '- `/email done old 30` — 30일 이상 미회신 **일괄 완료** (스캔이 쌓은 과거 백로그 정리)',
        '- `/email 1` — 기간을 명시하면 **표시도 그 범위로 제한** (무인자 `/email` 은 누적 전체)',
        '- `/email list [gmail|outlook] [일수]` — 원시 목록 (G1../O1.. 번호)',
        '- `/email read O2` — 내용 읽기 (G=Gmail 스레드, O=Outlook)',
        '- `/email open O216` — **원본 메일을 Outlook 창으로 열기** (Gmail 은 브라우저)',
        '- `/email wikify O2` — 지식 문서로 위키화 → brain 반영',
        '- `/email reply O2 [지시사항]` — Outlook 회신 초안 → **Drafts 저장만** (발송 안 함)',
        '',
        '- 🕐 백그라운드 워처가 주기(기본 60분)마다 자동 수집·추적하고, 미회신이 임계(기본 24h)를 넘으면 텔레그램으로 1회 알립니다. (`g1nation.email.*` 설정)',
        '- Gmail: Google OAuth 재연결 1회 필요 (gmail.readonly 동의 — Astra Settings → Google 연결)',
        '- Outlook: 클래식 데스크톱 Outlook 필요 ("새 Outlook" 토글 OFF) — 별도 인증 없음',
        '',
    ].join('\n'));
}

async function runEmail(arg: string, view: Webview, context?: vscode.ExtensionContext): Promise<boolean> {
    const parts = arg.trim().split(/\s+/).filter(Boolean);
    const sub = (parts[0] || '').toLowerCase();
    const rest = parts.slice(1);
    try {
        switch (sub) {
            case '': case 'brief': await cmdBrief(rest, view, context); break;
            case 'help': cmdHelp(view); break;
            case 'list': await cmdList(rest, view, context); break;
            case 'read': await cmdRead(rest, view, context); break;
            case 'open': await cmdOpen(rest, view); break;
            case 'wikify': await cmdWikify(rest, view, context); break;
            case 'reply': case 'draft': await cmdReply(rest, view, context); break;
            case 'scan': await cmdScan(rest, view, context); break;
            case 'done': await cmdTriage('done', rest, view); break;
            case 'mute': await cmdTriage('muted', rest, view); break;
            case 'unmute': await cmdTriage('waiting', rest, view); break;
            case 'sync':
                // 워처 사이클 즉시 1회 (수집→상태 병합→임계 알림)
                if (context) { chunk(view, '\n🔄 이메일 워처 사이클 실행 중…\n'); await emailWatcherFireOnce(context); chunk(view, '완료 — `/email` 로 현황을 확인하세요.\n'); }
                else chunk(view, '\n⚠️ ExtensionContext 없음.\n');
                break;
            default:
                // `/email 14` → 14일 브리핑, `/email gmail 7` → 목록. 그 외는 도움말.
                if (/^\d+$/.test(sub)) await cmdBrief(parts, view, context);
                else if (/^(gmail|outlook)$/.test(sub)) await cmdList(parts, view, context);
                else cmdHelp(view);
        }
    } catch (e: any) {
        chunk(view, `\n❌ /email 오류: ${e?.message || String(e)}\n`);
    }
    return true;
}

registerSlashCommand({
    name: '/email',
    description: 'Gmail + Outlook(클래식) — 받은편지함 브리핑(주제·답변여부 정리)·읽기·위키화·회신초안',
    handler: runEmail,
});
