/**
 * TeamOps Trackers — /runway · /customers · /hire (event-sourced 트래커 3종).
 *
 * v2.2.196 에서 slashRouter.ts 에서 분리. 모두 `.astra/*.jsonl` event log 를
 * 읽고 (createEventStore via 각 store 모듈) 대시보드 / 수정 명령 제공.
 *
 * 공통 헬퍼 (fmtKrw / parseAmount / daysUntil / stageEmoji / STAGE_ORDER /
 * TERMINAL_STAGES) 는 `./_shared.ts` 에서.
 */

import { registerSlashCommand, chunk } from '../../datacollect/slashRouter';
import {
    fmtKrw, parseAmount, daysUntil,
    stageEmoji, STAGE_ORDER, TERMINAL_STAGES,
} from './_shared';
import {
    appendRunway, readRunway, getRunwayFilePath, computeRunwayStatus,
    type RunwayEntry, type RunwayEntryType,
} from '../../runway/runwayStore';
import {
    appendEvent as appendCustomerEvent, readEvents as readCustomerEvents,
    getCustomersFilePath, customerIdFromName, computeCustomerStates,
    type CustomerEvent, type CustomerState,
} from '../../customers/customersStore';
import {
    appendHireEvent, readHireEvents, getHireFilePath, candidateIdFromName,
    computeCandidateStates, type HireEvent, type CandidateState,
} from '../../hire/hireStore';

// ─── /runway ──────────────────────────────────────────────────────────────

function _runwayShowStatus(view: any): void {
    const s = computeRunwayStatus();
    chunk(view, '\n💰 **/runway — 현금 / 월 소진율 / 런웨이**\n');
    if (s.latestCash === null) {
        chunk(view, '\nℹ️ 잔고 기록 없음. 시작: `/runway cash 5000만` (현재 통장 잔고 입력)\n');
        return;
    }
    const cashDate = (s.latestCashAt || '').slice(0, 10);
    chunk(view, `\n## 현재 현금\n- **${fmtKrw(s.latestCash)}원** _(기준: ${cashDate})_\n`);

    chunk(view, '\n## 월 소진율 (burn)\n');
    if (s.explicitBurn !== null) {
        chunk(view, `- **${fmtKrw(s.explicitBurn)}원/월** _(수동 설정)_\n`);
    } else if (s.computedBurn !== null) {
        const ann = s.last30Days < 30 ? ` _(${s.last30Days}일 데이터 → 30일 환산)_` : ' _(최근 30일 실적)_';
        chunk(view, `- **${fmtKrw(s.computedBurn)}원/월**${ann}\n`);
        chunk(view, `  · 지출 ${fmtKrw(s.last30Expense)}원 − 수입 ${fmtKrw(s.last30Revenue)}원\n`);
    } else {
        chunk(view, '- _데이터 부족_ — `/runway burn 1500만` 또는 `/runway expense 300만 급여` 로 기록\n');
    }

    chunk(view, '\n## 런웨이\n');
    if (s.runwayMonths === null) {
        chunk(view, '- _계산 불가_ (잔고 또는 burn 미정)\n');
    } else if (!Number.isFinite(s.runwayMonths)) {
        chunk(view, '- ♾️ **흑자 운영** (지출 ≤ 수입)\n');
    } else {
        const m = s.runwayMonths;
        const emoji = m < 3 ? '🔴' : m < 6 ? '🟡' : '🟢';
        const months = m.toFixed(1);
        const exitDate = new Date(Date.now() + m * 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
        chunk(view, `- ${emoji} **${months}개월** _(예상 소진: ${exitDate})_\n`);
        if (m < 3) chunk(view, '  · ⚠️ **3개월 미만** — 즉시 자금 조달 또는 비용 절감 필요\n');
        else if (m < 6) chunk(view, '  · ⚠️ **6개월 미만** — 자금 계획 점검 권장\n');
    }

    chunk(view, `\n_누적 ${s.totalEntries}건 기록. \`/runway log\` 로 전체 보기, \`/runway path\` 로 파일 위치._\n`);
}

function _runwayLog(view: any, limit: number): void {
    const all = readRunway().slice().sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || '')).slice(0, limit);
    if (all.length === 0) { chunk(view, '\nℹ️ 기록 없음. `/runway cash 5000만` 으로 시작.\n'); return; }
    chunk(view, `\n📒 **최근 ${all.length}건** (최신순)\n\n`);
    const emoji: Record<RunwayEntryType, string> = {
        snapshot: '💰', expense: '💸', revenue: '💵', burn: '🔥',
    };
    for (const e of all) {
        const date = (e.timestamp || '').slice(0, 10);
        const cat = e.category ? ` [${e.category}]` : '';
        const memo = e.memo ? ` — ${e.memo}` : '';
        const typeLabel = e.type === 'snapshot' ? '잔고' : e.type === 'expense' ? '지출' : e.type === 'revenue' ? '수입' : '월 burn';
        chunk(view, `- ${emoji[e.type]} \`${date}\` ${typeLabel}: ${fmtKrw(e.amount)}원${cat}${memo}\n`);
    }
}

async function runRunway(arg: string, view: any): Promise<boolean> {
    const trimmed = arg.trim();
    if (!trimmed) { _runwayShowStatus(view); return true; }

    const parts = trimmed.split(/\s+/);
    const sub = parts[0].toLowerCase();

    if (sub === 'help' || sub === '?') {
        chunk(view, [
            '\n💰 **/runway — 현금 / 월 소진율 / 런웨이**',
            '',
            '사용법:',
            '  `/runway`                       — 현재 상태 카드 (현금 / burn / 남은 개월수)',
            '  `/runway cash <금액> [메모]`     — 통장 잔고 스냅샷 기록',
            '  `/runway expense <금액> [메모]`  — 지출 기록 (월 burn 자동 계산에 반영)',
            '  `/runway revenue <금액> [메모]`  — 수입 기록 (burn 상쇄)',
            '  `/runway burn <월 금액>`         — 월 소진율 수동 설정 (자동 계산보다 우선)',
            '  `/runway log [N]`               — 최근 N건 기록 (기본 20)',
            '  `/runway path`                  — .jsonl 파일 경로',
            '',
            '금액 단위: `5000만` / `1.5억` / `300000` 모두 OK. 소수점·콤마 허용.',
            '저장 위치: `<workspace>/.astra/runway.jsonl` (로컬 only, 외부 안 보냄).\n',
        ].join('\n'));
        return true;
    }

    if (sub === 'path') {
        const p = getRunwayFilePath();
        if (!p) { chunk(view, '\n⚠️ 워크스페이스 폴더 없음 — 저장 위치 결정 불가.\n'); return true; }
        const count = readRunway().length;
        chunk(view, `\n📂 \`${p}\`\n  · 누적 ${count}건 (.jsonl 형식, 한 줄=한 항목)\n  · 사람이 직접 편집 가능.\n`);
        return true;
    }

    if (sub === 'log') {
        const n = parts[1] ? parseInt(parts[1], 10) : 20;
        _runwayLog(view, Number.isFinite(n) && n > 0 ? n : 20);
        return true;
    }

    if (sub === 'cash' || sub === 'expense' || sub === 'revenue' || sub === 'burn') {
        const amount = parseAmount(parts[1] || '');
        if (amount === null) {
            chunk(view, `\n❌ 금액 파싱 실패: "${parts[1] || ''}". 예: \`5000만\` / \`1.5억\` / \`300000\`\n`);
            return true;
        }
        const memo = parts.slice(2).join(' ').trim() || undefined;
        const typeMap: Record<string, RunwayEntryType> = { cash: 'snapshot', expense: 'expense', revenue: 'revenue', burn: 'burn' };
        const entry: RunwayEntry = {
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            timestamp: new Date().toISOString(),
            type: typeMap[sub],
            amount,
            currency: 'KRW',
            memo,
        };
        const res = appendRunway(entry);
        if (!res.ok) { chunk(view, `\n❌ 저장 실패: ${res.error}\n`); return true; }
        const labels: Record<string, string> = { cash: '잔고 스냅샷', expense: '지출', revenue: '수입', burn: '월 burn 설정' };
        chunk(view, `\n✅ ${labels[sub]} 기록: **${fmtKrw(amount)}원**${memo ? ` — ${memo}` : ''}\n`);
        _runwayShowStatus(view);
        return true;
    }

    chunk(view, `\n❌ 알 수 없는 서브명령: "${sub}". \`/runway help\` 참조.\n`);
    return true;
}

// ─── /customers ───────────────────────────────────────────────────────────

function _customersDashboard(view: any): void {
    const states = computeCustomerStates();
    const all = Array.from(states.values());
    if (all.length === 0) {
        chunk(view, '\n📒 **/customers — 고객사 / MRR / 갱신**\n\nℹ️ 등록된 고객 없음. 시작: `/customers add <이름> <MRR> [갱신일] [요금제]`\n예: `/customers add 큐브앤코 200만 2026-12-01 enterprise`\n');
        return;
    }

    const active = all.filter((c) => c.status === 'active');
    const atRisk = all.filter((c) => c.status === 'at-risk');
    const churned = all.filter((c) => c.status === 'churned');
    const totalMrr = active.reduce((sum, c) => sum + c.mrr, 0) + atRisk.reduce((sum, c) => sum + c.mrr, 0);
    const riskMrr = atRisk.reduce((sum, c) => sum + c.mrr, 0);

    chunk(view, '\n📒 **/customers — 고객사 대시보드**\n');
    chunk(view, '\n## 요약\n');
    chunk(view, `- **MRR**: ${fmtKrw(totalMrr)}원/월 _(연 ${fmtKrw(totalMrr * 12)}원)_\n`);
    chunk(view, `- 활성 ${active.length}곳 · 위험 ${atRisk.length}곳 · 이탈 ${churned.length}곳\n`);
    if (riskMrr > 0) chunk(view, `- ⚠️ **위험 MRR**: ${fmtKrw(riskMrr)}원/월 _(전체의 ${((riskMrr / totalMrr) * 100).toFixed(0)}%)_\n`);

    const upcoming = [...active, ...atRisk]
        .map((c) => ({ c, days: daysUntil(c.renewalAt) }))
        .filter((x) => x.days !== null && x.days >= 0 && x.days <= 30)
        .sort((a, b) => (a.days as number) - (b.days as number));
    if (upcoming.length > 0) {
        chunk(view, '\n## 🔔 30일 내 갱신\n');
        for (const { c, days } of upcoming) {
            const emoji = c.status === 'at-risk' ? '⚠️' : (days as number) <= 7 ? '🔴' : (days as number) <= 14 ? '🟡' : '🟢';
            chunk(view, `- ${emoji} **${c.customerName}** — ${c.renewalAt} _(D-${days})_ · ${fmtKrw(c.mrr)}원/월\n`);
        }
    }

    if (atRisk.length > 0) {
        chunk(view, '\n## ⚠️ 위험 고객\n');
        for (const c of atRisk.sort((a, b) => b.mrr - a.mrr)) {
            const lastRisk = c.notes.slice().reverse().find((n) => n.type === 'risk');
            chunk(view, `- **${c.customerName}** — ${fmtKrw(c.mrr)}원/월${lastRisk ? ` · ${lastRisk.memo.slice(0, 60)}` : ''}\n`);
        }
    }

    if (active.length > 0) {
        chunk(view, '\n## 활성 고객 (MRR 순)\n');
        const top = active.slice().sort((a, b) => b.mrr - a.mrr).slice(0, 10);
        for (const c of top) {
            const renewalNote = c.renewalAt ? ` · 갱신 ${c.renewalAt}` : '';
            chunk(view, `- ${c.customerName} — ${fmtKrw(c.mrr)}원/월${renewalNote}\n`);
        }
        if (active.length > 10) chunk(view, `- _…+${active.length - 10}곳_\n`);
    }

    chunk(view, `\n_누적 이벤트 ${readCustomerEvents().length}건. \`/customers help\` 로 명령어._\n`);
}

function _customersList(view: any, filter: string | undefined): void {
    const states = computeCustomerStates();
    let all = Array.from(states.values());
    if (filter === 'active' || filter === 'at-risk' || filter === 'risk' || filter === 'churned') {
        const target = filter === 'risk' ? 'at-risk' : filter;
        all = all.filter((c) => c.status === target);
    }
    if (all.length === 0) { chunk(view, `\nℹ️ 매치 없음${filter ? ` (필터: ${filter})` : ''}.\n`); return; }
    chunk(view, `\n📋 **고객 목록 (${all.length}곳${filter ? `, ${filter}` : ''})**\n\n`);
    const sorted = all.slice().sort((a, b) => b.mrr - a.mrr);
    for (const c of sorted) {
        const emoji = c.status === 'active' ? '🟢' : c.status === 'at-risk' ? '🟡' : '🔴';
        const renewalNote = c.renewalAt ? ` · 갱신 ${c.renewalAt}` : '';
        const planNote = c.plan ? ` · ${c.plan}` : '';
        chunk(view, `- ${emoji} **${c.customerName}** — ${fmtKrw(c.mrr)}원/월${planNote}${renewalNote}\n`);
    }
}

function _customersShow(view: any, name: string): void {
    const states = computeCustomerStates();
    const cid = customerIdFromName(name);
    const c = states.get(cid);
    if (!c) {
        const candidates = Array.from(states.values()).filter((x) => x.customerName.toLowerCase().includes(name.toLowerCase()));
        if (candidates.length === 0) { chunk(view, `\n❌ "${name}" 매치 0건.\n`); return; }
        if (candidates.length > 1) {
            chunk(view, `\nℹ️ "${name}" 부분 매치 ${candidates.length}곳:\n`);
            for (const x of candidates) chunk(view, `- ${x.customerName}\n`);
            return;
        }
        return _customersShow(view, candidates[0].customerName);
    }

    const emoji = c.status === 'active' ? '🟢' : c.status === 'at-risk' ? '🟡' : '🔴';
    chunk(view, `\n${emoji} **${c.customerName}** _(${c.status})_\n`);
    chunk(view, `\n- MRR: **${fmtKrw(c.mrr)}원/월** _(연 ${fmtKrw(c.mrr * 12)}원)_\n`);
    if (c.plan) chunk(view, `- 요금제: ${c.plan}\n`);
    if (c.renewalAt) {
        const d = daysUntil(c.renewalAt);
        const dn = d !== null ? (d >= 0 ? `D-${d}` : `${-d}일 지남`) : '';
        chunk(view, `- 갱신일: ${c.renewalAt} _(${dn})_\n`);
    }
    chunk(view, `- 시작: ${(c.startedAt || '').slice(0, 10)} · 누적 이벤트 ${c.eventCount}건\n`);

    if (c.notes.length > 0) {
        chunk(view, `\n## 메모·이벤트 (${c.notes.length}건, 최신순)\n`);
        const recent = c.notes.slice().reverse().slice(0, 10);
        for (const n of recent) {
            const date = (n.timestamp || '').slice(0, 10);
            const tagEmoji = n.type === 'risk' ? '⚠️' : n.type === 'churn' ? '💀' : '📝';
            chunk(view, `- ${tagEmoji} \`${date}\` ${n.memo}\n`);
        }
        if (c.notes.length > 10) chunk(view, `- _…+${c.notes.length - 10}건_\n`);
    }
}

async function runCustomers(arg: string, view: any): Promise<boolean> {
    const trimmed = arg.trim();
    if (!trimmed) { _customersDashboard(view); return true; }

    const parts = trimmed.split(/\s+/);
    const sub = parts[0].toLowerCase();

    if (sub === 'help' || sub === '?') {
        chunk(view, [
            '\n📒 **/customers — 고객사 / MRR / 갱신 트래커**',
            '',
            '사용법:',
            '  `/customers`                                       — 대시보드 (MRR, 위험, 갱신 임박)',
            '  `/customers add <이름> <MRR> [갱신일] [요금제]`     — 신규 등록',
            '  `/customers update <이름> mrr=<금액> renewal=<날짜>`— 정보 수정',
            '  `/customers renew <이름> <새 갱신일> [새 MRR]`     — 갱신 처리 (active 로 복귀)',
            '  `/customers risk <이름> <사유>`                    — 위험 표시',
            '  `/customers churn <이름> <사유>`                   — 이탈 처리 (MRR=0)',
            '  `/customers note <이름> <텍스트>`                  — 자유 메모',
            '  `/customers show <이름>`                           — 상세 (부분 매치 OK)',
            '  `/customers list [active/risk/churned]`            — 필터 목록',
            '  `/customers path`                                  — .jsonl 파일 경로',
            '',
            'MRR 금액 단위: `200만` / `1.5억` / `300000` 모두 OK.',
            '갱신일: `YYYY-MM-DD` (예: `2026-12-01`).',
            '저장: `<workspace>/.astra/customers.jsonl` (로컬 only, 외부 안 보냄).\n',
        ].join('\n'));
        return true;
    }

    if (sub === 'path') {
        const p = getCustomersFilePath();
        if (!p) { chunk(view, '\n⚠️ 워크스페이스 폴더 없음.\n'); return true; }
        chunk(view, `\n📂 \`${p}\`\n  · 누적 이벤트 ${readCustomerEvents().length}건 (.jsonl).\n`);
        return true;
    }

    if (sub === 'list') { _customersList(view, parts[1]?.toLowerCase()); return true; }

    if (sub === 'show') {
        const name = parts.slice(1).join(' ').trim();
        if (!name) { chunk(view, '\n❌ 사용법: `/customers show <이름>`\n'); return true; }
        _customersShow(view, name);
        return true;
    }

    if (sub === 'add') {
        const name = parts[1];
        const mrrToken = parts[2];
        const renewalToken = parts[3];
        const planToken = parts[4];
        if (!name || !mrrToken) {
            chunk(view, '\n❌ 사용법: `/customers add <이름> <MRR> [갱신일] [요금제]`\n예: `/customers add 큐브앤코 200만 2026-12-01 enterprise`\n');
            return true;
        }
        const mrr = parseAmount(mrrToken);
        if (mrr === null) { chunk(view, `\n❌ MRR 파싱 실패: "${mrrToken}"\n`); return true; }
        const event: CustomerEvent = {
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            timestamp: new Date().toISOString(),
            customerId: customerIdFromName(name),
            customerName: name,
            type: 'add',
            mrr,
            renewalAt: renewalToken && /^\d{4}-\d{2}-\d{2}$/.test(renewalToken) ? renewalToken : undefined,
            plan: planToken,
        };
        const res = appendCustomerEvent(event);
        if (!res.ok) { chunk(view, `\n❌ 저장 실패: ${res.error}\n`); return true; }
        chunk(view, `\n✅ **${name}** 등록 — MRR ${fmtKrw(mrr)}원/월${event.renewalAt ? ` · 갱신 ${event.renewalAt}` : ''}${event.plan ? ` · ${event.plan}` : ''}\n`);
        return true;
    }

    if (sub === 'renew') {
        const name = parts[1];
        const newRenewal = parts[2];
        const newMrrToken = parts[3];
        if (!name || !newRenewal) { chunk(view, '\n❌ 사용법: `/customers renew <이름> <새 갱신일> [새 MRR]`\n'); return true; }
        if (!/^\d{4}-\d{2}-\d{2}$/.test(newRenewal)) { chunk(view, `\n❌ 갱신일 형식: YYYY-MM-DD (입력: "${newRenewal}")\n`); return true; }
        const newMrr = newMrrToken ? parseAmount(newMrrToken) : null;
        const event: CustomerEvent = {
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            timestamp: new Date().toISOString(),
            customerId: customerIdFromName(name),
            customerName: name,
            type: 'renew',
            renewalAt: newRenewal,
            mrr: newMrr ?? undefined,
        };
        const res = appendCustomerEvent(event);
        if (!res.ok) { chunk(view, `\n❌ 저장 실패: ${res.error}\n`); return true; }
        chunk(view, `\n🔄 **${name}** 갱신 — ${newRenewal}${newMrr !== null ? ` · MRR ${fmtKrw(newMrr)}원/월` : ''}\n`);
        return true;
    }

    if (sub === 'risk' || sub === 'churn' || sub === 'note') {
        const name = parts[1];
        const memo = parts.slice(2).join(' ').trim();
        if (!name || !memo) {
            chunk(view, `\n❌ 사용법: \`/customers ${sub} <이름> <${sub === 'note' ? '텍스트' : '사유'}>\`\n`);
            return true;
        }
        const event: CustomerEvent = {
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            timestamp: new Date().toISOString(),
            customerId: customerIdFromName(name),
            customerName: name,
            type: sub,
            memo,
        };
        const res = appendCustomerEvent(event);
        if (!res.ok) { chunk(view, `\n❌ 저장 실패: ${res.error}\n`); return true; }
        const emoji = sub === 'risk' ? '⚠️' : sub === 'churn' ? '💀' : '📝';
        const label = sub === 'risk' ? '위험 표시' : sub === 'churn' ? '이탈 처리' : '메모 추가';
        chunk(view, `\n${emoji} **${name}** ${label}: ${memo}\n`);
        return true;
    }

    if (sub === 'update') {
        const name = parts[1];
        if (!name) { chunk(view, '\n❌ 사용법: `/customers update <이름> mrr=<금액> renewal=<YYYY-MM-DD> plan=<요금제>`\n'); return true; }
        const rest = parts.slice(2);
        const event: CustomerEvent = {
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            timestamp: new Date().toISOString(),
            customerId: customerIdFromName(name),
            customerName: name,
            type: 'update',
        };
        let touched = false;
        for (const kv of rest) {
            const m = kv.match(/^(\w+)=(.+)$/);
            if (!m) continue;
            const k = m[1].toLowerCase();
            const v = m[2];
            if (k === 'mrr') {
                const n = parseAmount(v);
                if (n !== null) { event.mrr = n; touched = true; }
            } else if (k === 'renewal' || k === 'renewalat') {
                if (/^\d{4}-\d{2}-\d{2}$/.test(v)) { event.renewalAt = v; touched = true; }
            } else if (k === 'plan') {
                event.plan = v; touched = true;
            }
        }
        if (!touched) { chunk(view, '\n❌ 변경할 필드 없음. 예: `/customers update 큐브앤코 mrr=300만 renewal=2026-12-15`\n'); return true; }
        const res = appendCustomerEvent(event);
        if (!res.ok) { chunk(view, `\n❌ 저장 실패: ${res.error}\n`); return true; }
        const changes: string[] = [];
        if (event.mrr !== undefined) changes.push(`MRR=${fmtKrw(event.mrr)}원`);
        if (event.renewalAt) changes.push(`갱신=${event.renewalAt}`);
        if (event.plan) changes.push(`요금제=${event.plan}`);
        chunk(view, `\n✏️ **${name}** 업데이트: ${changes.join(' · ')}\n`);
        return true;
    }

    chunk(view, `\n❌ 알 수 없는 서브명령: "${sub}". \`/customers help\` 참조.\n`);
    return true;
}

// ─── /hire ────────────────────────────────────────────────────────────────

function _hireDashboard(view: any): void {
    const states = computeCandidateStates();
    const all = Array.from(states.values());
    if (all.length === 0) {
        chunk(view, '\n👥 **/hire — 채용 파이프라인**\n\nℹ️ 등록된 후보 없음. 시작: `/hire add <이름> <역할>`\n예: `/hire add 김개발 백엔드`\n');
        return;
    }

    const active = all.filter((c) => !TERMINAL_STAGES.has(c.stage));
    const hired = all.filter((c) => c.stage === 'hired');
    const rejected = all.filter((c) => c.stage === 'rejected' || c.stage === 'declined');

    chunk(view, '\n👥 **/hire — 채용 파이프라인**\n');
    chunk(view, '\n## 요약\n');
    chunk(view, `- 진행 중 **${active.length}명** · 합격 ${hired.length}명 · 종료 ${rejected.length}명\n`);

    const byRole = new Map<string, CandidateState[]>();
    for (const c of active) {
        const role = c.role || '미지정';
        if (!byRole.has(role)) byRole.set(role, []);
        byRole.get(role)!.push(c);
    }
    if (byRole.size > 0) {
        chunk(view, '\n## 역할별 진행\n');
        for (const [role, cs] of byRole) {
            chunk(view, `- **${role}**: ${cs.length}명\n`);
        }
    }

    const byStage = new Map<string, CandidateState[]>();
    for (const c of active) {
        if (!byStage.has(c.stage)) byStage.set(c.stage, []);
        byStage.get(c.stage)!.push(c);
    }
    const sortedStages = Array.from(byStage.keys()).sort((a, b) => (STAGE_ORDER[a] ?? 50) - (STAGE_ORDER[b] ?? 50));
    if (sortedStages.length > 0) {
        chunk(view, '\n## 단계별\n');
        for (const stage of sortedStages) {
            const list = byStage.get(stage)!;
            chunk(view, `\n### ${stageEmoji(stage)} ${stage} (${list.length})\n`);
            for (const c of list.slice().sort((a, b) => (b.lastEventAt || '').localeCompare(a.lastEventAt || ''))) {
                const daysIn = Math.floor((Date.now() - Date.parse(c.lastEventAt)) / (24 * 60 * 60 * 1000));
                const stale = daysIn > 7 ? ` ⏰ ${daysIn}일 정체` : '';
                const salary = c.salary !== undefined ? ` · ${fmtKrw(c.salary)}원` : '';
                chunk(view, `- ${c.candidateName} _(${c.role || '미지정'})_${salary}${stale}\n`);
            }
        }
    }

    if (hired.length > 0) {
        chunk(view, `\n## 🎉 최근 합격 (${hired.length}명)\n`);
        const recent = hired.slice().sort((a, b) => (b.lastEventAt || '').localeCompare(a.lastEventAt || '')).slice(0, 5);
        for (const c of recent) {
            chunk(view, `- ${c.candidateName} _(${c.role})_ — ${(c.lastEventAt || '').slice(0, 10)}\n`);
        }
    }

    chunk(view, `\n_누적 이벤트 ${readHireEvents().length}건. \`/hire help\` 로 명령어._\n`);
}

function _hireList(view: any, filter: string | undefined): void {
    const states = computeCandidateStates();
    let all = Array.from(states.values());
    if (filter) {
        const f = filter.toLowerCase();
        if (f === 'active') all = all.filter((c) => !TERMINAL_STAGES.has(c.stage));
        else if (f === 'closed' || f === 'terminal') all = all.filter((c) => TERMINAL_STAGES.has(c.stage));
        else all = all.filter((c) => c.stage === f || (c.role || '').toLowerCase() === f);
    }
    if (all.length === 0) { chunk(view, `\nℹ️ 매치 없음${filter ? ` (필터: ${filter})` : ''}.\n`); return; }
    chunk(view, `\n📋 **후보 목록 (${all.length}명${filter ? `, ${filter}` : ''})**\n\n`);
    const sorted = all.slice().sort((a, b) => (STAGE_ORDER[a.stage] ?? 50) - (STAGE_ORDER[b.stage] ?? 50));
    for (const c of sorted) {
        const daysIn = Math.floor((Date.now() - Date.parse(c.lastEventAt)) / (24 * 60 * 60 * 1000));
        chunk(view, `- ${stageEmoji(c.stage)} **${c.candidateName}** _(${c.role || '미지정'})_ · ${c.stage} · ${daysIn}일 전\n`);
    }
}

function _hireShow(view: any, name: string): void {
    const states = computeCandidateStates();
    const cid = candidateIdFromName(name);
    let c = states.get(cid);
    if (!c) {
        const candidates = Array.from(states.values()).filter((x) => x.candidateName.toLowerCase().includes(name.toLowerCase()));
        if (candidates.length === 0) { chunk(view, `\n❌ "${name}" 매치 0건.\n`); return; }
        if (candidates.length > 1) {
            chunk(view, `\nℹ️ "${name}" 부분 매치 ${candidates.length}명:\n`);
            for (const x of candidates) chunk(view, `- ${x.candidateName} (${x.role})\n`);
            return;
        }
        c = candidates[0];
    }

    chunk(view, `\n${stageEmoji(c.stage)} **${c.candidateName}** _(${c.role || '미지정'})_\n`);
    chunk(view, `\n- 단계: **${c.stage}**\n`);
    if (c.salary !== undefined) chunk(view, `- 제안 연봉: ${fmtKrw(c.salary)}원\n`);
    chunk(view, `- 시작: ${(c.addedAt || '').slice(0, 10)} · 최근 변경: ${(c.lastEventAt || '').slice(0, 10)} · 이벤트 ${c.eventCount}건\n`);

    if (c.notes.length > 0) {
        chunk(view, `\n## 메모·이벤트 (${c.notes.length}건)\n`);
        const recent = c.notes.slice().reverse().slice(0, 10);
        for (const n of recent) {
            const date = (n.timestamp || '').slice(0, 10);
            chunk(view, `- \`${date}\` ${n.memo}\n`);
        }
        if (c.notes.length > 10) chunk(view, `- _…+${c.notes.length - 10}건_\n`);
    }
}

async function runHire(arg: string, view: any): Promise<boolean> {
    const trimmed = arg.trim();
    if (!trimmed) { _hireDashboard(view); return true; }

    const parts = trimmed.split(/\s+/);
    const sub = parts[0].toLowerCase();

    if (sub === 'help' || sub === '?') {
        chunk(view, [
            '\n👥 **/hire — 채용 파이프라인**',
            '',
            '사용법:',
            '  `/hire`                                  — 파이프라인 대시보드',
            '  `/hire add <이름> <역할>`                — 신규 후보 (inbox 단계)',
            '  `/hire stage <이름> <새 단계>`           — 단계 이동',
            '  `/hire note <이름> <텍스트>`             — 자유 메모',
            '  `/hire offer <이름> <연봉> [입사일]`     — 오퍼 발송 (단계=offer)',
            '  `/hire hire <이름> [입사일]`             — 입사 확정 (단계=hired)',
            '  `/hire reject <이름> <사유>`             — 거절 (회사 측)',
            '  `/hire decline <이름> <사유>`            — 후보 사양',
            '  `/hire show <이름>`                      — 상세 + 이력',
            '  `/hire list [active/closed/단계명/역할]` — 필터 목록',
            '  `/hire path`                             — 파일 위치',
            '',
            '단계 (기본 파이프라인): inbox → screened → interview → final → offer → accepted → hired',
            '터미널: rejected · declined',
            '',
            '연봉 단위: `4500만` / `1억` / `45000000` 모두 OK.',
            '입사일: `YYYY-MM-DD`.',
            '저장: `<workspace>/.astra/hire.jsonl` (로컬 only).\n',
        ].join('\n'));
        return true;
    }

    if (sub === 'path') {
        const p = getHireFilePath();
        if (!p) { chunk(view, '\n⚠️ 워크스페이스 폴더 없음.\n'); return true; }
        chunk(view, `\n📂 \`${p}\`\n  · 누적 이벤트 ${readHireEvents().length}건 (.jsonl).\n`);
        return true;
    }

    if (sub === 'list') { _hireList(view, parts[1]); return true; }
    if (sub === 'show') {
        const name = parts.slice(1).join(' ').trim();
        if (!name) { chunk(view, '\n❌ 사용법: `/hire show <이름>`\n'); return true; }
        _hireShow(view, name);
        return true;
    }

    if (sub === 'add') {
        const name = parts[1];
        const role = parts.slice(2).join(' ').trim();
        if (!name || !role) { chunk(view, '\n❌ 사용법: `/hire add <이름> <역할>`\n예: `/hire add 김개발 백엔드 시니어`\n'); return true; }
        const event: HireEvent = {
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            timestamp: new Date().toISOString(),
            candidateId: candidateIdFromName(name),
            candidateName: name,
            role,
            type: 'add',
            stage: 'inbox',
        };
        const res = appendHireEvent(event);
        if (!res.ok) { chunk(view, `\n❌ 저장 실패: ${res.error}\n`); return true; }
        chunk(view, `\n📥 **${name}** 등록 — ${role} (inbox)\n`);
        return true;
    }

    if (sub === 'stage') {
        const name = parts[1];
        const newStage = parts[2]?.toLowerCase();
        if (!name || !newStage) { chunk(view, '\n❌ 사용법: `/hire stage <이름> <단계>`\n'); return true; }
        const existing = computeCandidateStates().get(candidateIdFromName(name));
        if (!existing) { chunk(view, `\n❌ "${name}" 등록되지 않음. \`/hire add\` 먼저.\n`); return true; }
        const event: HireEvent = {
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            timestamp: new Date().toISOString(),
            candidateId: existing.candidateId,
            candidateName: existing.candidateName,
            role: existing.role,
            type: 'stage',
            stage: newStage,
        };
        const res = appendHireEvent(event);
        if (!res.ok) { chunk(view, `\n❌ 저장 실패: ${res.error}\n`); return true; }
        chunk(view, `\n${stageEmoji(newStage)} **${existing.candidateName}**: ${existing.stage} → ${newStage}\n`);
        return true;
    }

    if (sub === 'offer') {
        const name = parts[1];
        const salaryToken = parts[2];
        const startDate = parts[3];
        if (!name || !salaryToken) { chunk(view, '\n❌ 사용법: `/hire offer <이름> <연봉> [입사일]`\n예: `/hire offer 김개발 6000만 2026-07-01`\n'); return true; }
        const existing = computeCandidateStates().get(candidateIdFromName(name));
        if (!existing) { chunk(view, `\n❌ "${name}" 등록되지 않음.\n`); return true; }
        const salary = parseAmount(salaryToken);
        if (salary === null) { chunk(view, `\n❌ 연봉 파싱 실패: "${salaryToken}"\n`); return true; }
        const event: HireEvent = {
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            timestamp: new Date().toISOString(),
            candidateId: existing.candidateId,
            candidateName: existing.candidateName,
            role: existing.role,
            type: 'offer',
            stage: 'offer',
            salary,
            memo: startDate ? `오퍼 발송 (입사 예정: ${startDate})` : '오퍼 발송',
        };
        const res = appendHireEvent(event);
        if (!res.ok) { chunk(view, `\n❌ 저장 실패: ${res.error}\n`); return true; }
        chunk(view, `\n📨 **${existing.candidateName}** 오퍼 — ${fmtKrw(salary)}원${startDate ? ` · 입사 ${startDate}` : ''}\n`);
        return true;
    }

    if (sub === 'hire') {
        const name = parts[1];
        const startDate = parts[2];
        if (!name) { chunk(view, '\n❌ 사용법: `/hire hire <이름> [입사일]`\n'); return true; }
        const existing = computeCandidateStates().get(candidateIdFromName(name));
        if (!existing) { chunk(view, `\n❌ "${name}" 등록되지 않음.\n`); return true; }
        const event: HireEvent = {
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            timestamp: new Date().toISOString(),
            candidateId: existing.candidateId,
            candidateName: existing.candidateName,
            role: existing.role,
            type: 'hire',
            stage: 'hired',
            memo: startDate ? `입사 확정 (시작: ${startDate})` : '입사 확정',
        };
        const res = appendHireEvent(event);
        if (!res.ok) { chunk(view, `\n❌ 저장 실패: ${res.error}\n`); return true; }
        chunk(view, `\n🎉 **${existing.candidateName}** 입사 확정 — ${existing.role}${startDate ? ` (${startDate})` : ''}\n`);
        return true;
    }

    if (sub === 'reject' || sub === 'decline' || sub === 'note') {
        const name = parts[1];
        const memo = parts.slice(2).join(' ').trim();
        if (!name || !memo) { chunk(view, `\n❌ 사용법: \`/hire ${sub} <이름> <${sub === 'note' ? '텍스트' : '사유'}>\`\n`); return true; }
        const existing = computeCandidateStates().get(candidateIdFromName(name));
        if (!existing) { chunk(view, `\n❌ "${name}" 등록되지 않음.\n`); return true; }
        const event: HireEvent = {
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            timestamp: new Date().toISOString(),
            candidateId: existing.candidateId,
            candidateName: existing.candidateName,
            role: existing.role,
            type: sub,
            stage: sub === 'note' ? undefined : sub === 'reject' ? 'rejected' : 'declined',
            memo,
        };
        const res = appendHireEvent(event);
        if (!res.ok) { chunk(view, `\n❌ 저장 실패: ${res.error}\n`); return true; }
        const labels: Record<string, string> = { reject: '❌ 거절', decline: '🚪 사양', note: '📝 메모' };
        chunk(view, `\n${labels[sub]} **${existing.candidateName}**: ${memo}\n`);
        return true;
    }

    chunk(view, `\n❌ 알 수 없는 서브명령: "${sub}". \`/hire help\` 참조.\n`);
    return true;
}

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

registerSlashCommand({ name: '/runway', description: '현금 / 월 소진율 / 런웨이 — 4인 기업 CEO 의 가장 중요한 숫자 (로컬 .jsonl)', handler: runRunway });
registerSlashCommand({ name: '/customers', description: '고객사 / MRR / 갱신 / 위험 트래커 — event-sourced 로그 (로컬 .jsonl)', handler: runCustomers });
registerSlashCommand({ name: '/hire', description: '채용 파이프라인 — 후보자 단계·오퍼·합격 트래커 (로컬 .jsonl)', handler: runHire });
