/**
 * TeamOps Coordination — /task · /decisions · /onesie · /blocked · /standup.
 *
 * v2.2.199 에서 slashRouter.ts 에서 분리. 작업·결정·1:1·블로커·스탠드업 등
 * "팀 운영의 실시간 부분" 클러스터.
 *
 * 공통 헬퍼는 `./_shared.ts` 에서. 옛 slashRouter 의 local parseTaskOwner/
 * parseFlexibleDate 도 여기로 (parseFlexibleDate 는 /task 만 사용).
 */

import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { registerSlashCommand, chunk } from '../../datacollect/slashRouter';
import { parseTaskOwner } from './_shared';
import { createCalendarEvent, createTask, listTasks, _addDaysDate } from '../../calendar';
import { ChronicleProjectStore } from '../../../sidebar/managers/chronicleProjectStore';
import { callLmSynthesis } from '../../datacollect/llm';
import { buildWeeklyPrompt, WEEKLY_SYSTEM, type WeeklyTask } from '../../datacollect/prompts/weeklyPrompt';

// ─── 공통 헬퍼 — /task 전용 ──────────────────────────────────────────────

/** 유연한 한국 날짜 파서. YY/MM/DD · YYYY/MM/DD · YYYY-MM-DD 지원. */
function parseFlexibleDate(s: string): string | null {
    if (!s) return null;
    let y: number, mo: number, d: number;
    let m = s.match(/^(\d{2})\/(\d{1,2})\/(\d{1,2})$/);
    if (m) { y = 2000 + Number(m[1]); mo = Number(m[2]); d = Number(m[3]); }
    else if ((m = s.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/))) { y = Number(m[1]); mo = Number(m[2]); d = Number(m[3]); }
    else if ((m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/))) { y = Number(m[1]); mo = Number(m[2]); d = Number(m[3]); }
    else return null;
    if (mo < 1 || mo > 12 || d < 1 || d > 31) return null;
    const date = new Date(y, mo - 1, d);
    if (Number.isNaN(date.getTime()) || date.getMonth() !== mo - 1 || date.getDate() !== d) return null;
    return `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
}

// ─── /task — Google Tasks + Calendar 동시 등록 ──────────────────────────

async function runTask(arg: string, view: any, context?: vscode.ExtensionContext): Promise<boolean> {
    if (!context) { chunk(view, '\n❌ ExtensionContext 없음 — /task 실행 불가.\n'); return true; }
    if (!arg.trim()) {
        chunk(view, [
            '\n📋 **/task — Google Tasks + Calendar 동시 등록**',
            '',
            '사용법:',
            '  `/task [@담당자] <제목> <시작일> <완료일>`  — 기간 작업',
            '  `/task [@담당자] <제목> <날짜>`             — 하루짜리 작업',
            '',
            '날짜 형식: `YY/MM/DD` (예: `26/05/27`) · `YYYY-MM-DD` · `YYYY/MM/DD`',
            '담당자: `@` 접두사로 첫 토큰에 (예: `@기획자`). 생략 가능 — 있으면 제목 앞 `[담당자]` 로 prefix 됨.',
            '',
            '예시:',
            '  `/task @기획자 Apple 계정 생성 요청 26/05/27 26/06/28`',
            '  `/task @디자이너 메인 화면 시안 2026-07-01 2026-07-15`',
            '  `/task 약값 결제 26/06/01`  (담당자 없음)',
            '',
            'Tasks API + Calendar API 양쪽 등록. Tasks 는 마감일만(시작일은 노트), Calendar 는 기간 all-day 일정.',
            '',
        ].join('\n'));
        return true;
    }

    const tokens = arg.trim().split(/\s+/);
    if (tokens.length < 2) { chunk(view, '\n❌ 인자 부족 — 최소 제목 + 날짜 1개 필요.\n'); return true; }

    let owner: string | undefined;
    if (tokens[0]?.startsWith('@') && tokens[0].length > 1) {
        owner = tokens[0].slice(1);
        tokens.shift();
        if (tokens.length < 1) { chunk(view, '\n❌ 제목·날짜 누락 (담당자만 입력됨).\n'); return true; }
    }

    const lastDate = parseFlexibleDate(tokens[tokens.length - 1]);
    const secondLastDate = tokens.length >= 3 ? parseFlexibleDate(tokens[tokens.length - 2]) : null;
    let startYmd: string, endYmd: string, titleTokens: string[];
    if (lastDate && secondLastDate) {
        startYmd = secondLastDate; endYmd = lastDate; titleTokens = tokens.slice(0, -2);
    } else if (lastDate) {
        startYmd = lastDate; endYmd = lastDate; titleTokens = tokens.slice(0, -1);
    } else {
        chunk(view, `\n❌ 마지막 토큰이 날짜 형식이 아닙니다 ("${tokens[tokens.length - 1]}"). 사용 가능 형식: YY/MM/DD · YYYY-MM-DD · YYYY/MM/DD.\n사용법: \`/task\` (인자 없이) 실행해 도움말 보기.\n`);
        return true;
    }
    const baseTitle = titleTokens.join(' ').trim();
    if (!baseTitle) { chunk(view, '\n❌ 제목 누락.\n'); return true; }
    if (startYmd > endYmd) { chunk(view, `\n❌ 시작일(${startYmd})이 완료일(${endYmd})보다 늦습니다.\n`); return true; }

    const title = owner ? `[${owner}] ${baseTitle}` : baseTitle;
    const isRange = startYmd !== endYmd;
    const periodLabel = isRange ? `${startYmd} ~ ${endYmd}` : startYmd;
    chunk(view, `\n📝 **Google Tasks + Calendar 등록**: ${title}\n  · 기간: ${periodLabel}${owner ? ` · 담당: @${owner}` : ''}\n`);

    const notes = `${owner ? `담당: @${owner}\n` : ''}Astra /task 직접 등록\n기간: ${periodLabel}`;
    const successes: string[] = [];
    const failures: string[] = [];
    let calLink: string | undefined;

    const taskNotes = isRange ? `${notes}\n(Tasks 는 마감일만 사용 — 시작일은 노트 참조)` : notes;
    const taskResult = await createTask(context, { title, due: endYmd, notes: taskNotes });
    if (taskResult.ok) successes.push('Tasks');
    else failures.push(`Tasks: ${taskResult.error}`);

    const calEnd = _addDaysDate(endYmd, 1);
    const calResult = await createCalendarEvent(context, { title, start: startYmd, end: calEnd, allDay: true, description: notes });
    if (calResult.ok) { successes.push('Calendar'); calLink = calResult.event.htmlLink; }
    else failures.push(`Calendar: ${calResult.error}`);

    if (failures.length === 0) chunk(view, `✅ 등록 완료 — ${successes.join(' + ')}\n`);
    else if (successes.length > 0) {
        chunk(view, `✅ 부분 성공 — ${successes.join(' + ')}\n`);
        for (const f of failures) chunk(view, `  ⚠️ ${f}\n`);
    } else {
        chunk(view, `❌ 모두 실패\n`);
        for (const f of failures) chunk(view, `  · ${f}\n`);
    }
    if (calLink) chunk(view, `🔗 Calendar 일정 열기: ${calLink}\n`);
    return true;
}

// ─── /decisions — Chronicle ADR 검색 ─────────────────────────────────────

async function runDecisions(arg: string, view: any, context?: vscode.ExtensionContext): Promise<boolean> {
    if (!context) { chunk(view, '\n❌ ExtensionContext 없음 — /decisions 실행 불가.\n'); return true; }

    const tokens = arg.trim().split(/\s+/).filter(Boolean);
    let ownerFilter: string | undefined;
    const keywordParts: string[] = [];
    for (const t of tokens) {
        if (t.startsWith('@') && t.length > 1) ownerFilter = t.slice(1);
        else keywordParts.push(t);
    }
    const keyword = keywordParts.join(' ').toLowerCase().trim();

    if (!keyword && !ownerFilter) {
        chunk(view, [
            '\n📋 **/decisions — Chronicle 결정 기록 검색**',
            '',
            '사용법:',
            '  `/decisions <키워드>`              — 키워드로 ADR 검색',
            '  `/decisions @<담당자>`             — 담당자 언급 결정만',
            '  `/decisions <키워드> @<담당자>`    — 둘 다 만족',
            '',
            '예시:',
            '  `/decisions 환불 정책`',
            '  `/decisions @기획자`',
            '  `/decisions 결제 흐름 @개발`',
            '',
            'Chronicle ADR 파일 (`<recordRoot>/decisions/ADR-NNNN-*.md`) 을 스캔합니다. 최신순 정렬, 최대 20건.',
            '',
        ].join('\n'));
        return true;
    }

    const store = new ChronicleProjectStore(context);
    const profiles = store.getAll();
    if (profiles.length === 0) {
        chunk(view, '\n❌ Chronicle 프로젝트가 없습니다. workspace 폴더를 열고 사이드바에서 chronicle 활성화하세요.\n');
        return true;
    }

    interface Hit { project: string; file: string; filePath: string; mtime: number; title: string; snippet: string; }
    const hits: Hit[] = [];

    for (const profile of profiles) {
        const decisionsDir = path.join(profile.recordRoot, 'decisions');
        if (!fs.existsSync(decisionsDir)) continue;
        let fileNames: string[] = [];
        try { fileNames = fs.readdirSync(decisionsDir); } catch { continue; }
        for (const fileName of fileNames) {
            if (!fileName.endsWith('.md') || !fileName.startsWith('ADR-')) continue;
            const filePath = path.join(decisionsDir, fileName);
            let content = '';
            try { content = fs.readFileSync(filePath, 'utf-8'); } catch { continue; }
            const lower = content.toLowerCase();

            if (keyword && !lower.includes(keyword)) continue;
            if (ownerFilter && !content.includes(ownerFilter)) continue;

            const titleMatch = content.match(/^#\s+(.+)$/m);
            const title = titleMatch ? titleMatch[1].trim() : fileName.replace(/\.md$/, '');

            let snippet = '';
            if (keyword) {
                const idx = lower.indexOf(keyword);
                if (idx >= 0) {
                    const start = Math.max(0, idx - 60);
                    const end = Math.min(content.length, idx + 180);
                    snippet = content.slice(start, end).replace(/\s+/g, ' ').trim();
                }
            } else {
                const paragraphs = content.split(/\n\s*\n/).map(p => p.trim()).filter(p => p && !p.startsWith('#') && !p.startsWith('>'));
                snippet = (paragraphs[0] || '').slice(0, 220).replace(/\s+/g, ' ').trim();
            }

            let mtime = 0;
            try { mtime = fs.statSync(filePath).mtimeMs; } catch { /* keep 0 */ }
            hits.push({ project: profile.projectName, file: fileName, filePath, mtime, title, snippet });
        }
    }

    if (hits.length === 0) {
        const filterDesc = [keyword && `키워드 "${keyword}"`, ownerFilter && `@${ownerFilter}`].filter(Boolean).join(' + ');
        chunk(view, `\nℹ️ ${filterDesc} 에 매치되는 결정 기록 없음. (검색 대상: ${profiles.length}개 프로젝트)\n`);
        return true;
    }

    hits.sort((a, b) => b.mtime - a.mtime);
    const filterDesc = [keyword && `키워드: ${keyword}`, ownerFilter && `담당: @${ownerFilter}`].filter(Boolean).join(' · ');
    chunk(view, `\n📋 **결정 검색 결과 ${hits.length}건** (${filterDesc})\n\n`);
    const MAX_SHOW = 20;
    for (const h of hits.slice(0, MAX_SHOW)) {
        const date = h.mtime ? new Date(h.mtime).toISOString().slice(0, 10) : '날짜 미상';
        chunk(view, `### ${h.title}\n`);
        chunk(view, `- 📅 ${date} · 📁 ${h.project} · \`${h.file}\`\n`);
        if (h.snippet) chunk(view, `- 💬 …${h.snippet}…\n`);
        chunk(view, `- 🔗 \`${h.filePath}\`\n\n`);
    }
    if (hits.length > MAX_SHOW) chunk(view, `_…+${hits.length - MAX_SHOW}건 더 (필터를 좁히면 줄어듭니다)_\n`);
    return true;
}

// ─── /onesie — 멤버별 1:1 미팅 준비 카드 ─────────────────────────────────

async function runOnesie(arg: string, view: any, context?: vscode.ExtensionContext): Promise<boolean> {
    if (!context) { chunk(view, '\n❌ ExtensionContext 없음 — /onesie 실행 불가.\n'); return true; }
    const memberRaw = arg.trim().split(/\s+/)[0] || '';
    const member = memberRaw.replace(/^@/, '').trim();
    if (!member) {
        chunk(view, [
            '\n📋 **/onesie [멤버] — 1:1 미팅 준비 카드**',
            '',
            '사용법: `/onesie <담당자>` 또는 `/onesie @<담당자>`',
            '예: `/onesie 기획자` · `/onesie @디자이너` · `/onesie 개발`',
            '',
            '대상자의 Tasks 진행 상황(완료/지연/다가오는)과 최근 Chronicle 결정 기록을 모아 1:1 준비 카드 생성. 자동 대화 토픽 제안 포함.',
            '',
            '※ `/task @<멤버> ...` 로 task 를 등록해 두면 자동으로 잡힙니다. `/meet` 액션 아이템도 owner 가 있으면 잡힘.',
            '',
        ].join('\n'));
        return true;
    }

    chunk(view, `\n📋 **1:1 준비 카드 — @${member}**\n`);

    chunk(view, '\n📥 Tasks 가져오는 중...\n');
    const taskResult = await listTasks(context, { showCompleted: true, maxResults: 200 });
    const allTasks = taskResult.ok ? taskResult.tasks : [];
    if (!taskResult.ok) chunk(view, `\n⚠️ Tasks 조회 실패: ${taskResult.error}\n  (Chronicle 검색은 계속 진행)\n`);

    const memberPrefix = `[${member}]`;
    const memberTasks = allTasks.filter((t) =>
        t.title.includes(memberPrefix)
        || (t.notes || '').includes(`@${member}`)
        || (t.notes || '').includes(`담당: ${member}`),
    );

    const today = new Date().toISOString().slice(0, 10);
    const thirtyDaysAgoIso = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
    const completedRecent = memberTasks
        .filter((t) => t.status === 'completed' && (t.completed || '') >= thirtyDaysAgoIso)
        .sort((a, b) => (b.completed || '').localeCompare(a.completed || ''));
    const overdue = memberTasks
        .filter((t) => t.status === 'needsAction' && t.due && t.due < today)
        .sort((a, b) => (a.due || '').localeCompare(b.due || ''));
    const upcoming = memberTasks
        .filter((t) => t.status === 'needsAction' && t.due && t.due >= today)
        .sort((a, b) => (a.due || '').localeCompare(b.due || ''));
    const noDate = memberTasks.filter((t) => t.status === 'needsAction' && !t.due);

    const store = new ChronicleProjectStore(context);
    const profiles = store.getAll();
    interface AdrHit { date: string; mtime: number; title: string; file: string; project: string; }
    const adrHits: AdrHit[] = [];
    for (const profile of profiles) {
        const decisionsDir = path.join(profile.recordRoot, 'decisions');
        if (!fs.existsSync(decisionsDir)) continue;
        let names: string[] = [];
        try { names = fs.readdirSync(decisionsDir); } catch { continue; }
        for (const fn of names) {
            if (!fn.endsWith('.md') || !fn.startsWith('ADR-')) continue;
            const fp = path.join(decisionsDir, fn);
            let content = '';
            try { content = fs.readFileSync(fp, 'utf-8'); } catch { continue; }
            if (!content.includes(member)) continue;
            const titleMatch = content.match(/^#\s+(.+)$/m);
            const title = titleMatch ? titleMatch[1].trim() : fn.replace(/\.md$/, '');
            let mtime = 0;
            try { mtime = fs.statSync(fp).mtimeMs; } catch { /* keep 0 */ }
            const date = mtime ? new Date(mtime).toISOString().slice(0, 10) : '';
            adrHits.push({ date, mtime, title, file: fn, project: profile.projectName });
        }
    }
    adrHits.sort((a, b) => b.mtime - a.mtime);

    chunk(view, `\n## 최근 30일 완료 (${completedRecent.length}건)\n`);
    if (completedRecent.length === 0) chunk(view, '_없음_\n');
    else for (const t of completedRecent.slice(0, 10)) {
        const d = (t.completed || '').slice(0, 10);
        chunk(view, `- ✅ ${d} — ${t.title}\n`);
    }
    if (completedRecent.length > 10) chunk(view, `_…+${completedRecent.length - 10}건 더_\n`);

    chunk(view, `\n## 지연 (${overdue.length}건)\n`);
    if (overdue.length === 0) chunk(view, '_없음_\n');
    else for (const t of overdue) chunk(view, `- 🔴 ${t.due} (마감 지남) — ${t.title}\n`);

    chunk(view, `\n## 진행 중 / 다가오는 (${upcoming.length}건)\n`);
    if (upcoming.length === 0) chunk(view, '_없음_\n');
    else for (const t of upcoming.slice(0, 10)) chunk(view, `- 🟡 ${t.due} — ${t.title}\n`);
    if (upcoming.length > 10) chunk(view, `_…+${upcoming.length - 10}건 더_\n`);

    if (noDate.length > 0) {
        chunk(view, `\n## 마감일 없음 (${noDate.length}건)\n`);
        for (const t of noDate.slice(0, 5)) chunk(view, `- ⚪ ${t.title}\n`);
        if (noDate.length > 5) chunk(view, `_…+${noDate.length - 5}건 더_\n`);
    }

    chunk(view, `\n## 최근 결정 — @${member} 언급 (${adrHits.length}건)\n`);
    if (adrHits.length === 0) chunk(view, '_없음_\n');
    else for (const h of adrHits.slice(0, 5)) chunk(view, `- 📋 ${h.date} — ${h.title} (\`${h.file}\`)\n`);

    const topics: string[] = [];
    if (overdue.length > 0) topics.push(`🔴 지연 ${overdue.length}건 블로커 확인 — 무엇이 막혔나, 도와줄 일은`);
    if (upcoming.length > 5) topics.push(`🟡 다가오는 마감 ${upcoming.length}건 — 우선순위 합의·과부하 여부`);
    if (completedRecent.length > 5) topics.push(`✅ 최근 완료 ${completedRecent.length}건 많음 — 회고 / 잘 된 점 / 패턴`);
    else if (completedRecent.length === 0 && memberTasks.length > 0) topics.push('⚠️ 최근 30일 완료 0건 — 어떤 일에 시간을 쓰고 있는지 확인');
    else if (memberTasks.length === 0) topics.push('⚠️ 등록된 Task 자체가 0 — 일하는 게 안 보임. owner 태깅 시작 필요');
    if (noDate.length > 3) topics.push(`⚪ 마감일 없는 작업 ${noDate.length}건 — 우선순위 합의로 마감 부여`);
    if (adrHits.length > 0) topics.push(`📋 최근 결정 (${adrHits[0].title.slice(0, 40)}…) — 이해·실행 상황 확인`);

    chunk(view, `\n## 💬 1:1 대화 토픽 제안\n`);
    if (topics.length === 0) chunk(view, '_특이사항 없음 — 일반 안부 + 다음 주 우선순위 정도_\n');
    else for (const t of topics) chunk(view, `- ${t}\n`);
    return true;
}

// ─── /blocked — 전사 across 지연·블로커 한 화면 ──────────────────────────

async function runBlocked(arg: string, view: any, context?: vscode.ExtensionContext): Promise<boolean> {
    if (!context) { chunk(view, '\n❌ ExtensionContext 없음 — /blocked 실행 불가.\n'); return true; }

    const memberFilter = (arg.trim().split(/\s+/)[0] || '').replace(/^@/, '').trim() || undefined;

    chunk(view, `\n🚨 **전사 블로커·지연 뷰**${memberFilter ? ` — @${memberFilter}` : ''}\n`);
    chunk(view, '\n📥 Tasks 가져오는 중...\n');

    const result = await listTasks(context, { showCompleted: false, maxResults: 300 });
    if (!result.ok) { chunk(view, `\n❌ Tasks 조회 실패: ${result.error}\n`); return true; }
    const tasks = result.tasks;

    interface Row { due?: string; owner?: string; title: string; }
    const today = new Date().toISOString().slice(0, 10);
    const weekLater = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);

    const overdue: Row[] = [];
    const thisWeek: Row[] = [];
    const noDate: Row[] = [];

    for (const t of tasks) {
        const { owner, displayTitle } = parseTaskOwner(t.title, t.notes);
        if (memberFilter && (owner || '').toLowerCase() !== memberFilter.toLowerCase()) continue;
        const row: Row = { due: t.due, owner, title: displayTitle };
        if (!t.due) noDate.push(row);
        else if (t.due < today) overdue.push(row);
        else if (t.due <= weekLater) thisWeek.push(row);
    }
    overdue.sort((a, b) => (a.due || '').localeCompare(b.due || ''));
    thisWeek.sort((a, b) => (a.due || '').localeCompare(b.due || ''));
    noDate.sort((a, b) => (a.owner || 'zzz').localeCompare(b.owner || 'zzz'));

    const fmtRow = (r: Row): string => {
        const o = r.owner ? `@${r.owner}` : '(owner 없음)';
        return `- 📅 \`${r.due || '----------'}\` · **${o}** — ${r.title}`;
    };

    const totalShown = overdue.length + thisWeek.length + noDate.length;
    if (totalShown === 0) {
        chunk(view, `\n✅ 지연·임박 항목 없음${memberFilter ? ` (@${memberFilter})` : ''}. 진행 상황 양호.\n`);
        return true;
    }

    if (overdue.length > 0) {
        chunk(view, `\n## 🔴 지연 ${overdue.length}건 — 즉시 확인 필요\n`);
        const MAX_OVERDUE = 20;
        for (const r of overdue.slice(0, MAX_OVERDUE)) chunk(view, fmtRow(r) + '\n');
        if (overdue.length > MAX_OVERDUE) chunk(view, `_…+${overdue.length - MAX_OVERDUE}건 더_\n`);
    }
    if (thisWeek.length > 0) {
        chunk(view, `\n## 🟡 이번 주 마감 ${thisWeek.length}건 (~${weekLater})\n`);
        const MAX_WEEK = 15;
        for (const r of thisWeek.slice(0, MAX_WEEK)) chunk(view, fmtRow(r) + '\n');
        if (thisWeek.length > MAX_WEEK) chunk(view, `_…+${thisWeek.length - MAX_WEEK}건 더_\n`);
    }
    if (noDate.length > 0) {
        chunk(view, `\n## ⚪ 마감일 없음 ${noDate.length}건 — 우선순위 합의 필요\n`);
        const MAX_NODATE = 10;
        for (const r of noDate.slice(0, MAX_NODATE)) chunk(view, fmtRow(r) + '\n');
        if (noDate.length > MAX_NODATE) chunk(view, `_…+${noDate.length - MAX_NODATE}건 더_\n`);
    }

    if (!memberFilter && (overdue.length + thisWeek.length) > 0) {
        const counts = new Map<string, { overdue: number; week: number }>();
        for (const r of overdue) {
            const k = r.owner || '(없음)';
            const c = counts.get(k) || { overdue: 0, week: 0 };
            c.overdue++;
            counts.set(k, c);
        }
        for (const r of thisWeek) {
            const k = r.owner || '(없음)';
            const c = counts.get(k) || { overdue: 0, week: 0 };
            c.week++;
            counts.set(k, c);
        }
        const ranked = [...counts.entries()]
            .sort((a, b) => (b[1].overdue * 2 + b[1].week) - (a[1].overdue * 2 + a[1].week));
        chunk(view, `\n## 📊 멤버별 압박 ${ranked.length}명\n`);
        for (const [member, c] of ranked) {
            chunk(view, `- **@${member}** — 지연 ${c.overdue}건${c.week ? ` · 이번 주 ${c.week}건` : ''}\n`);
        }
        chunk(view, '\n💡 압박 큰 멤버부터 `/onesie @<멤버>` 로 1:1 카드 확인 권장.\n');
    }
    return true;
}

// ─── /standup — 팀 스탠드업 카드 (슬랙 복붙 친화) ────────────────────────

async function runStandup(arg: string, view: any, context?: vscode.ExtensionContext): Promise<boolean> {
    if (!context) { chunk(view, '\n❌ ExtensionContext 없음 — /standup 실행 불가.\n'); return true; }

    const mode = (arg.trim().split(/\s+/)[0] || 'weekly').toLowerCase();
    const windowDays = mode === 'daily' ? 1 : mode === 'monthly' ? 30 : 7;
    const modeLabel = mode === 'daily' ? '일일' : mode === 'monthly' ? '월간' : '주간';

    if (arg.trim() && !['daily', 'weekly', 'monthly', ''].includes(mode)) {
        chunk(view, [
            '\n📋 **/standup [daily/weekly/monthly] — 팀 스탠드업 카드**',
            '',
            '사용법:',
            '  `/standup`           — 주간 (기본, 7일 윈도우)',
            '  `/standup daily`     — 일일 (1일 윈도우)',
            '  `/standup weekly`    — 주간 (7일)',
            '  `/standup monthly`   — 월간 (30일)',
            '',
            '멤버별로 완료 / 진행·예정 / 블로커 3-row + 이번 기간 결정 목록을 슬랙·노션에 복붙 가능한 마크다운으로 출력.',
            '',
        ].join('\n'));
        return true;
    }

    chunk(view, `\n📊 **팀 스탠드업 — ${modeLabel} (${windowDays}일 윈도우)**\n  · ${new Date().toISOString().slice(0, 10)} 기준\n`);
    chunk(view, '\n📥 Tasks 가져오는 중...\n');

    const result = await listTasks(context, { showCompleted: true, maxResults: 300 });
    if (!result.ok) { chunk(view, `\n❌ Tasks 조회 실패: ${result.error}\n`); return true; }

    const today = new Date().toISOString().slice(0, 10);
    const windowAgoIso = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000).toISOString();
    const upcomingEnd = new Date(Date.now() + windowDays * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);

    interface MemberSlot { completed: { date: string; title: string }[]; upcoming: { due: string; title: string }[]; overdue: { due: string; title: string }[]; noDate: { title: string }[]; }
    const byMember = new Map<string, MemberSlot>();
    const ensure = (k: string): MemberSlot => {
        let s = byMember.get(k);
        if (!s) { s = { completed: [], upcoming: [], overdue: [], noDate: [] }; byMember.set(k, s); }
        return s;
    };

    for (const t of result.tasks) {
        const { owner, displayTitle } = parseTaskOwner(t.title, t.notes);
        const member = owner || '(owner 없음)';
        const slot = ensure(member);
        if (t.status === 'completed') {
            if ((t.completed || '') >= windowAgoIso) {
                slot.completed.push({ date: (t.completed || '').slice(0, 10), title: displayTitle });
            }
        } else {
            if (!t.due) slot.noDate.push({ title: displayTitle });
            else if (t.due < today) slot.overdue.push({ due: t.due, title: displayTitle });
            else if (t.due <= upcomingEnd) slot.upcoming.push({ due: t.due, title: displayTitle });
        }
    }
    for (const s of byMember.values()) {
        s.completed.sort((a, b) => b.date.localeCompare(a.date));
        s.upcoming.sort((a, b) => a.due.localeCompare(b.due));
        s.overdue.sort((a, b) => a.due.localeCompare(b.due));
    }

    if (byMember.size === 0) {
        chunk(view, '\nℹ️ 이 기간에 활동이 있는 task 가 없습니다. `/task @<멤버> ...` 로 등록 시작.\n');
        return true;
    }

    const store = new ChronicleProjectStore(context);
    const profiles = store.getAll();
    interface RecentAdr { date: string; title: string; file: string; mtime: number; }
    const recentAdrs: RecentAdr[] = [];
    const windowAgoMs = Date.now() - windowDays * 24 * 60 * 60 * 1000;
    for (const profile of profiles) {
        const decisionsDir = path.join(profile.recordRoot, 'decisions');
        if (!fs.existsSync(decisionsDir)) continue;
        let names: string[] = [];
        try { names = fs.readdirSync(decisionsDir); } catch { continue; }
        for (const fn of names) {
            if (!fn.endsWith('.md') || !fn.startsWith('ADR-')) continue;
            const fp = path.join(decisionsDir, fn);
            let mtime = 0;
            try { mtime = fs.statSync(fp).mtimeMs; } catch { continue; }
            if (mtime < windowAgoMs) continue;
            let content = '';
            try { content = fs.readFileSync(fp, 'utf-8'); } catch { continue; }
            const titleMatch = content.match(/^#\s+(.+)$/m);
            const title = titleMatch ? titleMatch[1].trim() : fn.replace(/\.md$/, '');
            recentAdrs.push({ date: new Date(mtime).toISOString().slice(0, 10), title, file: fn, mtime });
        }
    }
    recentAdrs.sort((a, b) => b.mtime - a.mtime);

    const memberRanked = [...byMember.entries()].sort((a, b) => {
        const score = (s: MemberSlot) => s.completed.length + s.upcoming.length + s.overdue.length * 2;
        return score(b[1]) - score(a[1]);
    });

    chunk(view, '\n---\n');
    chunk(view, `\n## 📊 팀 스탠드업 — ${modeLabel}\n`);
    chunk(view, `*${windowDays}일 윈도우 · 기준일 ${today}*\n`);

    for (const [member, s] of memberRanked) {
        chunk(view, `\n### @${member}\n`);

        if (s.completed.length === 0) chunk(view, `**✅ 완료**: _없음_\n`);
        else {
            chunk(view, `**✅ 완료 (${s.completed.length})**:\n`);
            const MAX = 8;
            for (const c of s.completed.slice(0, MAX)) chunk(view, `- ${c.date} — ${c.title}\n`);
            if (s.completed.length > MAX) chunk(view, `- _…+${s.completed.length - MAX}건_\n`);
        }

        if (s.upcoming.length === 0 && s.noDate.length === 0) chunk(view, `**🎯 진행/예정**: _없음_\n`);
        else {
            chunk(view, `**🎯 진행/예정 (${s.upcoming.length + s.noDate.length})**:\n`);
            const MAX = 6;
            for (const u of s.upcoming.slice(0, MAX)) chunk(view, `- ${u.due} — ${u.title}\n`);
            for (const n of s.noDate.slice(0, Math.max(0, MAX - s.upcoming.length))) chunk(view, `- (마감 미정) — ${n.title}\n`);
            const total = s.upcoming.length + s.noDate.length;
            if (total > MAX) chunk(view, `- _…+${total - MAX}건_\n`);
        }

        if (s.overdue.length === 0) chunk(view, `**🚧 블로커**: _없음_\n`);
        else {
            chunk(view, `**🚧 블로커 (${s.overdue.length}건 지연)**:\n`);
            for (const o of s.overdue.slice(0, 5)) chunk(view, `- 🔴 ${o.due} (지남) — ${o.title}\n`);
            if (s.overdue.length > 5) chunk(view, `- _…+${s.overdue.length - 5}건_\n`);
        }
    }

    if (recentAdrs.length > 0) {
        chunk(view, `\n---\n\n## 📋 이번 ${modeLabel} 결정 (${recentAdrs.length}건)\n`);
        for (const a of recentAdrs.slice(0, 10)) chunk(view, `- ${a.date} — ${a.title}\n`);
        if (recentAdrs.length > 10) chunk(view, `- _…+${recentAdrs.length - 10}건_\n`);
    }

    chunk(view, '\n---\n\n💡 위 마크다운을 그대로 슬랙·노션에 복붙하세요. 멤버 활동 순 정렬.\n');
    return true;
}

// ─── /weekly — 캘린더 task 기반 주간 보고서 (금주/차주) ──────────────────
// `/weekly <차주시작일> <차주종료일>` — 입력한 날짜는 **차주** 기준.
// 금주(차주 시작 직전 7일)는 자동 역산해 함께 검색한다. 버킷팅은 due/completed
// 날짜로 코드가 처리(예측 가능), 서술 포맷팅만 LLM(meet 메모 → narrative bullet).

async function runWeekly(arg: string, view: any, context?: vscode.ExtensionContext): Promise<boolean> {
    if (!context) { chunk(view, '\n❌ ExtensionContext 없음 — /weekly 실행 불가.\n'); return true; }

    const tokens = arg.trim().split(/\s+/).filter(Boolean);
    if (tokens.length < 2) {
        chunk(view, [
            '\n📋 **/weekly — 주간 업무 보고서 (금주/차주)**',
            '',
            '사용법: `/weekly <차주 시작일> <차주 종료일>`',
            '  · 입력한 두 날짜는 **차주** 기준입니다.',
            '  · 금주(차주 시작 직전 7일)는 자동으로 역산해 함께 검색합니다.',
            '',
            '날짜 형식: `YYYY-MM-DD` · `YYYY/MM/DD` · `YY/MM/DD`',
            '',
            '예시: `/weekly 2026-06-08 2026-06-12`  → 차주 6/8~6/12, 금주 6/1~6/7',
            '',
            '캘린더(Google Tasks)에 등록된 작업의 마감·완료일로 금주/차주를 나누고,',
            '각 작업의 메모(작업 상세·맥락)를 근거로 하위 세부 항목을 채웁니다.',
            '※ `/meet`·`/task` 로 등록한 작업이 소스입니다 — 메모가 충실할수록 보고서가 정확합니다.',
            '',
        ].join('\n'));
        return true;
    }

    let nextStart = parseFlexibleDate(tokens[0]);
    let nextEnd = parseFlexibleDate(tokens[1]);
    if (!nextStart || !nextEnd) {
        chunk(view, `\n❌ 날짜 형식 오류 — "${tokens[0]}" / "${tokens[1]}". 사용 가능: YYYY-MM-DD · YYYY/MM/DD · YY/MM/DD.\n`);
        return true;
    }
    if (nextStart > nextEnd) { [nextStart, nextEnd] = [nextEnd, nextStart]; }

    // 금주 = 차주 시작 직전 7일 (차주 시작 -7 ~ 차주 시작 -1).
    const thisStart = _addDaysDate(nextStart, -7);
    const thisEnd = _addDaysDate(nextStart, -1);

    chunk(view, `\n📊 **주간 보고서**\n  · 금주: ${thisStart} ~ ${thisEnd}\n  · 차주: ${nextStart} ~ ${nextEnd}\n`);
    chunk(view, '\n📥 Tasks 가져오는 중...\n');

    const result = await listTasks(context, { showCompleted: true, maxResults: 300 });
    if (!result.ok) { chunk(view, `\n❌ Tasks 조회 실패: ${result.error}\n`); return true; }

    const inRange = (d: string, a: string, b: string) => !!d && d >= a && d <= b;
    const toWeekly = (t: typeof result.tasks[number]): WeeklyTask => ({
        title: t.title,
        due: t.due || '',
        status: t.status,
        completedYmd: t.completed ? t.completed.slice(0, 10) : undefined,
        notes: t.notes,
    });

    const thisWeek: WeeklyTask[] = [];
    const nextWeek: WeeklyTask[] = [];
    for (const t of result.tasks) {
        const completedYmd = (t.completed || '').slice(0, 10);
        if (inRange(t.due || '', nextStart, nextEnd)) {
            nextWeek.push(toWeekly(t));
        } else if (inRange(t.due || '', thisStart, thisEnd) || inRange(completedYmd, thisStart, thisEnd)) {
            thisWeek.push(toWeekly(t));
        }
    }

    if (thisWeek.length === 0 && nextWeek.length === 0) {
        chunk(view, `\nℹ️ 금주(${thisStart}~${thisEnd})·차주(${nextStart}~${nextEnd}) 범위에 등록된 task 가 없습니다.\n  (\`/meet\` 또는 \`/task\` 로 등록하면 잡힙니다.)\n`);
        return true;
    }

    chunk(view, `\n· 금주 ${thisWeek.length}건 · 차주 ${nextWeek.length}건 → 보고서 합성 중…\n`);

    let report = '';
    try {
        report = await callLmSynthesis(
            buildWeeklyPrompt({ thisWeek, nextWeek, thisRange: [thisStart, thisEnd], nextRange: [nextStart, nextEnd] }),
            WEEKLY_SYSTEM,
        );
        if (!report) throw new Error('LLM 응답이 비어 있습니다.');
    } catch (e: any) {
        chunk(view, `\n⚠️ 보고서 합성 실패: ${e?.message || String(e)}\n(LM 서버가 떠 있는지, \`g1nation.ollamaUrl\` / \`defaultModel\` 설정을 확인하세요.)\n`);
        return true;
    }

    chunk(view, '\n' + report.trim() + '\n');
    return true;
}

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

registerSlashCommand({ name: '/task', description: '단일 작업을 Google Tasks + Calendar 양쪽에 등록 (제목 + 시작일 + 완료일)', handler: runTask });
registerSlashCommand({ name: '/weekly', description: '캘린더 task 기반 주간 보고서 — 차주 날짜 입력, 금주 자동 역산 (금주/차주 포맷)', handler: runWeekly });
registerSlashCommand({ name: '/decisions', description: 'Chronicle 결정 기록(ADR) 검색 — 키워드/담당자로 필터', handler: runDecisions });
registerSlashCommand({ name: '/onesie', description: '멤버별 1:1 미팅 준비 카드 — Tasks 진행 + 최근 결정 + 대화 토픽 제안', handler: runOnesie });
registerSlashCommand({ name: '/blocked', description: '전사 across 지연·블로커 한 화면 — 일일 아침 ritual', handler: runBlocked });
registerSlashCommand({ name: '/standup', description: '팀 스탠드업 카드 (멤버별 완료/진행/블로커, 슬랙 복붙 친화)', handler: runStandup });
