/**
 * TeamOps Dashboards — /morning · /evening · /cohort (CEO 일·월 리듬).
 *
 * v2.2.198 에서 slashRouter.ts 에서 분리. cross-data 합성 (Tasks + customers +
 * hire + runway + Chronicle ADR) 으로 일/월 단위 시야 제공.
 * (구 /weekly CEO 리뷰 카드는 v2.2.204 에서 제거 — /weekly 는 task 기반 금주/차주
 *  보고서로 일원화, coordination.ts 참조.)
 *
 * 공통 헬퍼는 `./_shared.ts` 에서. dashboards 전용 helper (_morningActions 등) 는
 * 이 파일 안에.
 */

import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { registerSlashCommand, chunk } from '../../datacollect/slashRouter';
import {
    fmtKrw as _fmtKrw, daysUntil as _daysUntil,
    parseTaskOwner, stageEmoji as _stageEmoji,
    STAGE_ORDER, TERMINAL_STAGES,
} from './_shared';
import { listTasks } from '../../calendar';
import { ChronicleProjectStore } from '../../../sidebar/managers/chronicleProjectStore';
import {
    computeRunwayStatus, readRunway, type RunwayEntry,
} from '../../runway/runwayStore';
import {
    computeCustomerStates, readEvents as readCustomerEvents,
    type CustomerEvent, type CustomerState,
} from '../../customers/customersStore';
import {
    computeCandidateStates, readHireEvents,
    type HireEvent, type CandidateState,
} from '../../hire/hireStore';

// ─── /morning — 매일 아침 통합 대시보드 ──────────────────────────────────

async function runMorning(arg: string, view: any, context?: vscode.ExtensionContext): Promise<boolean> {
    const mode = (arg.trim().split(/\s+/)[0] || '').toLowerCase();
    const brief = mode === 'brief' || mode === 'short';

    const today = new Date().toISOString().slice(0, 10);
    chunk(view, `\n☀️ **오늘 (${today}) — 통합 대시보드**\n`);

    const runway = computeRunwayStatus();
    const customerStates = computeCustomerStates();
    const customers = Array.from(customerStates.values());
    const candidateStates = computeCandidateStates();
    const candidates = Array.from(candidateStates.values());

    let tasks: any[] = [];
    let tasksError: string | undefined;
    if (context) {
        try {
            const res = await listTasks(context, { showCompleted: false, maxResults: 300 });
            if (res.ok) tasks = res.tasks;
            else tasksError = res.error;
        } catch (e: any) { tasksError = e?.message || String(e); }
    }

    const urgent: string[] = [];
    if (runway.runwayMonths !== null && Number.isFinite(runway.runwayMonths) && runway.runwayMonths < 3) {
        urgent.push(`🔴 **런웨이 ${runway.runwayMonths.toFixed(1)}개월** — 즉시 자금 조달/절감 필요`);
    } else if (runway.runwayMonths !== null && Number.isFinite(runway.runwayMonths) && runway.runwayMonths < 6) {
        urgent.push(`🟡 런웨이 ${runway.runwayMonths.toFixed(1)}개월 — 자금 계획 점검 권장`);
    }
    const atRiskCustomers = customers.filter((c) => c.status === 'at-risk');
    const atRiskMrr = atRiskCustomers.reduce((s, c) => s + c.mrr, 0);
    if (atRiskCustomers.length > 0) {
        urgent.push(`⚠️ 위험 고객 **${atRiskCustomers.length}곳** (MRR ${_fmtKrw(atRiskMrr)}원/월 노출)`);
    }
    const upcomingRenewals = [...customers].filter((c) => c.status !== 'churned')
        .map((c) => ({ c, days: _daysUntil(c.renewalAt) }))
        .filter((x) => x.days !== null && x.days >= 0 && x.days <= 7);
    if (upcomingRenewals.length > 0) urgent.push(`🔔 7일 내 갱신 **${upcomingRenewals.length}건**`);
    const overdueTasks = tasks.filter((t) => t.due && t.due < today);
    if (overdueTasks.length > 0) urgent.push(`🚧 지연 작업 **${overdueTasks.length}건**`);
    const activeCandidate = candidates.filter((c) => !TERMINAL_STAGES.has(c.stage));
    const stalled = activeCandidate.filter((c) => (Date.now() - Date.parse(c.lastEventAt)) > 7 * 24 * 60 * 60 * 1000);
    if (stalled.length > 0) urgent.push(`⏰ 정체 후보 **${stalled.length}명** (7일+ 미변동)`);

    if (urgent.length === 0) chunk(view, '\n## ✅ 긴급 알림 없음\n');
    else {
        chunk(view, `\n## 🚨 긴급 (${urgent.length}건)\n`);
        for (const u of urgent) chunk(view, `- ${u}\n`);
    }

    if (brief) {
        chunk(view, '\n## 📋 오늘의 액션 (top 3)\n');
        const actions = _morningActions(runway, customers, upcomingRenewals, overdueTasks, stalled, candidates);
        for (const a of actions.slice(0, 3)) chunk(view, `- ${a}\n`);
        return true;
    }

    chunk(view, '\n## 💰 재무\n');
    if (runway.latestCash === null) {
        chunk(view, '- _데이터 없음_ — `/runway cash <금액>` 으로 시작\n');
    } else {
        chunk(view, `- 현금 **${_fmtKrw(runway.latestCash)}원** _(${(runway.latestCashAt || '').slice(0, 10)})_\n`);
        if (runway.effectiveBurn !== null) {
            chunk(view, `- 월 burn ${_fmtKrw(runway.effectiveBurn)}원 ${runway.explicitBurn !== null ? '_(수동)_' : '_(30일 실적)_'}\n`);
        }
        if (runway.runwayMonths !== null && Number.isFinite(runway.runwayMonths)) {
            const emoji = runway.runwayMonths < 3 ? '🔴' : runway.runwayMonths < 6 ? '🟡' : '🟢';
            chunk(view, `- 런웨이 ${emoji} **${runway.runwayMonths.toFixed(1)}개월**\n`);
        } else if (runway.runwayMonths !== null) {
            chunk(view, '- 런웨이 ♾️ 흑자 운영\n');
        }
    }

    chunk(view, '\n## 📒 고객\n');
    if (customers.length === 0) {
        chunk(view, '- _데이터 없음_ — `/customers add` 로 시작\n');
    } else {
        const active = customers.filter((c) => c.status === 'active');
        const totalMrr = [...active, ...atRiskCustomers].reduce((s, c) => s + c.mrr, 0);
        chunk(view, `- 총 MRR **${_fmtKrw(totalMrr)}원/월** _(연 ${_fmtKrw(totalMrr * 12)}원)_\n`);
        chunk(view, `- 활성 ${active.length} · 위험 ${atRiskCustomers.length} · 이탈 ${customers.length - active.length - atRiskCustomers.length}\n`);
        if (upcomingRenewals.length > 0) {
            for (const { c, days } of upcomingRenewals.slice(0, 5)) {
                const emoji = (days as number) <= 3 ? '🔴' : '🟡';
                chunk(view, `  - ${emoji} ${c.customerName} — D-${days} · ${_fmtKrw(c.mrr)}원/월\n`);
            }
        }
    }

    chunk(view, '\n## 👥 팀\n');
    if (tasksError) {
        chunk(view, `- _Tasks 조회 실패: ${tasksError}_\n`);
    } else if (tasks.length === 0) {
        chunk(view, '- _Tasks 없음_ — `/task` 로 등록 시작\n');
    } else {
        const memberOverdue = new Map<string, number>();
        const weekLater = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
        let weekCount = 0;
        for (const t of tasks) {
            const { owner } = parseTaskOwner(t.title, t.notes);
            const k = owner || '(미지정)';
            if (t.due && t.due < today) memberOverdue.set(k, (memberOverdue.get(k) || 0) + 1);
            if (t.due && t.due >= today && t.due <= weekLater) weekCount++;
        }
        chunk(view, `- 지연 ${overdueTasks.length}건 · 이번 주 ${weekCount}건 (전체 ${tasks.length})\n`);
        if (memberOverdue.size > 0) {
            const ranked = [...memberOverdue.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4);
            for (const [member, n] of ranked) chunk(view, `  - **@${member}** 지연 ${n}건\n`);
        }
    }

    chunk(view, '\n## 🎯 채용\n');
    if (candidates.length === 0) {
        chunk(view, '- _데이터 없음_\n');
    } else {
        const hired = candidates.filter((c) => c.stage === 'hired').length;
        chunk(view, `- 진행 중 ${activeCandidate.length}명 · 합격 ${hired}\n`);
        const stageCount = new Map<string, number>();
        for (const c of activeCandidate) stageCount.set(c.stage, (stageCount.get(c.stage) || 0) + 1);
        const stages = [...stageCount.entries()].sort((a, b) => (STAGE_ORDER[a[0]] ?? 50) - (STAGE_ORDER[b[0]] ?? 50));
        if (stages.length > 0) {
            const parts = stages.map(([s, n]) => `${_stageEmoji(s)} ${s} ${n}`);
            chunk(view, `  - ${parts.join(' · ')}\n`);
        }
        if (stalled.length > 0) {
            chunk(view, `- ⏰ 정체 ${stalled.length}명:\n`);
            for (const c of stalled.slice(0, 3)) {
                const days = Math.floor((Date.now() - Date.parse(c.lastEventAt)) / (24 * 60 * 60 * 1000));
                chunk(view, `  - ${c.candidateName} _(${c.role})_ · ${c.stage} · ${days}일 정체\n`);
            }
        }
    }

    chunk(view, '\n## 📋 오늘의 액션\n');
    const actions = _morningActions(runway, customers, upcomingRenewals, overdueTasks, stalled, candidates);
    if (actions.length === 0) {
        chunk(view, '- ✨ 특별한 조치 필요 없음. 깊은 작업 시간 확보 권장.\n');
    } else {
        for (const a of actions.slice(0, 5)) chunk(view, `- ${a}\n`);
    }
    return true;
}

function _morningActions(
    runway: ReturnType<typeof computeRunwayStatus>,
    customers: CustomerState[],
    upcomingRenewals: Array<{ c: CustomerState; days: number | null }>,
    overdueTasks: any[],
    stalled: CandidateState[],
    candidates: CandidateState[],
): string[] {
    const actions: string[] = [];
    if (runway.runwayMonths !== null && Number.isFinite(runway.runwayMonths) && runway.runwayMonths < 3) {
        actions.push(`💸 **자금 조달 계획** — 런웨이 ${runway.runwayMonths.toFixed(1)}개월. 투자자 미팅 / 비용 절감 즉시.`);
    }
    const atRisk = customers.filter((c) => c.status === 'at-risk').sort((a, b) => b.mrr - a.mrr);
    if (atRisk.length > 0) {
        const top = atRisk[0];
        actions.push(`📞 **${top.customerName}** 위험 대응 — MRR ${_fmtKrw(top.mrr)}원. 사유 점검 후 액션.`);
    }
    if (upcomingRenewals.length > 0) {
        const next = upcomingRenewals[0];
        actions.push(`📨 **${next.c.customerName}** 갱신 D-${next.days} — 갱신 의사 확인 / 가격 협의.`);
    }
    if (overdueTasks.length >= 5) {
        actions.push(`🚧 지연 작업 ${overdueTasks.length}건 — \`/blocked\` 로 멤버별 확인 후 우선순위 재조정.`);
    } else if (overdueTasks.length > 0) {
        actions.push(`🚧 지연 작업 ${overdueTasks.length}건 — \`/blocked\` 로 확인.`);
    }
    if (stalled.length > 0) {
        const next = stalled[0];
        const days = Math.floor((Date.now() - Date.parse(next.lastEventAt)) / (24 * 60 * 60 * 1000));
        actions.push(`👥 **${next.candidateName}** 채용 후속 — ${next.stage} 단계 ${days}일 정체.`);
    }
    const inboxCount = candidates.filter((c) => c.stage === 'inbox').length;
    if (inboxCount >= 5) actions.push(`📥 채용 inbox ${inboxCount}명 누적 — 스크리닝 시간 확보.`);
    return actions;
}

// ─── /evening — 하루 마무리 카드 ─────────────────────────────────────────

async function runEvening(arg: string, view: any, context?: vscode.ExtensionContext): Promise<boolean> {
    const today = new Date().toISOString().slice(0, 10);
    const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
    const dayStartMs = Date.parse(today + 'T00:00:00');

    chunk(view, `\n🌙 **오늘 (${today}) — 마무리 카드**\n`);

    let completedTasks: any[] = [];
    let tasksError: string | undefined;
    if (context) {
        try {
            const res = await listTasks(context, { showCompleted: true, maxResults: 300 });
            if (res.ok) {
                completedTasks = res.tasks.filter((t: any) => t.status === 'completed' && t.completed && Date.parse(t.completed) >= dayStartMs);
            } else { tasksError = res.error; }
        } catch (e: any) { tasksError = e?.message || String(e); }
    }

    const customerEvents = readCustomerEvents().filter((e) => Date.parse(e.timestamp) >= dayStartMs);
    const hireEvents = readHireEvents().filter((e) => Date.parse(e.timestamp) >= dayStartMs);
    const runwayToday = readRunway().filter((e) => Date.parse(e.timestamp) >= dayStartMs);

    chunk(view, '\n## ✅ 오늘의 진척\n');
    const progressEmpty = completedTasks.length === 0 && customerEvents.length === 0 && hireEvents.length === 0 && runwayToday.length === 0;
    if (progressEmpty) {
        chunk(view, '- _기록된 진척 없음._ (작업 완료 / 고객 이벤트 / 채용 이동 등이 오늘 입력되지 않음)\n');
        if (tasksError) chunk(view, `  _Tasks 조회 실패: ${tasksError}_\n`);
    } else {
        if (completedTasks.length > 0) {
            chunk(view, `\n### 작업 완료 (${completedTasks.length}건)\n`);
            const byOwner = new Map<string, any[]>();
            for (const t of completedTasks) {
                const { owner, displayTitle } = parseTaskOwner(t.title, t.notes);
                const k = owner || '(미지정)';
                if (!byOwner.has(k)) byOwner.set(k, []);
                byOwner.get(k)!.push({ title: displayTitle });
            }
            const ranked = [...byOwner.entries()].sort((a, b) => b[1].length - a[1].length);
            for (const [owner, list] of ranked) {
                chunk(view, `- **@${owner}** (${list.length}건)\n`);
                for (const t of list.slice(0, 5)) chunk(view, `  - ${t.title}\n`);
                if (list.length > 5) chunk(view, `  - _…+${list.length - 5}건_\n`);
            }
        }
        if (customerEvents.length > 0) {
            chunk(view, `\n### 📒 고객 이벤트 (${customerEvents.length}건)\n`);
            for (const e of customerEvents.slice(0, 10)) {
                const tagEmoji = e.type === 'add' ? '➕' : e.type === 'renew' ? '🔄' : e.type === 'risk' ? '⚠️' : e.type === 'churn' ? '💀' : '📝';
                const detail = e.type === 'add' || e.type === 'renew' || e.type === 'update'
                    ? (e.mrr !== undefined ? ` MRR ${_fmtKrw(e.mrr)}원` : '')
                    : (e.memo ? ` — ${e.memo.slice(0, 60)}` : '');
                chunk(view, `- ${tagEmoji} ${e.customerName} ${e.type}${detail}\n`);
            }
        }
        if (hireEvents.length > 0) {
            chunk(view, `\n### 🎯 채용 이벤트 (${hireEvents.length}건)\n`);
            for (const e of hireEvents.slice(0, 10)) {
                const stageNote = e.stage ? ` → ${e.stage}` : '';
                const memo = e.memo ? ` — ${e.memo.slice(0, 60)}` : '';
                chunk(view, `- ${e.candidateName} ${e.type}${stageNote}${memo}\n`);
            }
        }
        if (runwayToday.length > 0) {
            chunk(view, `\n### 💰 재무 기록 (${runwayToday.length}건)\n`);
            for (const e of runwayToday) {
                const typeLabel = e.type === 'snapshot' ? '잔고' : e.type === 'expense' ? '지출' : e.type === 'revenue' ? '수입' : '월 burn';
                chunk(view, `- ${typeLabel}: ${_fmtKrw(e.amount)}원${e.memo ? ` — ${e.memo}` : ''}\n`);
            }
        }
    }

    chunk(view, '\n## 🌅 내일 준비\n');
    let tomorrowTasks: any[] = [];
    if (context && !tasksError) {
        try {
            const res = await listTasks(context, { showCompleted: false, maxResults: 300 });
            if (res.ok) tomorrowTasks = res.tasks.filter((t: any) => t.due === tomorrow);
        } catch { /* ignore */ }
    }
    if (tomorrowTasks.length > 0) {
        chunk(view, `\n### 내일 마감 (${tomorrowTasks.length}건)\n`);
        for (const t of tomorrowTasks.slice(0, 8)) {
            const { owner, displayTitle } = parseTaskOwner(t.title, t.notes);
            chunk(view, `- **@${owner || '(미지정)'}** — ${displayTitle}\n`);
        }
    }

    const customers = Array.from(computeCustomerStates().values());
    const upcomingRenewals = customers.filter((c) => c.status !== 'churned')
        .map((c) => ({ c, days: _daysUntil(c.renewalAt) }))
        .filter((x) => x.days !== null && x.days >= 0 && x.days <= 7);
    if (upcomingRenewals.length > 0) {
        chunk(view, `\n### 🔔 7일 내 갱신 (${upcomingRenewals.length}건)\n`);
        for (const { c, days } of upcomingRenewals.slice(0, 5)) {
            const emoji = (days as number) <= 3 ? '🔴' : '🟡';
            chunk(view, `- ${emoji} ${c.customerName} D-${days} · ${_fmtKrw(c.mrr)}원/월\n`);
        }
    }

    const stalled = Array.from(computeCandidateStates().values())
        .filter((c) => !TERMINAL_STAGES.has(c.stage))
        .filter((c) => (Date.now() - Date.parse(c.lastEventAt)) > 7 * 24 * 60 * 60 * 1000);
    if (stalled.length > 0) {
        chunk(view, `\n### ⏰ 정체 후보 (${stalled.length}명)\n`);
        for (const c of stalled.slice(0, 3)) {
            const days = Math.floor((Date.now() - Date.parse(c.lastEventAt)) / (24 * 60 * 60 * 1000));
            chunk(view, `- ${c.candidateName} _(${c.role})_ · ${c.stage} · ${days}일 정체\n`);
        }
    }

    if (tomorrowTasks.length === 0 && upcomingRenewals.length === 0 && stalled.length === 0) {
        chunk(view, '- _내일 마감·갱신 임박·정체 후보 모두 없음._ ✨\n');
    }

    const reflections = [
        '오늘 가장 중요한 한 가지는 무엇이었나? 의도한 대로 됐나?',
        '내일 무엇을 안 하기로 했나? 안 할 일을 정해야 할 일이 또렷해진다.',
        '오늘 한 결정 중 일주일 뒤에도 옳을 결정은 어느 것인가?',
        '시간이 가장 많이 든 활동은 가장 영향력 있는 활동과 일치했나?',
        '오늘 멤버들에게 충분한 명확함을 줬나? 무엇을 미루지 않고 답해야 하나?',
        '에너지가 가장 좋았던 30분은 무엇을 하던 때였나?',
        '오늘 안 한 일 중 내일도 안 해도 되는 일은 무엇인가?',
        '리스크 한 가지를 꼽는다면? 그것에 대해 누구와 이야기해야 하나?',
    ];
    const idx = (Date.parse(today) / (24 * 60 * 60 * 1000)) % reflections.length;
    chunk(view, `\n## 🧭 회고\n> ${reflections[idx]}\n`);
    chunk(view, '\n_명령 한 줄로 기록 남기기:_ `/decisions` · `/feedback` · `/customers note` · `/hire note`\n');
    return true;
}

// ─── /cohort — MoM 추세 분석 ─────────────────────────────────────────────

interface MonthlyBucket {
    yearMonth: string;
    newCustomers: number;
    churnedCustomers: number;
    renewals: number;
    mrrDelta: number;
    expenseTotal: number;
    revenueTotal: number;
    cashSnapshots: number[];
}

function _yearMonth(iso: string): string {
    return (iso || '').slice(0, 7);
}

function _buildMonthlyBuckets(monthsBack: number): Map<string, MonthlyBucket> {
    const map = new Map<string, MonthlyBucket>();
    const now = new Date();
    for (let i = monthsBack - 1; i >= 0; i--) {
        const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
        const ym = d.toISOString().slice(0, 7);
        map.set(ym, {
            yearMonth: ym, newCustomers: 0, churnedCustomers: 0, renewals: 0,
            mrrDelta: 0, expenseTotal: 0, revenueTotal: 0, cashSnapshots: [],
        });
    }
    for (const e of readCustomerEvents()) {
        const ym = _yearMonth(e.timestamp);
        const b = map.get(ym);
        if (!b) continue;
        if (e.type === 'add') {
            b.newCustomers++;
            if (e.mrr) b.mrrDelta += e.mrr;
        } else if (e.type === 'churn') b.churnedCustomers++;
        else if (e.type === 'renew') b.renewals++;
    }
    for (const e of readRunway()) {
        const ym = _yearMonth(e.timestamp);
        const b = map.get(ym);
        if (!b) continue;
        if (e.type === 'expense') b.expenseTotal += e.amount;
        else if (e.type === 'revenue') b.revenueTotal += e.amount;
        else if (e.type === 'snapshot') b.cashSnapshots.push(e.amount);
    }
    return map;
}

function _cohortDashboard(view: any, monthsBack: number): void {
    const buckets = _buildMonthlyBuckets(monthsBack);
    if (buckets.size === 0) {
        chunk(view, '\nℹ️ 데이터 없음. `/customers add` / `/runway cash` 로 시작.\n');
        return;
    }
    const rows = Array.from(buckets.values());
    chunk(view, `\n📈 **/cohort — 최근 ${monthsBack}개월 추세**\n`);

    chunk(view, '\n## 고객 & MRR 추이\n');
    chunk(view, '| 월 | 신규 | 갱신 | 이탈 | MRR Δ |\n');
    chunk(view, '|---|---:|---:|---:|---:|\n');
    for (const r of rows) {
        chunk(view, `| ${r.yearMonth} | ${r.newCustomers} | ${r.renewals} | ${r.churnedCustomers} | ${r.mrrDelta > 0 ? '+' : ''}${_fmtKrw(r.mrrDelta)} |\n`);
    }
    const totNew = rows.reduce((s, r) => s + r.newCustomers, 0);
    const totChurn = rows.reduce((s, r) => s + r.churnedCustomers, 0);
    const totMrr = rows.reduce((s, r) => s + r.mrrDelta, 0);
    chunk(view, `\n- **누적 ${monthsBack}개월**: 신규 +${totNew} · 이탈 -${totChurn} · 순 ${totNew - totChurn >= 0 ? '+' : ''}${totNew - totChurn}\n`);
    chunk(view, `- **MRR 순증**: ${totMrr >= 0 ? '+' : ''}${_fmtKrw(totMrr)}원/월 _(추가된 신규 MRR 만, 이탈로 인한 감소는 history 부재로 미반영)_\n`);
    const avgNew = totNew / monthsBack;
    const avgChurn = totChurn / monthsBack;
    if (avgNew > 0 || avgChurn > 0) {
        chunk(view, `- 월평균 신규 ${avgNew.toFixed(1)}곳, 월평균 이탈 ${avgChurn.toFixed(1)}곳`);
        if (totNew > 0) chunk(view, ` (이탈/신규 비율 ${((totChurn / totNew) * 100).toFixed(0)}%)\n`);
        else chunk(view, '\n');
    }

    chunk(view, '\n## 재무 추이\n');
    chunk(view, '| 월 | 지출 | 수입 | 순 burn | 월말 잔고 |\n');
    chunk(view, '|---|---:|---:|---:|---:|\n');
    for (const r of rows) {
        const netBurn = r.expenseTotal - r.revenueTotal;
        const lastCash = r.cashSnapshots.length > 0 ? r.cashSnapshots[r.cashSnapshots.length - 1] : null;
        const cashCell = lastCash !== null ? _fmtKrw(lastCash) : '-';
        chunk(view, `| ${r.yearMonth} | ${_fmtKrw(r.expenseTotal)} | ${_fmtKrw(r.revenueTotal)} | ${netBurn > 0 ? '+' : ''}${_fmtKrw(netBurn)} | ${cashCell} |\n`);
    }
    const totExp = rows.reduce((s, r) => s + r.expenseTotal, 0);
    const totRev = rows.reduce((s, r) => s + r.revenueTotal, 0);
    const totBurn = totExp - totRev;
    const avgBurn = totBurn / monthsBack;
    chunk(view, `\n- **${monthsBack}개월 누계**: 지출 ${_fmtKrw(totExp)} · 수입 ${_fmtKrw(totRev)} · 순 burn ${totBurn > 0 ? '+' : ''}${_fmtKrw(totBurn)}\n`);
    chunk(view, `- **월평균 burn**: ${_fmtKrw(avgBurn)}원/월\n`);

    chunk(view, '\n## 💡 인사이트\n');
    const insights: string[] = [];
    if (monthsBack >= 6) {
        const recent3 = rows.slice(-3);
        const prior3 = rows.slice(-6, -3);
        const recentNew = recent3.reduce((s, r) => s + r.newCustomers, 0);
        const priorNew = prior3.reduce((s, r) => s + r.newCustomers, 0);
        if (recentNew > priorNew * 1.2) insights.push('🟢 최근 3개월 신규 획득 가속 (이전 3개월 대비 +20%↑)');
        else if (recentNew < priorNew * 0.8 && priorNew >= 2) insights.push('🟡 최근 3개월 신규 획득 둔화 (이전 3개월 대비 -20%↓)');
        const recentBurn = recent3.reduce((s, r) => s + (r.expenseTotal - r.revenueTotal), 0);
        const priorBurn = prior3.reduce((s, r) => s + (r.expenseTotal - r.revenueTotal), 0);
        if (priorBurn > 0 && recentBurn > priorBurn * 1.3) insights.push('🔴 최근 3개월 burn 가속 (이전 3개월 대비 +30%↑) — 비용 점검 권장');
    }
    if (avgBurn > 0 && totRev > 0) {
        const coverage = totRev / totExp;
        if (coverage > 0.8) insights.push(`🟢 매출 커버리지 ${(coverage * 100).toFixed(0)}% — 흑자 진입 임박`);
        else if (coverage < 0.2 && totExp > 0) insights.push(`🟡 매출 커버리지 ${(coverage * 100).toFixed(0)}% — 매출 기반 약함`);
    }
    if (insights.length === 0) chunk(view, '- _데이터 부족 또는 추세 신호 약함._ 더 누적되면 인사이트 표시.\n');
    else for (const i of insights) chunk(view, `- ${i}\n`);

    chunk(view, '\n_데이터 출처: `.astra/customers.jsonl` + `.astra/runway.jsonl`. 더 많은 이벤트 누적 시 추세 정확도↑._\n');
}

async function runCohort(arg: string, view: any): Promise<boolean> {
    const trimmed = arg.trim().toLowerCase();
    if (trimmed === 'help' || trimmed === '?') {
        chunk(view, [
            '\n📈 **/cohort — MoM 추세 분석**',
            '',
            '사용법:',
            '  `/cohort`                — 최근 6개월 추세 (기본)',
            '  `/cohort yearly`         — 최근 12개월',
            '  `/cohort <N>`            — 최근 N개월 (1~24)',
            '',
            '데이터 출처: `/customers` events + `/runway` events 의 timestamp 월별 그룹핑.',
            '표시 항목: 신규/갱신/이탈 + MRR 변화 + 지출/수입/burn + 월말 잔고 + 인사이트 한 줄.\n',
        ].join('\n'));
        return true;
    }
    let monthsBack = 6;
    if (trimmed === 'yearly' || trimmed === 'year') monthsBack = 12;
    else if (/^\d+$/.test(trimmed)) monthsBack = Math.max(1, Math.min(24, parseInt(trimmed, 10)));
    _cohortDashboard(view, monthsBack);
    return true;
}

// ─── 등록 ─────────────────────────────────────────────────────────────────

registerSlashCommand({ name: '/morning', description: '매일 아침 통합 대시보드 — runway/customers/tasks/hire 핵심 한 화면 + 오늘의 액션', handler: runMorning });
registerSlashCommand({ name: '/evening', description: '하루 마무리 카드 — 오늘의 진척 + 내일 준비 + 회고 한 줄', handler: runEvening });
registerSlashCommand({ name: '/cohort', description: 'MoM 추세 분석 — customers + runway events 월별 그룹핑 (신규/이탈/MRR/burn)', handler: runCohort });
