import * as fs from 'fs';
import * as path from 'path';
import { Command } from 'commander';
import chalk from 'chalk';
import unzipper from 'unzipper';
import { requireConfig } from '../config';

const VALID_LANGUAGES = ['typescript', 'python', 'go', 'java', 'rust'] as const;
type ScaffoldLanguage = (typeof VALID_LANGUAGES)[number];

function isValidLanguage(lang: string): lang is ScaffoldLanguage {
  return (VALID_LANGUAGES as readonly string[]).includes(lang);
}

export function registerScaffold(program: Command): void {
  program
    .command('scaffold')
    .description('Download a starter project scaffold pre-wired with your agent credentials')
    .requiredOption('--agent-id <id>', 'Agent ID to scaffold for')
    .option(
      '--language <lang>',
      `SDK language (${VALID_LANGUAGES.join(', ')})`,
      'typescript',
    )
    .option('--out <directory>', 'Output directory for the extracted scaffold', '.')
    .action(async (opts: { agentId: string; language: string; out: string }) => {
      const { agentId, language, out: outDir } = opts;

      if (!isValidLanguage(language)) {
        console.error(
          chalk.red('Error:'),
          `Unsupported language '${language}'. Choose: ${VALID_LANGUAGES.join(', ')}`,
        );
        process.exit(1);
      }

      const config = requireConfig();

      // Resolve and create output directory
      const resolvedOut = path.resolve(outDir);
      if (!fs.existsSync(resolvedOut)) {
        fs.mkdirSync(resolvedOut, { recursive: true });
      }

      console.log(
        chalk.dim(`Downloading ${language} scaffold for agent ${agentId}...`),
      );

      try {
        // We need a raw binary response — fetch the token via apiRequest pattern
        // then make a raw fetch for the ZIP stream.
        const token = await getToken(config);

        const url = `${config.apiUrl}/sdk/scaffold/${encodeURIComponent(agentId)}?language=${encodeURIComponent(language)}`;
        const res = await fetch(url, {
          headers: { Authorization: `Bearer ${token}` },
        });

        if (!res.ok) {
          const text = await res.text();
          handleHttpError(res.status, text);
          process.exit(1);
        }

        if (res.body === null) {
          console.error(chalk.red('Error:'), 'Empty response body from server.');
          process.exit(1);
        }

        // Pipe the response body through unzipper into the output directory
        await new Promise<void>((resolve, reject) => {
          const nodeStream = streamFromWeb(res.body!);
          nodeStream
            .pipe(unzipper.Extract({ path: resolvedOut }))
            .on('close', resolve)
            .on('error', reject);
        });

        console.log(chalk.green('Scaffold extracted to:'), chalk.bold(resolvedOut));
        console.log('');
        console.log('Next steps:');
        console.log(
          `  1. ${chalk.cyan('cd')} ${resolvedOut}`,
        );
        if (language === 'typescript') {
          console.log(`  2. ${chalk.cyan('npm install')}`);
          console.log(`  3. Copy ${chalk.yellow('.env.example')} to ${chalk.yellow('.env')} and fill in your client secret`);
          console.log(`  4. ${chalk.cyan('npm run dev')}`);
        } else if (language === 'python') {
          console.log(`  2. ${chalk.cyan('pip install -r requirements.txt')}`);
          console.log(`  3. Copy ${chalk.yellow('.env.example')} to ${chalk.yellow('.env')} and fill in your client secret`);
          console.log(`  4. ${chalk.cyan('python main.py')}`);
        } else if (language === 'go') {
          console.log(`  2. ${chalk.cyan('go mod download')}`);
          console.log(`  3. Copy ${chalk.yellow('.env.example')} to ${chalk.yellow('.env')} and fill in your client secret`);
          console.log(`  4. ${chalk.cyan('go run main.go')}`);
        } else if (language === 'java') {
          console.log(`  2. ${chalk.cyan('mvn install')}`);
          console.log(`  3. Copy ${chalk.yellow('.env.example')} to ${chalk.yellow('.env')} and fill in your client secret`);
          console.log(`  4. ${chalk.cyan('mvn exec:java')}`);
        } else if (language === 'rust') {
          console.log(`  2. Copy ${chalk.yellow('.env.example')} to ${chalk.yellow('.env')} and fill in your client secret`);
          console.log(`  3. ${chalk.cyan('cargo run')}`);
        }
      } catch (err) {
        console.error(
          chalk.red('Error:'),
          err instanceof Error ? err.message : String(err),
        );
        process.exit(1);
      }
    });
}

/** Obtain a bearer token by making a dummy apiRequest that uses the token cache. */
async function getToken(config: import('../config').Config): Promise<string> {
  // apiRequest internally calls fetchToken which caches tokens.
  // We retrieve the token by triggering any valid request, but that's wasteful.
  // Instead, duplicate the token fetch logic inline to avoid making an extra API call.
  const body = new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: config.clientId,
    client_secret: config.clientSecret,
  });

  const res = await fetch(`${config.apiUrl}/oauth2/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: body.toString(),
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Authentication failed (${res.status}): ${text}`);
  }

  const data = (await res.json()) as { access_token: string };
  return data.access_token;
}

function handleHttpError(status: number, body: string): void {
  if (status === 400) {
    console.error(chalk.red('Error:'), `Invalid request: ${body}`);
  } else if (status === 401) {
    console.error(
      chalk.red('Error:'),
      'Authentication failed. Run `sentryagent configure` to update credentials.',
    );
  } else if (status === 403) {
    console.error(
      chalk.red('Error:'),
      'Access denied. You do not own this agent.',
    );
  } else if (status === 404) {
    console.error(
      chalk.red('Error:'),
      'Agent not found. Check the agent ID with `sentryagent list-agents`.',
    );
  } else {
    console.error(chalk.red('Error:'), `Server error (${status}): ${body}`);
  }
}

/**
 * Converts a WHATWG ReadableStream (from fetch) to a Node.js Readable stream.
 * Node 18+ supports ReadableStream natively via stream.Readable.fromWeb().
 */
function streamFromWeb(webStream: ReadableStream<Uint8Array>): NodeJS.ReadableStream {
  // Node.js 18+ has stream.Readable.fromWeb
  // eslint-disable-next-line @typescript-eslint/no-require-imports
  const { Readable } = require('stream') as typeof import('stream');
  return Readable.fromWeb(webStream as Parameters<typeof Readable.fromWeb>[0]) as NodeJS.ReadableStream;
}
