import { AgentStatus, resolveMode } from './agents.js';
import { getDelta } from './summarizer.js';
import { debug } from './debug.js';
import { buildClaudeLabelMap } from '../session/discover.js';
import { resolveTeammateDelivery } from './delivery.js';
import { hasUncommittedChanges } from './worktree.js';
/**
 * Truncate a bash command for status output.
 * Handles heredocs specially - shows the redirect target instead of contents.
 */
function truncateBashCommand(cmd, maxLen = 120) {
    // Detect heredoc patterns: cat <<'EOF' > path or cat << EOF > path
    const heredocMatch = cmd.match(/cat\s+<<['"]?(\w+)['"]?\s*>\s*([^\s]+)/);
    if (heredocMatch) {
        return `cat <<${heredocMatch[1]} > ${heredocMatch[2]}`;
    }
    // For regular commands, just truncate
    if (cmd.length <= maxLen)
        return cmd;
    return cmd.substring(0, maxLen - 3) + '...';
}
function eventToolName(event) {
    const eventType = event.type || '';
    if (eventType === 'bash')
        return 'Bash';
    if (eventType === 'file_write')
        return 'Write';
    if (eventType === 'file_create')
        return 'Create';
    if (eventType === 'file_read')
        return 'Read';
    if (eventType === 'file_delete')
        return 'Delete';
    if (eventType === 'directory_list')
        return 'List';
    if (eventType === 'tool_use')
        return event.tool || 'Tool';
    return null;
}
function eventToolSummary(event) {
    const value = event.command ||
        event.path ||
        event.args?.command ||
        event.args?.path ||
        event.input?.command ||
        event.input?.path ||
        '';
    return truncateBashCommand(String(value || eventToolName(event) || 'tool call'));
}
function recentToolCalls(events, max = 10) {
    return events
        .filter((event) => eventToolName(event))
        .slice(-max)
        .map((event) => ({
        tool: eventToolName(event) || 'Tool',
        summary: eventToolSummary(event),
        timestamp: typeof event.timestamp === 'string' ? event.timestamp : null,
    }));
}
/** Max files to name per category in the summary. Counts are always exact. */
const SUMMARY_MAX_FILE_NAMES = 6;
/** Max messages in the summary. */
const SUMMARY_MAX_MESSAGES = 3;
/** Max chars per message body in the summary. */
const SUMMARY_MESSAGE_MAX_CHARS = 400;
/**
 * Reduce a file path to just its basename for compact rendering. Keeps the
 * orchestrator oriented ("you touched types.ts") without dumping the full
 * absolute path every time. The full paths are still in `AgentStatusDetail`
 * for the verbose path.
 */
function basenameOf(p) {
    const ix = p.lastIndexOf('/');
    return ix < 0 ? p : p.slice(ix + 1);
}
/**
 * Trim long assistant messages to a fixed budget. We collapse leading
 * whitespace so the budget covers actual content, not indent.
 */
function trimMessage(msg, max = SUMMARY_MESSAGE_MAX_CHARS) {
    const s = msg.replace(/^\s+/, '');
    if (s.length <= max)
        return s;
    return s.slice(0, max - 1) + '…';
}
/** Pull at most `max` basenames out of a path list, preserving order. */
function compactFileList(paths, max = SUMMARY_MAX_FILE_NAMES) {
    const seen = new Set();
    const names = [];
    for (const p of paths) {
        const base = basenameOf(p);
        if (seen.has(base))
            continue;
        seen.add(base);
        names.push(base);
        if (names.length >= max)
            break;
    }
    return { count: paths.length, names };
}
/**
 * Project a full AgentStatusDetail down to a compact AgentStatusSummary.
 * Drops `prompt` entirely (caller knows what they queued), folds file lists
 * to basenames + counts, caps `last_messages` to 3 × {@link SUMMARY_MESSAGE_MAX_CHARS}.
 */
export function toAgentStatusSummary(detail) {
    return {
        agent_id: detail.agent_id,
        name: detail.name ?? null,
        agent_type: detail.agent_type,
        status: detail.status,
        delivery: detail.delivery ??
            resolveTeammateDelivery({ status: detail.status, prUrl: detail.pr_url }),
        duration: detail.duration,
        tool_count: detail.tool_count,
        has_errors: detail.has_errors,
        pr_url: detail.pr_url ?? null,
        files: {
            modified: compactFileList(detail.files_modified),
            created: compactFileList(detail.files_created),
            deleted: compactFileList(detail.files_deleted),
            read: { count: detail.files_read.length },
        },
        bash_commands: detail.bash_commands,
        last_messages: detail.last_messages
            .slice(-SUMMARY_MAX_MESSAGES)
            .map((m) => trimMessage(m)),
        host: detail.host ?? null,
        failure: detail.failure ?? null,
        workspace_dir: detail.workspace_dir ?? null,
        cursor: detail.cursor,
    };
}
/** Project a full TaskStatusResult down to the compact summary shape. */
export function toTaskStatusSummary(result) {
    return {
        task_name: result.task_name,
        agents: result.agents.map(toAgentStatusSummary),
        summary: result.summary,
        cursor: result.cursor,
    };
}
/** Spawn a new teammate in a task and return its initial metadata. */
export async function handleSpawn(manager, taskName, agentType, prompt, cwd, mode, effort = 'medium', parentSessionId = null, workspaceDir = null, version = null, name = null, after = [], model = null, envOverrides = null, taskType = null, cloudProvider = null, cloudSessionId = null, cloudRepo = null, cloudBranch = null, worktreeName = null, worktreePath = null, profileName = null, hostName = null, hostTarget = null, repoPath = null, 
/** The team's `--project`; grants resolve from it at launch. */
project = null) {
    const defaultMode = manager.getDefaultMode();
    const resolvedMode = resolveMode(mode, defaultMode);
    const resolvedEffort = effort ?? 'medium';
    debug(`[spawn] Spawning ${agentType} agent for task "${taskName}" [${resolvedMode}] effort=${resolvedEffort}${profileName ? ` profile=${profileName}` : ''}...`);
    // Budget pre-flight gate (issue #346). Teammates inherit the project's caps:
    // before launching one, project its estimated cost onto current spend and
    // refuse when on_exceed:block would be breached. Cross-vendor by construction
    // — a Claude teammate and a Codex teammate draw down the same per_project /
    // per_day pool. Dormant (no-op) when no caps are configured.
    {
        const gateCwd = cwd || workspaceDir || worktreePath || process.cwd();
        const { runPreflightGate } = await import('../budget/preflight.js');
        const gate = runPreflightGate({
            agent: agentType,
            model: model ?? `${agentType}-default`,
            mode: resolvedMode,
            prompt,
            project: gateCwd,
            cwd: gateCwd,
        });
        if (!gate.dormant && !gate.decision.allow) {
            throw new Error(`[budget] BLOCKED teammate "${taskName}" (${agentType}): ${gate.decision.reason}`);
        }
    }
    const agent = await manager.spawn(taskName, agentType, prompt, cwd, resolvedMode, resolvedEffort, parentSessionId, workspaceDir, version, name, after, model, envOverrides, taskType, cloudProvider, cloudSessionId, cloudRepo, cloudBranch, worktreeName, worktreePath, profileName, hostName, hostTarget, repoPath, project);
    debug(`[spawn] Spawned ${agentType} agent ${agent.agentId} for task "${taskName}"`);
    return {
        task_name: taskName,
        agent_id: agent.agentId,
        agent_type: agent.agentType,
        status: agent.status,
        started_at: agent.startedAt.toISOString(),
        version: agent.version,
        profile_name: agent.profileName,
        remote_session_id: agent.remoteSessionId,
        name: agent.name,
        after: agent.after,
        task_type: agent.taskType,
        cloud_provider: agent.cloudProvider,
        cloud_session_id: agent.cloudSessionId,
        host: agent.hostName,
    };
}
/** Retrieve the current status of all teammates in a task, with optional timestamp-based delta filtering. */
export async function handleStatus(manager, taskName, filter, since, // Optional ISO timestamp - return only events after this time
parentSessionId) {
    // Default to 'all' so callers see completed/failed agents unless they opt to filter
    const effectiveFilter = filter || 'all';
    const normalizedTaskName = taskName?.trim() || '';
    const normalizedParentSessionId = parentSessionId?.trim() || '';
    if (!normalizedTaskName && !normalizedParentSessionId) {
        throw new Error('task_name is required when parent_session_id is not provided');
    }
    const lookupLabel = normalizedParentSessionId && !normalizedTaskName
        ? `parent_session_id "${normalizedParentSessionId}"`
        : `task "${normalizedTaskName}"`;
    debug(`[status] Getting status for agents in ${lookupLabel} (filter=${effectiveFilter})...`);
    const allAgents = normalizedParentSessionId && !normalizedTaskName
        ? await manager.listByParentSession(normalizedParentSessionId)
        : await manager.listByTask(normalizedTaskName);
    // Filter agents by status ('all' shows everything)
    const agents = effectiveFilter === 'all'
        ? allAgents
        : allAgents.filter((a) => a.status === effectiveFilter);
    const agentStatuses = [];
    const counts = { pending: 0, running: 0, completed: 0, stranded: 0, failed: 0, stopped: 0 };
    // Count ALL agents for summary (not just filtered)
    for (const agent of allAgents) {
        if (agent.status === AgentStatus.PENDING)
            counts.pending++;
        else if (agent.status === AgentStatus.RUNNING)
            counts.running++;
        else if (agent.status === AgentStatus.COMPLETED)
            counts.completed++;
        else if (agent.status === AgentStatus.FAILED)
            counts.failed++;
        else if (agent.status === AgentStatus.STOPPED)
            counts.stopped++;
    }
    // Stranded count is computed over ALL agents, not the filter-narrowed view,
    // so a `--filter running` status still reports teammates that completed dirty.
    // Cache the probe result so we can reuse it when building details for the
    // filtered subset without probing the worktree twice.
    const uncommittedCache = new Map();
    for (const agent of allAgents) {
        if (agent.status !== AgentStatus.COMPLETED)
            continue;
        const shouldProbeWorktree = !agent.prUrl?.trim() &&
            !agent.hostName &&
            Boolean(agent.workspaceDir);
        const hasUncommitted = shouldProbeWorktree
            ? await hasUncommittedChanges(agent.workspaceDir)
            : false;
        uncommittedCache.set(agent.agentId, hasUncommitted);
        const delivery = resolveTeammateDelivery({
            status: agent.status,
            prUrl: agent.prUrl,
            hasUncommittedChanges: hasUncommitted,
        });
        if (delivery === 'stranded') {
            counts.stranded++;
        }
    }
    // Build details only for filtered agents
    let maxTimestamp = since || new Date(0).toISOString(); // Track max timestamp for cursor
    const claudeLabels = allAgents.some((agent) => agent.agentType === 'claude')
        ? buildClaudeLabelMap()
        : new Map();
    for (const agent of agents) {
        await agent.readNewEvents();
        const events = agent.events;
        // Use getDelta to filter events by timestamp (or get all if no since)
        const delta = getDelta(agent.agentId, agent.agentType, agent.status, events, since);
        // Find latest timestamp from this agent's events
        const latestEvent = events[events.length - 1];
        const agentTimestamp = latestEvent?.timestamp || new Date().toISOString();
        if (agentTimestamp > maxTimestamp) {
            maxTimestamp = agentTimestamp;
        }
        const hasUncommitted = uncommittedCache.get(agent.agentId) ?? false;
        const delivery = resolveTeammateDelivery({
            status: agent.status,
            prUrl: agent.prUrl,
            hasUncommittedChanges: hasUncommitted,
        });
        const detail = {
            agent_id: agent.agentId,
            agent_type: agent.agentType,
            status: agent.status,
            prompt: agent.prompt,
            started_at: agent.startedAt.toISOString(),
            completed_at: agent.completedAt?.toISOString() ?? null,
            duration: agent.duration(),
            version: agent.version,
            remote_session_id: agent.remoteSessionId,
            session_label: agent.remoteSessionId
                ? claudeLabels.get(agent.remoteSessionId) ?? null
                : null,
            name: agent.name,
            after: agent.after,
            task_type: agent.taskType,
            host: agent.hostName,
            workspace_dir: agent.workspaceDir,
            failure: agent.failure,
            mode: agent.mode,
            cloud_session_id: agent.cloudSessionId,
            cloud_provider: agent.cloudProvider,
            pr_url: agent.prUrl,
            delivery,
            files_created: delta.new_files_created,
            files_modified: delta.new_files_modified,
            files_read: delta.new_files_read,
            files_deleted: delta.new_files_deleted,
            bash_commands: delta.new_bash_commands.map((cmd) => truncateBashCommand(cmd)),
            recent_tool_calls: recentToolCalls(events),
            last_messages: delta.new_messages,
            tool_count: delta.new_tool_count,
            has_errors: delta.new_errors.length > 0,
            cursor: agentTimestamp,
        };
        agentStatuses.push(detail);
    }
    debug(`[status] ${lookupLabel}: returning ${agents.length}/${allAgents.length} agents (running=${counts.running}, completed=${counts.completed}, failed=${counts.failed}, stopped=${counts.stopped})`);
    return {
        task_name: normalizedTaskName,
        agents: agentStatuses,
        summary: counts,
        cursor: maxTimestamp, // Max timestamp across all agents
    };
}
/** List all known tasks grouped by task name, sorted by most recent activity. */
export async function handleTasks(manager, limit = 10) {
    debug(`[tasks] Listing tasks (limit=${limit})...`);
    const allAgents = await manager.listAll();
    // Group agents by taskName
    const taskMap = new Map();
    for (const agent of allAgents) {
        const existing = taskMap.get(agent.taskName) || [];
        existing.push(agent);
        taskMap.set(agent.taskName, existing);
    }
    const tasks = [];
    for (const [taskName, agents] of taskMap) {
        let pending = 0, running = 0, completed = 0, stranded = 0, failed = 0, stopped = 0;
        let earliestStart = null;
        let latestActivity = null;
        let workspaceDir = null;
        for (const agent of agents) {
            // Count by status
            if (agent.status === AgentStatus.PENDING)
                pending++;
            else if (agent.status === AgentStatus.RUNNING)
                running++;
            else if (agent.status === AgentStatus.COMPLETED)
                completed++;
            else if (agent.status === AgentStatus.FAILED)
                failed++;
            else if (agent.status === AgentStatus.STOPPED)
                stopped++;
            // Stranded = completed, no PR, local worktree still dirty. Probes the real
            // worktree so `teams tasks` and `teams list --status` don't classify lost
            // work as done (PHNX-2951).
            if (agent.status === AgentStatus.COMPLETED && !agent.prUrl?.trim() && !agent.hostName && agent.workspaceDir) {
                const delivery = resolveTeammateDelivery({
                    status: agent.status,
                    prUrl: agent.prUrl,
                    hasUncommittedChanges: await hasUncommittedChanges(agent.workspaceDir),
                });
                if (delivery === 'stranded') {
                    stranded++;
                }
            }
            // Track earliest start (created_at)
            if (!earliestStart || agent.startedAt < earliestStart) {
                earliestStart = agent.startedAt;
            }
            // Track latest activity (modified_at)
            // For running agents, use current time; for others use completedAt or startedAt
            const activityTime = agent.status === AgentStatus.RUNNING
                ? new Date()
                : (agent.completedAt || agent.startedAt);
            if (!latestActivity || activityTime > latestActivity) {
                latestActivity = activityTime;
            }
            // Use first non-null workspaceDir found
            if (!workspaceDir && agent.workspaceDir) {
                workspaceDir = agent.workspaceDir;
            }
        }
        tasks.push({
            task_name: taskName,
            agent_count: agents.length,
            pending,
            running,
            completed,
            stranded,
            failed,
            stopped,
            workspace_dir: workspaceDir,
            created_at: earliestStart?.toISOString() || new Date().toISOString(),
            modified_at: latestActivity?.toISOString() || new Date().toISOString(),
        });
    }
    // Sort by modified_at descending (most recent first)
    tasks.sort((a, b) => new Date(b.modified_at).getTime() - new Date(a.modified_at).getTime());
    // Apply limit
    const limitedTasks = tasks.slice(0, limit);
    debug(`[tasks] Returning ${limitedTasks.length}/${tasks.length} tasks`);
    return { tasks: limitedTasks };
}
/** Stop a specific teammate or all teammates in a task. */
export async function handleStop(manager, taskName, agentId) {
    if (agentId) {
        debug(`[stop] Stopping agent ${agentId} in task "${taskName}"...`);
        const agent = await manager.get(agentId);
        if (!agent) {
            debug(`[stop] Agent ${agentId} not found`);
            return {
                task_name: taskName,
                stopped: [],
                already_stopped: [],
                not_found: [agentId],
            };
        }
        if (agent.taskName !== taskName) {
            debug(`[stop] Agent ${agentId} not in task ${taskName}`);
            return { error: `Agent ${agentId} not in task ${taskName}` };
        }
        if (agent.status === AgentStatus.RUNNING) {
            const success = await manager.stop(agentId);
            debug(`[stop] Agent ${agentId}: ${success ? 'stopped' : 'failed to stop'}`);
            return {
                task_name: taskName,
                stopped: success ? [agentId] : [],
                already_stopped: success ? [] : [agentId],
                not_found: [],
            };
        }
        else {
            debug(`[stop] Agent ${agentId} already stopped (status=${agent.status})`);
            return {
                task_name: taskName,
                stopped: [],
                already_stopped: [agentId],
                not_found: [],
            };
        }
    }
    else {
        debug(`[stop] Stopping all agents in task "${taskName}"...`);
        const result = await manager.stopByTask(taskName);
        debug(`[stop] Task "${taskName}": stopped ${result.stopped.length}, already_stopped ${result.alreadyStopped.length}`);
        return {
            task_name: taskName,
            stopped: result.stopped,
            already_stopped: result.alreadyStopped,
            not_found: [],
        };
    }
}
