import { createCommand } from '../../types';
import { getVersion, getCompareUrl, getReleaseUrl, toTag } from '../../version';
import { getCommand } from '../../command-prefix';
import { z } from 'zod';
import { ErrorCode, createError, exitWithError } from '../../errors';
import * as tui from '../../tui';
import { downloadWithProgress } from '../../download';
import { $ } from 'bun';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import { access, constants } from 'node:fs/promises';
import { StructuredError } from '@agentuity/core';
export const PermissionError = StructuredError('PermissionError')();
async function checkWritePermission(binaryPath) {
    try {
        await access(binaryPath, constants.W_OK);
    }
    catch {
        throw new PermissionError({
            binaryPath,
            reason: `Cannot write to ${binaryPath}. You may need to run with elevated permissions (e.g., sudo) or reinstall to a user-writable location.`,
            message: `Permission denied: Cannot write to ${binaryPath}`,
        });
    }
    const parentDir = dirname(binaryPath);
    try {
        await access(parentDir, constants.W_OK);
    }
    catch {
        throw new PermissionError({
            binaryPath,
            reason: `Cannot write to directory ${parentDir}. You may need to run with elevated permissions (e.g., sudo) or reinstall to a user-writable location.`,
            message: `Permission denied: Cannot write to directory ${parentDir}`,
        });
    }
}
const UpgradeOptionsSchema = z.object({
    force: z.boolean().optional().describe('Force upgrade even if version is the same'),
});
const UpgradeResponseSchema = z.object({
    upgraded: z.boolean().describe('Whether an upgrade was performed'),
    from: z.string().describe('Version before upgrade'),
    to: z.string().describe('Version after upgrade'),
    message: z.string().describe('Status message'),
});
/**
 * Check if running from a compiled executable (not via bun/bunx)
 * @internal Exported for testing
 */
export function isRunningFromExecutable() {
    const scriptPath = process.argv[1] || '';
    // Check if running from compiled binary (uses Bun's virtual filesystem)
    // When compiled with `bun build --compile`, the script path is in the virtual /$bunfs/root/ directory
    // Note: process.argv[0] is the executable path (e.g., /usr/local/bin/agentuity), not 'bun'
    if (scriptPath.startsWith('/$bunfs/root/')) {
        return true;
    }
    // If running via bun/bunx (from node_modules or .ts files), it's not an executable
    if (Bun.main.includes('/node_modules/') || Bun.main.includes('.ts')) {
        return false;
    }
    // Check if in a bin directory but not in node_modules (globally installed)
    const normalized = Bun.main;
    const isGlobal = normalized.includes('/bin/') &&
        !normalized.includes('/node_modules/') &&
        !normalized.includes('/packages/cli/bin');
    return isGlobal;
}
/**
 * Get the OS and architecture for downloading the binary
 * @internal Exported for testing
 */
export function getPlatformInfo() {
    const platform = process.platform;
    const arch = process.arch;
    let os;
    let archStr;
    switch (platform) {
        case 'darwin':
            os = 'darwin';
            break;
        case 'linux':
            os = 'linux';
            break;
        default:
            throw new Error(`Unsupported platform: ${platform}`);
    }
    switch (arch) {
        case 'x64':
            archStr = 'x64';
            break;
        case 'arm64':
            archStr = 'arm64';
            break;
        default:
            throw new Error(`Unsupported architecture: ${arch}`);
    }
    return { os, arch: archStr };
}
/**
 * Fetch the latest version from the API
 * @internal Exported for testing
 */
export async function fetchLatestVersion() {
    const response = await fetch('https://agentuity.sh/release/sdk/version', {
        signal: AbortSignal.timeout(10000), // 10 second timeout
    });
    if (!response.ok) {
        throw new Error(`Failed to fetch version: ${response.statusText}`);
    }
    const version = await response.text();
    const trimmedVersion = version.trim();
    // Validate version format
    if (!/^v?[0-9]+\.[0-9]+\.[0-9]+/.test(trimmedVersion) ||
        trimmedVersion.includes('message') ||
        trimmedVersion.includes('error') ||
        trimmedVersion.includes('<html>')) {
        throw new Error(`Invalid version format received: ${trimmedVersion}`);
    }
    // Ensure version has 'v' prefix
    return trimmedVersion.startsWith('v') ? trimmedVersion : `v${trimmedVersion}`;
}
/**
 * Download the binary for the specified version
 */
async function downloadBinary(version, platform) {
    const { os, arch } = platform;
    const url = `https://agentuity.sh/release/sdk/${version}/${os}/${arch}`;
    const tmpDir = tmpdir();
    const tmpFile = join(tmpDir, `agentuity-${randomUUID()}`);
    const gzFile = `${tmpFile}.gz`;
    const stream = await downloadWithProgress({
        url,
        message: `Downloading version ${version}...`,
    });
    // Write to temp file
    const writer = Bun.file(gzFile).writer();
    for await (const chunk of stream) {
        writer.write(chunk);
    }
    await writer.end();
    // Verify file was downloaded
    if (!(await Bun.file(gzFile).exists())) {
        throw new Error('Download failed - file not created');
    }
    // Decompress using gunzip
    try {
        await $ `gunzip ${gzFile}`.quiet();
    }
    catch (error) {
        if (await Bun.file(gzFile).exists()) {
            await $ `rm ${gzFile}`.quiet();
        }
        throw new Error(`Decompression failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
    // Verify decompressed file exists
    if (!(await Bun.file(tmpFile).exists())) {
        throw new Error('Decompression failed - file not found');
    }
    // Verify it's a valid binary
    const fileType = await $ `file ${tmpFile}`.text();
    if (!fileType.match(/(executable|ELF|Mach-O|PE32)/i)) {
        throw new Error('Downloaded file is not a valid executable');
    }
    // Make executable
    await $ `chmod 755 ${tmpFile}`.quiet();
    return tmpFile;
}
/**
 * Validate the downloaded binary by running version command
 */
async function validateBinary(binaryPath, expectedVersion) {
    try {
        // Use spawn to capture both stdout and stderr
        const proc = Bun.spawn([binaryPath, 'version'], {
            stdout: 'pipe',
            stderr: 'pipe',
        });
        const [stdout, stderr] = await Promise.all([
            new Response(proc.stdout).text(),
            new Response(proc.stderr).text(),
        ]);
        const exitCode = await proc.exited;
        if (exitCode !== 0) {
            const errorDetails = [];
            if (stdout.trim())
                errorDetails.push(`stdout: ${stdout.trim()}`);
            if (stderr.trim())
                errorDetails.push(`stderr: ${stderr.trim()}`);
            const details = errorDetails.length > 0 ? `\n${errorDetails.join('\n')}` : '';
            throw new Error(`Failed with exit code ${exitCode}${details}`);
        }
        const actualVersion = stdout.trim();
        // Normalize versions for comparison (remove 'v' prefix)
        const normalizedExpected = expectedVersion.replace(/^v/, '');
        const normalizedActual = actualVersion.replace(/^v/, '');
        if (normalizedActual !== normalizedExpected) {
            throw new Error(`Version mismatch: expected ${expectedVersion}, got ${actualVersion}`);
        }
    }
    catch (error) {
        if (error instanceof Error) {
            throw new Error(`Binary validation failed: ${error.message}`);
        }
        throw new Error('Binary validation failed');
    }
}
/**
 * Replace the current binary with the new one
 * Uses platform-specific safe replacement strategies
 */
async function replaceBinary(newBinaryPath, currentBinaryPath) {
    const platform = process.platform;
    if (platform === 'darwin' || platform === 'linux') {
        // Unix: Use atomic move via temp file
        const backupPath = `${currentBinaryPath}.backup`;
        const tempPath = `${currentBinaryPath}.new`;
        try {
            // Copy new binary to temp location next to current binary
            await $ `cp ${newBinaryPath} ${tempPath}`.quiet();
            await $ `chmod 755 ${tempPath}`.quiet();
            // Backup current binary
            if (await Bun.file(currentBinaryPath).exists()) {
                await $ `cp ${currentBinaryPath} ${backupPath}`.quiet();
            }
            // Atomic rename
            await $ `mv ${tempPath} ${currentBinaryPath}`.quiet();
            // Clean up backup after successful replacement
            if (await Bun.file(backupPath).exists()) {
                await $ `rm ${backupPath}`.quiet();
            }
        }
        catch (error) {
            // Try to restore backup if replacement failed
            if (await Bun.file(backupPath).exists()) {
                await $ `mv ${backupPath} ${currentBinaryPath}`.quiet();
            }
            // Clean up temp file if it exists
            if (await Bun.file(tempPath).exists()) {
                await $ `rm ${tempPath}`.quiet();
            }
            throw error;
        }
    }
    else {
        throw new Error(`Unsupported platform for binary replacement: ${platform}`);
    }
}
export const command = createCommand({
    name: 'upgrade',
    description: 'Upgrade the CLI to the latest version',
    executable: true,
    skipUpgradeCheck: true,
    tags: ['update'],
    examples: [
        {
            command: getCommand('upgrade'),
            description: 'Check for updates and prompt to upgrade',
        },
        {
            command: getCommand('upgrade --force'),
            description: 'Force upgrade even if already on latest version',
        },
    ],
    schema: {
        options: UpgradeOptionsSchema,
        response: UpgradeResponseSchema,
    },
    async handler(ctx) {
        const { logger, options } = ctx;
        const { force } = ctx.opts;
        const currentVersion = getVersion();
        // Use process.execPath to get the actual file path (Bun.main is virtual for compiled binaries)
        const currentBinaryPath = process.execPath;
        try {
            // Fetch latest version
            const latestVersion = await tui.spinner({
                message: 'Checking for updates...',
                clearOnSuccess: true,
                callback: async () => await fetchLatestVersion(),
            });
            // Compare versions
            const normalizedCurrent = currentVersion.replace(/^v/, '');
            const normalizedLatest = latestVersion.replace(/^v/, '');
            if (normalizedCurrent === normalizedLatest && !force) {
                const message = `Already on latest version ${currentVersion}`;
                tui.success(message);
                return {
                    upgraded: false,
                    from: currentVersion,
                    to: latestVersion,
                    message,
                };
            }
            // Show version info
            if (!force) {
                tui.info(`Current version: ${tui.muted(normalizedCurrent)}`);
                tui.info(`Latest version:  ${tui.bold(normalizedLatest)}`);
                tui.newline();
                if (toTag(currentVersion) !== toTag(latestVersion)) {
                    tui.warning(`What's changed:  ${tui.link(getCompareUrl(currentVersion, latestVersion))}`);
                }
                tui.success(`Release notes:   ${tui.link(getReleaseUrl(latestVersion))}`);
                tui.newline();
            }
            // Check write permissions before prompting - fail early with helpful message
            try {
                await checkWritePermission(currentBinaryPath);
            }
            catch (error) {
                if (error instanceof PermissionError) {
                    tui.error('Unable to upgrade: permission denied');
                    tui.newline();
                    tui.warning(`The CLI binary at ${tui.bold(error.binaryPath)} is not writable.`);
                    tui.newline();
                    if (process.env.AGENTUITY_RUNTIME) {
                        console.log('You cannot self-upgrade the agentuity cli in the cloud runtime.');
                        console.log('The runtime will automatically update the cli and other software');
                        console.log('within a day or so. If you need assistance, please contact us');
                        console.log('at support@agentuity.com.');
                    }
                    else {
                        console.log('To fix this, you can either:');
                        console.log(`  1. Run with elevated permissions: ${tui.muted('sudo agentuity upgrade')}`);
                        console.log(`  2. Reinstall to a user-writable location`);
                    }
                    tui.newline();
                    exitWithError(createError(ErrorCode.PERMISSION_DENIED, 'Upgrade failed: permission denied', {
                        path: error.binaryPath,
                    }), logger, options.errorFormat);
                }
                throw error;
            }
            // Confirm upgrade
            if (!force) {
                const shouldUpgrade = await tui.confirm('Do you want to upgrade?', true);
                if (!shouldUpgrade) {
                    const message = 'Upgrade cancelled';
                    tui.info(message);
                    return {
                        upgraded: false,
                        from: currentVersion,
                        to: latestVersion,
                        message,
                    };
                }
            }
            // Get platform info
            const platform = getPlatformInfo();
            // Download binary
            const tmpBinaryPath = await tui.spinner({
                type: 'progress',
                message: 'Downloading...',
                callback: async () => await downloadBinary(latestVersion, platform),
            });
            // Validate binary
            await tui.spinner({
                message: 'Validating binary...',
                callback: async () => await validateBinary(tmpBinaryPath, latestVersion),
            });
            // Replace binary
            await tui.spinner({
                message: 'Installing...',
                callback: async () => await replaceBinary(tmpBinaryPath, currentBinaryPath),
            });
            // Clean up temp file
            if (await Bun.file(tmpBinaryPath).exists()) {
                await $ `rm ${tmpBinaryPath}`.quiet();
            }
            const message = normalizedCurrent === normalizedLatest
                ? `Successfully upgraded to ${normalizedLatest}`
                : `Successfully upgraded from ${normalizedCurrent} to ${normalizedLatest}`;
            tui.success(message);
            return {
                upgraded: true,
                from: currentVersion,
                to: latestVersion,
                message,
            };
        }
        catch (error) {
            let errorDetails = {
                error: error instanceof Error ? error.message : 'Unknown error',
            };
            if (error instanceof Error && error.message.includes('Binary validation failed')) {
                const match = error.message.match(/Failed with exit code (\d+)\n(stdout: .+\n)?(stderr: .+)?/s);
                if (match) {
                    const exitCode = match[1];
                    const stdout = match[2]?.replace('stdout: ', '').trim();
                    const stderr = match[3]?.replace('stderr: ', '').trim();
                    errorDetails = {
                        validation_exit_code: exitCode,
                        ...(stdout && { validation_stdout: stdout }),
                        ...(stderr && { validation_stderr: stderr }),
                    };
                }
            }
            exitWithError(createError(ErrorCode.INTERNAL_ERROR, 'Upgrade failed', errorDetails), logger, options.errorFormat);
        }
    },
});
//# sourceMappingURL=index.js.map