import chalk from 'chalk';
import { die, relTime, truncate, isJsonMode } from '../lib/format.js';
import { resolveProvider, getAllProviders, getDefaultProviderId } from '../lib/cloud/registry.js';
import { insertTask, updateTaskStatus, getTaskById, listTasks as listStoredTasks, listActiveTasks } from '../lib/cloud/store.js';
import { renderStream } from '../lib/cloud/stream.js';
import { MAX_IMAGES_PER_DISPATCH } from '../lib/cloud/types.js';
import { resolveCloudPrompt, executeCloudDispatch } from '../lib/cloud/dispatch.js';
import { normalizeTriggerEvent, validateTrigger, writeJob, setJobEnabled, jobExists, GITHUB_TRIGGER_EVENTS } from '../lib/scheduling/routines.js';
import { emit } from '../lib/feed/events.js';
/** Return a chalk color function appropriate for the given task status. */
export function statusColor(status) {
    switch (status) {
        case 'queued':
        case 'allocating': return chalk.blue;
        case 'running': return chalk.yellow;
        case 'idle': return chalk.gray;
        case 'completed': return chalk.green;
        case 'input_required': return chalk.magenta;
        case 'failed': return chalk.red;
        case 'cancelled': return chalk.gray;
        default: return chalk.white;
    }
}
/** Register the `agents cloud` command tree (run, list, status, logs, cancel, message, providers). */
export function registerCloudCommands(program) {
    const cloud = program
        .command('cloud', { hidden: true })
        .description('Dispatch and manage cloud agent tasks across providers (Rush, Codex, Cursor, Factory, Antigravity, Host).')
        .addHelpText('after', `
Each agent runs in its own cloud. Pass --agent and the provider is auto-selected
(claude→rush, codex→codex, cursor→cursor, droid→factory, antigravity→antigravity); --provider overrides.

Providers:
  rush         Rush Cloud — Claude against a GitHub repo + branch → PR
  codex        Codex Cloud — runs in a pre-built Codex environment (--env)
  cursor       Cursor Cloud Agents — v1 REST API, with optional GitHub repos
  factory      Factory Droid Computer — droid exec on a cloud VM (--computer)
  antigravity  Gemini Managed Agents — Antigravity harness in a remote sandbox

Examples:
  # Dispatch a quick fix to Rush Cloud and stream the output
  agents cloud run "fix the flaky e2e in apps/web/tests/checkout.spec.ts" --provider rush --repo acme/example --branch main

  # Fire-and-forget (returns the task id, no streaming)
  agents cloud run "bump tailwind to v4 and fix the breaks" --provider rush --repo acme/example --no-follow

  # Multi-repo dispatch: touch both rush and rush-extension in one task
  agents cloud run "rename POST /v1/charge -> /v2/charge across server + extension" --provider rush --repo acme/example --repo acme/example-extension

  # Codex Cloud against a saved environment
  agents cloud run "add pytest fixtures for the new billing module" --provider codex --env env_a1b2c3 --agent codex --timeout 30m

  # Cursor Cloud Agents against a GitHub repository
  agents cloud run "fix the flaky parser test" --agent cursor --repo acme/example

  # Factory pod targeting a specific computer (Droid)
  agents cloud run "QA the new onboarding flow end-to-end" --provider factory --computer linux-vm-1 --agent droid

  # See every cloud task you've dispatched (most recent first)
  agents cloud list

  # Inspect a specific task
  agents cloud status tsk_4f2a91

  # Live-tail logs for a running task
  agents cloud logs tsk_4f2a91

  # Send a follow-up while the task is in needs-review
  agents cloud message tsk_4f2a91 "Looks good — also update the OpenAPI spec"

  # Cancel a runaway task
  agents cloud cancel tsk_4f2a91

  # See which providers are signed in and ready
  agents cloud providers
`);
    // ── agents cloud run ──────────────────────────────────────────────────
    cloud
        .command('run [prompt]')
        .description('Dispatch a task to a cloud agent.')
        .option('--provider <id>', 'Cloud backend: rush, codex, cursor, factory, antigravity, host (overrides agent auto-routing)')
        .option('--agent <name>', 'Agent to run: claude, codex, cursor, droid, antigravity (auto-routes to its native cloud)')
        .option('--repo <owner/repo>', 'GitHub repository. Repeatable for multi-repo dispatch (Rush or Cursor Cloud).', (value, previous) => {
        const acc = Array.isArray(previous) ? previous : [];
        acc.push(value);
        return acc;
    })
        .option('--branch <name>', 'Target git branch')
        .option('--on <event>', 'Register this run as an event trigger instead of dispatching now: pull_request (pr), push, issue_comment, workflow_run. Persists a trigger-bound routine.')
        .option('--action <name>', 'GitHub webhook action filter for --on triggers (e.g. labeled)')
        .option('--label <name>', 'GitHub label filter for --on triggers')
        .option('--name <name>', 'Routine name to register under (with --on). Defaults to a generated name.')
        .option('-p, --prompt <text>', 'Inline prompt (alternative to positional argument)')
        .option('--timeout <duration>', 'Kill after duration (e.g., 30m, 2h)')
        .option('--model <model>', 'Model override')
        .option('--env <id>', 'Codex Cloud environment ID')
        .option('--computer <name>', 'Factory/Droid computer target')
        .option('--device <name>', 'One of your machines as the target (a registered device, capability tag, or user@host). Implies --provider host.')
        .option('--remote-cwd <dir>', 'Working directory on the device (--provider host only)')
        .option('--any', 'With --device <cap> (a capability tag), pick any matching device instead of erroring when several match')
        .option('--autonomy <level>', 'Factory/Droid autonomy: low, medium, high (default high)')
        .option('--mode <mode>', 'Execution mode (e.g., plan, edit, full)')
        .option('--image <path>', `Attach an image (.png/.jpg/.webp) for vision dispatch. Repeatable, up to ${MAX_IMAGES_PER_DISPATCH} (Rush Cloud only).`, (value, previous) => {
        const acc = Array.isArray(previous) ? previous : [];
        acc.push(value);
        return acc;
    })
        .option('--skill <id>', 'Ride-along skill by id (or id@version). Repeatable (Rush Cloud only).', (value, previous) => {
        const acc = Array.isArray(previous) ? previous : [];
        acc.push(value);
        return acc;
    })
        .option('-b, --balanced', 'Shortcut for --strategy balanced. Route the factory run across all healthy accounts.')
        .option('--strategy <strategy>', 'Account selection strategy for the factory: balanced. Sends all healthy accounts so the factory pod rotates between them on rate-limit.')
        .option('--json', 'Structured JSON output')
        .option('--no-follow', 'Dispatch and exit without streaming output')
        .addHelpText('after', `
Examples:
  # Rush Cloud
  agents cloud run "fix the flaky test" --provider rush --repo user/repo
  agents cloud run task.md --provider rush --repo org/project --agent codex

  # Rush Cloud — multi-repo (clones each into /workspace/<owner>/<name>/)
  agents cloud run "refactor shared logger" --provider rush --repo user/rush --repo user/agents

  # Codex Cloud
  agents cloud run "add auth tests" --provider codex --env env_abc123

  # One of your own machines (agents hosts / agents devices), over SSH
  agents cloud run "run the nightly benchmark" --device gpu-box --agent claude
  agents cloud run "rebuild the index" --device gpu --any --remote-cwd ~/proj

  # Default provider (set in ~/.agents/agents.yaml)
  agents cloud run "refactor auth module" --repo user/repo

  # Same dispatch as a run placement: agents run <agent> "<task>" --cloud
`)
        .action(async (positionalPrompt, options) => {
        const json = isJsonMode(options);
        // Resolve prompt: --prompt flag, positional arg, or file
        const prompt = resolveCloudPrompt(options.prompt || positionalPrompt, {
            json,
            hint: 'agents cloud run "<task>" --repo <owner/repo>',
        });
        // --device names one of YOUR machines as the target — that only means
        // something to the host provider, so it implies --provider host rather
        // than silently riding along to a cloud backend that would ignore it.
        if (options.device && options.provider && options.provider !== 'host') {
            die(`--device targets your own machines (--provider host), not ${options.provider}. Drop --device, or use --provider host.`, 1, { json });
        }
        const explicitProvider = options.provider ?? (options.device ? 'host' : undefined);
        // Agent-aware: with no --provider, the agent routes to its native cloud
        // (claude→rush, codex→codex, droid→factory, antigravity→antigravity).
        const provider = resolveProvider(explicitProvider, options.agent);
        // --repo is repeatable: commander gives us an array via our collector.
        // A single --repo value arrives as a one-element array; keep the legacy
        // singular `repo` field in sync so providers that only know that field
        // still dispatch correctly.
        const repoValues = Array.isArray(options.repo)
            ? options.repo
            : options.repo
                ? [options.repo]
                : [];
        const dispatchOptions = {
            prompt,
            agent: options.agent,
            repo: repoValues[0],
            repos: repoValues.length > 0 ? repoValues : undefined,
            branch: options.branch,
            timeout: options.timeout,
            model: options.model,
            providerOptions: {},
        };
        if (options.env)
            dispatchOptions.providerOptions.env = options.env;
        if (options.computer)
            dispatchOptions.providerOptions.computer = options.computer;
        if (options.device)
            dispatchOptions.providerOptions.host = options.device;
        if (options.remoteCwd)
            dispatchOptions.providerOptions.remoteCwd = options.remoteCwd;
        if (options.any)
            dispatchOptions.providerOptions.any = true;
        if (options.autonomy)
            dispatchOptions.providerOptions.autonomy = options.autonomy;
        if (options.mode)
            dispatchOptions.providerOptions.mode = options.mode;
        if (options.balanced || options.strategy === 'balanced') {
            dispatchOptions.providerOptions.strategy = 'balanced';
        }
        // --on <event>: register this run as an event trigger instead of
        // dispatching now. We parse + validate the event, attach it to
        // dispatchOptions.trigger, and persist a trigger-bound routine so the
        // local webhook receiver can fire it (src/lib/triggers/webhook.ts).
        // Remote firing of the trigger is a follow-up.
        if (options.on) {
            const event = normalizeTriggerEvent(options.on);
            if (!event) {
                die(`Unknown --on event "${options.on}". Use one of: ${GITHUB_TRIGGER_EVENTS.join(', ')} (aliases: pr, comment, workflow).`, 1, { json });
            }
            const trigger = { type: 'github_event', event: event };
            if (repoValues[0])
                trigger.repo = repoValues[0];
            if (options.branch)
                trigger.branch = options.branch;
            if (options.action)
                trigger.action = options.action;
            if (options.label)
                trigger.label = options.label;
            const triggerErrors = validateTrigger(trigger);
            if (triggerErrors.length > 0)
                die(`Invalid trigger: ${triggerErrors.join(', ')}`, 1, { json });
            dispatchOptions.trigger = trigger;
            const routineName = options.name
                || `cloud-${event}-${(repoValues[0] || 'any').replace(/[^a-z0-9]+/gi, '-')}`.toLowerCase();
            if (jobExists(routineName)) {
                die(`A routine named "${routineName}" already exists. Pass --name to register under a different name.`, 1, { json });
            }
            const routine = {
                name: routineName,
                trigger,
                agent: options.agent || 'claude',
                mode: 'plan',
                effort: 'auto',
                timeout: options.timeout || '10m',
                enabled: true,
                prompt,
            };
            if (repoValues[0])
                routine.repo = repoValues[0];
            // --device controls where the enabled local receiver dispatches the run.
            // Device activation remains in this receiver's device manifest.
            if (options.device) {
                routine.host = options.device;
                if (options.remoteCwd)
                    routine.remoteCwd = options.remoteCwd;
            }
            writeJob(routine);
            setJobEnabled(routine.name, true);
            if (json) {
                console.log(JSON.stringify({ ok: true, registered: routineName, trigger }, null, 2));
            }
            else {
                console.log(chalk.green(`Registered trigger routine "${routineName}"`));
                console.log(`  ${chalk.dim('on:')}    ${event}${trigger.repo ? ` (${trigger.repo}${trigger.branch ? `@${trigger.branch}` : ''})` : ''}`);
                console.log(`  ${chalk.dim('agent:')} ${routine.agent}`);
                console.log(chalk.dim(`\nFires when a matching GitHub webhook is delivered to the local receiver. Remote firing is a follow-up.`));
            }
            return;
        }
        // One dispatch path for every cloud surface — lib/cloud/dispatch.ts owns
        // capability checks, the missing-target picker, persistence, streaming,
        // and the budget kill-switch.
        await executeCloudDispatch({
            provider,
            dispatchOptions,
            imagePaths: Array.isArray(options.image) ? options.image : [],
            skillIds: Array.isArray(options.skill) ? options.skill : [],
            follow: options.follow !== false,
            json,
        });
    });
    // ── agents cloud list ─────────────────────────────────────────────────
    cloud
        .command('list')
        .description('List cloud tasks.')
        .option('--provider <id>', 'Filter by provider')
        .option('--status <status>', 'Filter by status')
        .option('--limit <n>', 'Max results', '20')
        .option('--json', 'JSON output')
        .action(async (options) => {
        const json = isJsonMode(options);
        const providerId = options.provider;
        const status = options.status;
        const limit = parseInt(options.limit, 10) || 20;
        // Auto-refresh tasks still in transient states (queued, allocating, running, input_required).
        // Groups by provider to minimise resolver calls, refreshes each via provider.status().
        const activeTasks = listActiveTasks();
        if (activeTasks.length > 0) {
            const byProvider = new Map();
            for (const t of activeTasks) {
                if (providerId && t.provider !== providerId)
                    continue;
                let ids = byProvider.get(t.provider);
                if (!ids) {
                    ids = [];
                    byProvider.set(t.provider, ids);
                }
                ids.push(t.id);
            }
            const refreshJobs = [];
            for (const [pid, ids] of byProvider) {
                try {
                    const provider = resolveProvider(pid);
                    for (const id of ids) {
                        refreshJobs.push(provider.status(id)
                            .then((fresh) => { insertTask(fresh); })
                            .catch(() => { }));
                    }
                }
                catch {
                    // provider not configured — skip
                }
            }
            if (refreshJobs.length > 0) {
                await Promise.allSettled(refreshJobs);
            }
        }
        const tasks = listStoredTasks({ provider: providerId, status, limit });
        if (json) {
            process.stdout.write(JSON.stringify(tasks, null, 2) + '\n');
            return;
        }
        if (tasks.length === 0) {
            console.log(chalk.dim('No cloud tasks found.'));
            return;
        }
        // Table header
        const header = [
            chalk.dim('ID'.padEnd(14)),
            chalk.dim('Provider'.padEnd(10)),
            chalk.dim('Status'.padEnd(16)),
            chalk.dim('Agent'.padEnd(8)),
            chalk.dim('Prompt'.padEnd(40)),
            chalk.dim('When'),
        ].join('  ');
        console.log(header);
        console.log(chalk.dim('-'.repeat(100)));
        for (const t of tasks) {
            const row = [
                t.id.slice(0, 12).padEnd(14),
                t.provider.padEnd(10),
                statusColor(t.status)(t.status.padEnd(16)),
                (t.agent ?? '-').padEnd(8),
                truncate(t.prompt.replace(/\n/g, ' '), 40).padEnd(40),
                chalk.dim(relTime(t.createdAt)),
            ].join('  ');
            console.log(row);
        }
    });
    // ── agents cloud status ───────────────────────────────────────────────
    cloud
        .command('status <id>')
        .description('Show task detail and latest status.')
        .option('--json', 'JSON output')
        .action(async (id, options) => {
        const json = isJsonMode(options);
        // Try local first, then remote
        let task = getTaskById(id);
        const providerId = task?.provider;
        if (providerId) {
            try {
                const provider = resolveProvider(providerId);
                task = await provider.status(id);
                insertTask(task);
            }
            catch {
                // Fall back to local cache
            }
        }
        if (!task)
            die(`Task ${id} not found.`);
        if (json) {
            process.stdout.write(JSON.stringify(task, null, 2) + '\n');
            return;
        }
        console.log(`${chalk.bold('Task')} ${task.id}`);
        console.log(`  ${chalk.dim('Provider:')}  ${task.provider}`);
        console.log(`  ${chalk.dim('Status:')}    ${statusColor(task.status)(task.status)}`);
        if (task.agent)
            console.log(`  ${chalk.dim('Agent:')}     ${task.agent}`);
        if (task.repo)
            console.log(`  ${chalk.dim('Repo:')}      ${task.repo}`);
        if (task.branch)
            console.log(`  ${chalk.dim('Branch:')}    ${task.branch}`);
        if (task.prUrl)
            console.log(`  ${chalk.dim('PR:')}        ${task.prUrl}`);
        console.log(`  ${chalk.dim('Prompt:')}    ${truncate(task.prompt.replace(/\n/g, ' '), 80)}`);
        console.log(`  ${chalk.dim('Created:')}   ${relTime(task.createdAt)}`);
        if (task.summary) {
            console.log(`  ${chalk.dim('Summary:')}   ${truncate(task.summary.replace(/\n/g, ' '), 120)}`);
        }
    });
    // ── agents cloud logs ─────────────────────────────────────────────────
    cloud
        .command('logs <id>')
        .description('Stream live output from a cloud task.')
        .option('-f, --follow', 'Follow output (default for running tasks)', true)
        .option('--json', 'JSON event stream')
        .action(async (id, options) => {
        const json = isJsonMode(options);
        const task = getTaskById(id);
        if (!task)
            die(`Task ${id} not found locally. Run 'agents cloud list' first.`);
        const provider = resolveProvider(task.provider);
        try {
            // Live budget kill-switch (issue #399) — wrap the stream so a cap
            // breach mid-stream cancels the task server-side. Dormant when no caps.
            const { wrapStreamWithBudgetGate } = await import('../lib/budget/live-cloud.js');
            const gated = wrapStreamWithBudgetGate({
                provider,
                taskId: id,
                project: task.repo ?? task.repos?.[0] ?? process.cwd(),
                agent: task.agent ?? 'cloud',
                cwd: process.cwd(),
            });
            const eventSource = gated ? gated.wrap(provider.stream(id)) : provider.stream(id);
            const result = await renderStream(eventSource, { json });
            updateTaskStatus(id, result.status, {
                summary: result.summary,
                prUrl: result.prUrl,
            });
            emit('cloud.complete', { module: 'cloud', taskId: id, status: result.status, prUrl: result.prUrl });
            if (gated?.gate.breached()) {
                process.exitCode = 7;
            }
        }
        catch (err) {
            process.stderr.write(chalk.dim(`\nStream ended. ${err.message}\n`));
        }
    });
    // ── agents cloud cancel ───────────────────────────────────────────────
    cloud
        .command('cancel <id>')
        .description('Cancel a running cloud task.')
        .action(async (id) => {
        const task = getTaskById(id);
        if (!task)
            die(`Task ${id} not found.`);
        const provider = resolveProvider(task.provider);
        try {
            await provider.cancel(id);
            updateTaskStatus(id, 'cancelled');
            emit('cloud.cancel', { module: 'cloud', taskId: id, provider: task.provider });
            console.log(chalk.green(`Task ${id} cancelled.`));
        }
        catch (err) {
            die(err.message);
        }
    });
    // ── agents cloud message ──────────────────────────────────────────────
    cloud
        .command('message <id> <text>')
        .description('Send a follow-up message to a finished or needs-review task.')
        .action(async (id, text) => {
        const task = getTaskById(id);
        if (!task)
            die(`Task ${id} not found.`);
        const provider = resolveProvider(task.provider);
        try {
            await provider.message(id, text);
            updateTaskStatus(id, 'running');
            emit('cloud.message', { module: 'cloud', taskId: id });
            console.log(chalk.green(`Message sent to task ${id}. Agent is continuing.`));
        }
        catch (err) {
            die(err.message);
        }
    });
    // ── agents cloud providers ────────────────────────────────────────────
    cloud
        .command('providers')
        .description('List available cloud providers and their status.')
        .option('--json', 'JSON output')
        .action((options) => {
        const json = isJsonMode(options);
        const providers = getAllProviders();
        const defaultId = getDefaultProviderId();
        if (json) {
            const data = providers.map((p) => ({
                id: p.id,
                name: p.name,
                available: p.capabilities().available,
                default: p.id === defaultId,
            }));
            process.stdout.write(JSON.stringify(data, null, 2) + '\n');
            return;
        }
        console.log(chalk.bold('Cloud Providers\n'));
        for (const p of providers) {
            const available = p.capabilities().available;
            const isDefault = p.id === defaultId;
            const status = available ? chalk.green('ready') : chalk.dim('not configured');
            const defaultTag = isDefault ? chalk.cyan(' (default)') : '';
            console.log(`  ${p.id.padEnd(12)} ${p.name.padEnd(20)} ${status}${defaultTag}`);
        }
    });
    // ── agents cloud envs ─────────────────────────────────────────────────
    // Discover the pre-provisioned targets a provider runs inside — Codex
    // environments, Factory Droid Computers — so users don't copy opaque IDs
    // out of a web UI.
    cloud
        .command('envs')
        .alias('targets')
        .description('List the pre-provisioned targets (Codex environments / Droid Computers) you can dispatch into.')
        .option('--provider <id>', 'Only this provider (codex, factory, ...)')
        .option('--json', 'JSON output')
        .action(async (options) => {
        const json = isJsonMode(options);
        const only = options.provider;
        // Providers that run inside a pre-provisioned target declare targetKind.
        const providers = getAllProviders().filter((p) => p.targetKind && (!only || p.id === only));
        if (only && providers.length === 0) {
            die(`Provider '${only}' has no pre-provisioned targets (or is unknown). Targets apply to: codex, factory.`);
        }
        const results = [];
        for (const p of providers) {
            const kind = p.targetKind;
            if (!p.listTargets) {
                // Not enumerable (Codex). Surface guidance instead of a list.
                const guidance = kind === 'env'
                    ? 'Codex environments are not listable from the CLI. Browse/create them with `codex cloud` (interactive), then use --env <id>.'
                    : 'Not enumerable from the CLI.';
                results.push({ provider: p.id, kind, targets: [], note: guidance });
                continue;
            }
            try {
                const targets = await p.listTargets();
                results.push({ provider: p.id, kind, targets: targets.map((t) => ({ id: t.id, label: t.label })) });
            }
            catch (err) {
                results.push({ provider: p.id, kind, targets: [], note: err.message });
            }
        }
        if (json) {
            process.stdout.write(JSON.stringify(results, null, 2) + '\n');
            return;
        }
        for (const r of results) {
            console.log(chalk.bold(`\n${r.provider}`) + chalk.dim(` (${r.kind})`));
            if (r.targets.length > 0) {
                for (const t of r.targets) {
                    console.log(`  ${t.id}${t.label ? '  ' + chalk.dim(t.label) : ''}`);
                }
            }
            else {
                console.log(chalk.dim(`  ${r.note ?? 'none'}`));
            }
        }
    });
}
