/**
 * @module        cardDashboard
 * @description   卡片看板仪表盘，按状态列展示所有任务卡及 Worker 分配情况
 *
 * @author        eddy
 * @organization  wykj
 * @ownership     wykj/eddy
 *
 * @created       2026-03-27
 * @updated       2026-04-03
 *
 * @role          command
 * @layer         command
 * @boundedContext taskManagement
 *
 * @trigger       sps card dashboard <project>
 * @inputs        项目名、flags
 * @outputs       看板视图（终端表格或 JSON）
 * @workflow      1. 加载项目配置 → 2. 读取卡片和状态 → 3. 渲染多列看板
 */
import { existsSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
import { ProjectPipelineAdapter } from '../core/projectPipelineAdapter.js';
import { loadRuntimeSnapshot } from '../core/runtimeSnapshot.js';
import { isPersistedSessionAlive } from '../core/sessionLiveness.js';
import { summarizeWorkerRuntime } from '../core/workerRuntimeSummary.js';
import { createTaskBackend } from '../providers/registry.js';
const HOME = process.env.HOME || '/home/coral';
/** Default states (used when no adapter available, e.g. error path) */
const DEFAULT_STATES = ['Planning', 'Backlog', 'Todo', 'Inprogress', 'QA', 'Done'];
/** Build ordered state list from adapter */
function buildStateList(adapter) {
    if (!adapter)
        return DEFAULT_STATES;
    const states = adapter.states;
    // Build ordered list: planning → backlog → ready → [stage active states] → done
    const ordered = [states.planning, states.backlog, states.ready];
    for (const stage of adapter.stages) {
        if (!ordered.includes(stage.activeState)) {
            ordered.push(stage.activeState);
        }
    }
    if (!ordered.includes(states.done)) {
        ordered.push(states.done);
    }
    return ordered;
}
/** Build display labels from state names */
function _buildStateLabels(states) {
    const labels = {};
    for (const s of states)
        labels[s] = s;
    return labels;
}
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const DIM = '\x1b[2m';
const FG = {
    red: '\x1b[31m',
    green: '\x1b[32m',
    yellow: '\x1b[33m',
    blue: '\x1b[34m',
    magenta: '\x1b[35m',
    cyan: '\x1b[36m',
    white: '\x1b[37m',
    gray: '\x1b[90m',
};
const STRIP_ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\x1b\[[?]?[0-9;]*[hlm]/g;
function stripAnsi(text) {
    return text.replace(STRIP_ANSI_RE, '');
}
/** Calculate visible terminal width, accounting for CJK full-width characters (2 columns each) */
function visibleLength(text) {
    const stripped = stripAnsi(text);
    let width = 0;
    for (const ch of stripped) {
        const code = ch.codePointAt(0) || 0;
        // CJK Unified Ideographs, CJK Compatibility, Fullwidth Forms, Hangul, Kana, etc.
        if ((code >= 0x1100 && code <= 0x115F) || // Hangul Jamo
            (code >= 0x2E80 && code <= 0x303E) || // CJK Radicals + Symbols
            (code >= 0x3040 && code <= 0x33BF) || // Hiragana, Katakana, CJK Compatibility
            (code >= 0x3400 && code <= 0x4DBF) || // CJK Unified Extension A
            (code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified Ideographs
            (code >= 0xA000 && code <= 0xA4CF) || // Yi
            (code >= 0xAC00 && code <= 0xD7AF) || // Hangul Syllables
            (code >= 0xF900 && code <= 0xFAFF) || // CJK Compatibility Ideographs
            (code >= 0xFE30 && code <= 0xFE6F) || // CJK Compatibility Forms
            (code >= 0xFF01 && code <= 0xFF60) || // Fullwidth Forms
            (code >= 0xFFE0 && code <= 0xFFE6) || // Fullwidth Signs
            (code >= 0x20000 && code <= 0x2FA1F) // CJK Extension B-F + Compatibility Supplement
        ) {
            width += 2;
        }
        else {
            width += 1;
        }
    }
    return width;
}
function charWidth(ch) {
    const code = ch.codePointAt(0) || 0;
    if ((code >= 0x1100 && code <= 0x115F) ||
        (code >= 0x2E80 && code <= 0x303E) ||
        (code >= 0x3040 && code <= 0x33BF) ||
        (code >= 0x3400 && code <= 0x4DBF) ||
        (code >= 0x4E00 && code <= 0x9FFF) ||
        (code >= 0xA000 && code <= 0xA4CF) ||
        (code >= 0xAC00 && code <= 0xD7AF) ||
        (code >= 0xF900 && code <= 0xFAFF) ||
        (code >= 0xFE30 && code <= 0xFE6F) ||
        (code >= 0xFF01 && code <= 0xFF60) ||
        (code >= 0xFFE0 && code <= 0xFFE6) ||
        (code >= 0x20000 && code <= 0x2FA1F))
        return 2;
    return 1;
}
function padOrTruncate(text, width) {
    const len = visibleLength(text);
    if (len > width) {
        let visible = 0;
        let result = '';
        let inEscape = false;
        for (const ch of text) {
            if (ch === '\x1b') {
                inEscape = true;
                result += ch;
                continue;
            }
            if (inEscape) {
                result += ch;
                if (/[a-zA-Z]/.test(ch))
                    inEscape = false;
                continue;
            }
            const cw = charWidth(ch);
            if (visible + cw > width - 1) {
                result += '…';
                // Pad if we stopped before a double-width char left a gap
                const finalLen = visibleLength(result);
                if (finalLen < width)
                    result += ' '.repeat(width - finalLen);
                break;
            }
            result += ch;
            visible += cw;
        }
        return result;
    }
    return text + ' '.repeat(width - len);
}
function centerText(text, width) {
    const len = visibleLength(text);
    if (len >= width)
        return padOrTruncate(text, width);
    const left = Math.floor((width - len) / 2);
    return ' '.repeat(left) + text + ' '.repeat(width - len - left);
}
/** State colors by position: [planning, backlog, ready, active, review, done] */
const STATE_COLORS = [FG.gray, FG.blue, FG.cyan, FG.green, FG.yellow, FG.white];
function stateColor(state, states = DEFAULT_STATES) {
    const idx = states.indexOf(state);
    return idx >= 0 && idx < STATE_COLORS.length ? STATE_COLORS[idx] : FG.white;
}
function discoverProjects() {
    const projectsDir = resolve(HOME, '.coral', 'projects');
    if (!existsSync(projectsDir))
        return [];
    try {
        return readdirSync(projectsDir, { withFileTypes: true })
            .filter(entry => entry.isDirectory() && existsSync(resolve(projectsDir, entry.name, 'conf')))
            .map(entry => entry.name)
            .sort();
    }
    catch {
        return [];
    }
}
function getTerminalSize() {
    return {
        cols: process.stdout.columns || 120,
        rows: process.stdout.rows || 40,
    };
}
function wrapText(text, width, maxLines) {
    const clean = stripAnsi(text).replace(/\s+/g, ' ').trim();
    if (!clean)
        return Array(maxLines).fill('');
    const words = clean.split(' ');
    const lines = [];
    let current = '';
    for (const word of words) {
        const candidate = current ? `${current} ${word}` : word;
        if (candidate.length <= width) {
            current = candidate;
            continue;
        }
        if (current)
            lines.push(current);
        current = word;
        if (lines.length >= maxLines)
            break;
    }
    if (current && lines.length < maxLines)
        lines.push(current);
    if (lines.length > maxLines)
        lines.length = maxLines;
    if (lines.length === maxLines && words.join(' ').length > lines.join(' ').length) {
        const last = lines[maxLines - 1];
        lines[maxLines - 1] = last.length >= width ? `${last.slice(0, Math.max(0, width - 1))}…` : `${last.slice(0, Math.max(0, width - 1))}…`;
    }
    while (lines.length < maxLines)
        lines.push('');
    return lines.map(line => padOrTruncate(line, width));
}
function labelSummary(labels) {
    const filtered = labels.filter(label => !['AI-PIPELINE', 'CLAIMED'].includes(label));
    if (filtered.length === 0)
        return 'AI-PIPELINE';
    return filtered.slice(0, 2).join(', ');
}
function deriveBlockedReason(labels, runtimeStatus, lease, evidence) {
    if (labels.includes('CONFLICT'))
        return 'CONFLICT';
    if (labels.includes('WAITING-CONFIRMATION') || runtimeStatus === 'waiting_input' || runtimeStatus === 'needs_confirmation')
        return 'WAITING';
    if (lease?.phase === 'suspended' && (evidence?.worktreeExists || evidence?.branchExists))
        return 'RESUMABLE';
    if (runtimeStatus === 'stale')
        return 'STALE';
    if (labels.includes('NEEDS-FIX'))
        return 'NEEDS-FIX';
    if (labels.includes('STALE-RUNTIME'))
        return 'STALE';
    return null;
}
async function buildProjectBoard(projectName) {
    try {
        const snapshot = await loadRuntimeSnapshot(projectName);
        const { ctx, state } = snapshot;
        const adapter = new ProjectPipelineAdapter(ctx.config, ctx.paths.repoDir);
        const allStateNames = [...new Set([
                adapter.states.planning, adapter.states.backlog,
                adapter.states.ready, adapter.states.done,
                ...adapter.stages.flatMap(s => [s.triggerState, s.activeState, s.onCompleteState]),
            ].filter(Boolean))];
        const taskBackend = createTaskBackend(ctx.config, allStateNames);
        const cards = await taskBackend.listAll();
        const snapshots = cards.map(card => {
            const active = state.activeCards[card.seq];
            const lease = state.leases[card.seq] || null;
            const evidence = state.worktreeEvidence[card.seq] || null;
            const workerSlot = active?.worker ?? null;
            const worker = workerSlot ? state.workers[workerSlot] : null;
            const session = workerSlot ? state.sessions[workerSlot] : null;
            const runtimeOwned = !!worker && worker.status !== 'idle';
            const effectiveState = runtimeOwned ? (active?.state || card.state) : card.state;
            const sessionAlive = worker ? isPersistedSessionAlive(worker, session) : false;
            const runtimeStatus = !runtimeOwned
                ? null
                : worker?.status === 'active' && worker && !sessionAlive
                    ? 'stale'
                    : session?.pendingInput
                        ? (session.pendingInput.type === 'input' ? 'waiting_input' : 'needs_confirmation')
                        : session?.currentRun?.status || worker?.remoteStatus || (worker?.status === 'active' ? 'running' : null);
            const blockedReason = deriveBlockedReason(card.labels, runtimeStatus, lease, evidence);
            return {
                seq: card.seq,
                title: card.title,
                state: effectiveState,
                labels: card.labels,
                workerSlot: runtimeOwned ? workerSlot : null,
                runtimeStatus,
                branch: runtimeOwned ? (worker?.branch || null) : null,
                blockedReason,
                updatedAt: runtimeOwned ? (session?.updatedAt || worker?.lastHeartbeat || active?.startedAt || null) : null,
            };
        }).sort((a, b) => parseInt(a.seq, 10) - parseInt(b.seq, 10));
        const projectStates = buildStateList(adapter);
        const counts = Object.fromEntries(projectStates.map(s => [s, 0]));
        for (const card of snapshots) {
            if (counts[card.state] !== undefined)
                counts[card.state] += 1;
            else
                counts[card.state] = 1; // custom state not in standard list
        }
        const workerSummary = summarizeWorkerRuntime(state);
        const waitingCards = snapshots.filter(card => ['waiting_input', 'needs_confirmation'].includes(card.runtimeStatus || '') || card.blockedReason === 'WAITING').length;
        const conflictCards = snapshots.filter(card => card.blockedReason === 'CONFLICT').length;
        return {
            project: projectName,
            displayName: ctx.config.PROJECT_NAME || projectName,
            cards: snapshots,
            counts,
            states: projectStates,
            activeWorkers: workerSummary.active,
            mergingWorkers: workerSummary.merging,
            staleWorkers: workerSummary.stale,
            workingWorkers: workerSummary.working,
            waitingCards,
            conflictCards,
        };
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        const counts = Object.fromEntries(DEFAULT_STATES.map(s => [s, 0]));
        return {
            project: projectName,
            displayName: projectName,
            cards: [],
            counts,
            states: DEFAULT_STATES,
            activeWorkers: 0,
            mergingWorkers: 0,
            staleWorkers: 0,
            workingWorkers: 0,
            waitingCards: 0,
            conflictCards: 0,
            error: msg,
        };
    }
}
async function buildSnapshot(projects) {
    return Promise.all(projects.map(project => buildProjectBoard(project)));
}
function renderHeader(termWidth) {
    const title = ' SPS Card Dashboard ';
    const now = new Date().toLocaleTimeString();
    const right = `${DIM}${now}  q=quit  r=refresh${RESET}`;
    return [
        `${BOLD}${FG.cyan}${'─'.repeat(termWidth)}${RESET}`,
        `${centerText(`${BOLD}${FG.cyan}${title}${RESET}`, termWidth - visibleLength(right))}${right}`,
        `${BOLD}${FG.cyan}${'─'.repeat(termWidth)}${RESET}`,
    ];
}
function renderProjectSummary(board) {
    const parts = [
        `${FG.green}${board.activeWorkers} active${RESET}`,
        `${FG.yellow}${board.mergingWorkers} merging${RESET}`,
        `${FG.red}${board.staleWorkers} stale${RESET}`,
        `${FG.cyan}${board.cards.length} cards${RESET}`,
    ];
    if (board.waitingCards > 0)
        parts.push(`${FG.yellow}${board.waitingCards} waiting${RESET}`);
    if (board.conflictCards > 0)
        parts.push(`${FG.red}${board.conflictCards} conflict${RESET}`);
    return `  ${BOLD}${board.displayName}${RESET}: ${parts.join(` ${DIM}│${RESET} `)}`;
}
function renderCardTile(card, width) {
    const innerWidth = width - 2;
    const titleLines = wrapText(`#${card.seq} ${card.title}`, innerWidth, 2);
    const labelLine = padOrTruncate(`${DIM}${labelSummary(card.labels)}${RESET}`, innerWidth);
    let statusLine = '';
    if (card.blockedReason) {
        statusLine = `${FG.red}${card.blockedReason}${RESET}`;
    }
    else if (card.workerSlot && card.runtimeStatus) {
        statusLine = `${FG.green}${card.workerSlot}${RESET} ${DIM}${card.runtimeStatus}${RESET}`;
    }
    else if (card.runtimeStatus) {
        statusLine = `${DIM}${card.runtimeStatus}${RESET}`;
    }
    else {
        statusLine = `${DIM}${card.state}${RESET}`;
    }
    return [
        `${FG.gray}╭${'─'.repeat(width - 2)}╮${RESET}`,
        `${FG.gray}│${RESET}${titleLines[0]}${FG.gray}│${RESET}`,
        `${FG.gray}│${RESET}${titleLines[1]}${FG.gray}│${RESET}`,
        `${FG.gray}│${RESET}${labelLine}${FG.gray}│${RESET}`,
        `${FG.gray}│${RESET}${padOrTruncate(statusLine, innerWidth)}${FG.gray}│${RESET}`,
        `${FG.gray}╰${'─'.repeat(width - 2)}╯${RESET}`,
    ];
}
function renderColumn(board, state, width, height) {
    const title = `${stateColor(state, board.states)}${state} (${board.counts[state] ?? 0})${RESET}`;
    const lines = [
        `${FG.gray}┌${'─'.repeat(width - 2)}┐${RESET}`,
        `${FG.gray}│${RESET}${padOrTruncate(title, width - 2)}${FG.gray}│${RESET}`,
        `${FG.gray}├${'─'.repeat(width - 2)}┤${RESET}`,
    ];
    const cards = board.cards.filter(card => card.state === state);
    const available = height - lines.length - 1;
    const tileHeight = 7;
    const maxTiles = Math.max(0, Math.floor(available / tileHeight));
    const visibleCards = cards.slice(0, maxTiles);
    const tileWidth = Math.max(14, width - 4);
    for (const card of visibleCards) {
        const tileLines = renderCardTile(card, tileWidth);
        for (const tileLine of tileLines) {
            lines.push(`${FG.gray}│${RESET} ${padOrTruncate(tileLine, width - 4)} ${FG.gray}│${RESET}`);
        }
    }
    const remaining = cards.length - visibleCards.length;
    const fillerTarget = height - 1;
    if (remaining > 0 && lines.length < fillerTarget) {
        lines.push(`${FG.gray}│${RESET}${padOrTruncate(`${DIM}+${remaining} more${RESET}`, width - 2)}${FG.gray}│${RESET}`);
    }
    while (lines.length < fillerTarget) {
        lines.push(`${FG.gray}│${RESET}${' '.repeat(width - 2)}${FG.gray}│${RESET}`);
    }
    lines.push(`${FG.gray}└${'─'.repeat(width - 2)}┘${RESET}`);
    return lines;
}
function renderSingleProject(board, termWidth, termHeight) {
    const output = [];
    output.push(...renderHeader(termWidth));
    output.push('');
    output.push(centerText(`${BOLD}${board.displayName}${RESET}`, termWidth));
    output.push('');
    output.push(renderProjectSummary(board));
    output.push('');
    const gridCols = termWidth >= 150 ? 6 : 3;
    const boardStates = board.states;
    const rows = Math.ceil(boardStates.length / gridCols);
    const colWidth = Math.max(22, Math.floor((termWidth - (gridCols - 1)) / gridCols));
    const availableRows = Math.max(12, termHeight - output.length - 2);
    const panelHeight = Math.max(10, Math.floor(availableRows / rows));
    for (let row = 0; row < rows; row++) {
        const rowColumns = [];
        for (let col = 0; col < gridCols; col++) {
            const idx = row * gridCols + col;
            if (idx < boardStates.length) {
                rowColumns.push(renderColumn(board, boardStates[idx], colWidth, panelHeight));
            }
            else {
                rowColumns.push(Array(panelHeight).fill(' '.repeat(colWidth)));
            }
        }
        for (let lineIdx = 0; lineIdx < panelHeight; lineIdx++) {
            output.push(rowColumns.map(column => column[lineIdx] ?? ' '.repeat(colWidth)).join(' '));
        }
        output.push('');
    }
    output.push(`${DIM}  r=refresh  q=quit${RESET}`);
    if (output.length > termHeight)
        output.length = termHeight;
    return output.join('\n');
}
function compactStateRow(board, states, allStates) {
    return states
        .map(state => `${stateColor(state, allStates)}${state}:${board.counts[state] ?? 0}${RESET}`)
        .join(` ${DIM}│${RESET} `);
}
function renderMiniProject(board, width, height) {
    const lines = [];
    lines.push(`${FG.gray}┌${'─'.repeat(width - 2)}┐${RESET}`);
    lines.push(`${FG.gray}│${RESET}${padOrTruncate(`${BOLD}${board.displayName}${RESET}`, width - 2)}${FG.gray}│${RESET}`);
    lines.push(`${FG.gray}├${'─'.repeat(width - 2)}┤${RESET}`);
    if (board.error) {
        lines.push(`${FG.gray}│${RESET}${padOrTruncate(`${FG.red}${board.error}${RESET}`, width - 2)}${FG.gray}│${RESET}`);
    }
    else {
        const summary = [
            `${FG.green}${board.activeWorkers} active${RESET}`,
            `${FG.yellow}${board.mergingWorkers} merging${RESET}`,
            `${FG.red}${board.staleWorkers} stale${RESET}`,
            `${FG.cyan}${board.cards.length} cards${RESET}`,
        ].join(` ${DIM}│${RESET} `);
        lines.push(`${FG.gray}│${RESET}${padOrTruncate(summary, width - 2)}${FG.gray}│${RESET}`);
        const bs = board.states;
        const half = Math.ceil(bs.length / 2);
        lines.push(`${FG.gray}│${RESET}${padOrTruncate(compactStateRow(board, bs.slice(0, half), bs), width - 2)}${FG.gray}│${RESET}`);
        lines.push(`${FG.gray}│${RESET}${padOrTruncate(compactStateRow(board, bs.slice(half), bs), width - 2)}${FG.gray}│${RESET}`);
        const hotCards = board.cards
            .filter(card => card.blockedReason || ['running', 'waiting_input', 'needs_confirmation', 'stalled_submit'].includes(card.runtimeStatus || ''))
            .slice(0, 2)
            .map(card => `#${card.seq} ${card.blockedReason || card.runtimeStatus}`)
            .join(' · ');
        lines.push(`${FG.gray}│${RESET}${padOrTruncate(hotCards ? `${DIM}${hotCards}${RESET}` : `${DIM}No hot cards${RESET}`, width - 2)}${FG.gray}│${RESET}`);
    }
    while (lines.length < height - 1) {
        lines.push(`${FG.gray}│${RESET}${' '.repeat(width - 2)}${FG.gray}│${RESET}`);
    }
    lines.push(`${FG.gray}└${'─'.repeat(width - 2)}┘${RESET}`);
    return lines;
}
function renderMultiProject(boards, termWidth, termHeight) {
    const output = [];
    output.push(...renderHeader(termWidth));
    output.push('');
    output.push(`${DIM}  Multi-project compact board view${RESET}`);
    output.push('');
    const cols = boards.length <= 2 ? boards.length : boards.length <= 4 ? 2 : 3;
    const rows = Math.ceil(boards.length / cols);
    const panelWidth = Math.max(30, Math.floor((termWidth - (cols - 1)) / cols));
    const availableRows = Math.max(10, termHeight - output.length - 2);
    const panelHeight = Math.max(8, Math.floor(availableRows / rows));
    for (let row = 0; row < rows; row++) {
        const rowPanels = [];
        for (let col = 0; col < cols; col++) {
            const idx = row * cols + col;
            if (idx < boards.length)
                rowPanels.push(renderMiniProject(boards[idx], panelWidth, panelHeight));
            else
                rowPanels.push(Array(panelHeight).fill(' '.repeat(panelWidth)));
        }
        for (let line = 0; line < panelHeight; line++) {
            output.push(rowPanels.map(panel => panel[line] ?? ' '.repeat(panelWidth)).join(' '));
        }
        output.push('');
    }
    output.push(`${DIM}  r=refresh  q=quit${RESET}`);
    if (output.length > termHeight)
        output.length = termHeight;
    return output.join('\n');
}
function renderDashboard(boards) {
    const { cols, rows } = getTerminalSize();
    if (boards.length === 1)
        return renderSingleProject(boards[0], cols, rows);
    return renderMultiProject(boards, cols, rows);
}
async function runLive(projects, intervalMs) {
    process.stdout.write('\x1b[?1049h\x1b[?25l');
    let drawing = false;
    const cleanup = () => {
        process.stdout.write('\x1b[?25h');
        process.stdout.write('\x1b[?1049l');
        process.stdout.write('\x1b[0m');
        process.exit(0);
    };
    process.on('SIGINT', cleanup);
    process.on('SIGTERM', cleanup);
    const draw = async () => {
        if (drawing)
            return;
        drawing = true;
        try {
            const boards = await buildSnapshot(projects);
            process.stdout.write('\x1b[H\x1b[J');
            process.stdout.write(renderDashboard(boards));
        }
        finally {
            drawing = false;
        }
    };
    if (process.stdin.isTTY) {
        process.stdin.setRawMode(true);
        process.stdin.resume();
        process.stdin.setEncoding('utf-8');
        process.stdin.on('data', (key) => {
            if (key === 'q' || key === '\x03')
                cleanup();
            if (key === 'r')
                void draw();
        });
    }
    await draw();
    setInterval(() => { void draw(); }, intervalMs);
    return new Promise(() => { });
}
export async function executeCardDashboard(projects, flags) {
    const jsonOutput = !!flags.json;
    const watch = !flags.once;
    if (projects.length === 0)
        projects = discoverProjects();
    if (projects.length === 0) {
        if (jsonOutput) {
            console.log(JSON.stringify({ timestamp: new Date().toISOString(), projects: [] }, null, 2));
        }
        else {
            console.error('No projects found in ~/.coral/projects/');
        }
        process.exit(1);
    }
    const boards = await buildSnapshot(projects);
    if (jsonOutput) {
        const payload = {
            timestamp: new Date().toISOString(),
            projects: boards,
        };
        console.log(JSON.stringify(payload, null, 2));
        return;
    }
    if (watch) {
        const intervalMs = parseInt(process.env.SPS_CARD_DASHBOARD_INTERVAL || '5000', 10);
        await runLive(projects, intervalMs);
        return;
    }
    console.log(renderDashboard(boards));
}
//# sourceMappingURL=cardDashboard.js.map