import type { Command } from 'commander';
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
import { dirname, resolve } from 'path';
import { homedir } from 'os';
import { createServer, type Server } from 'http';
import { URL } from 'url';
import { loadConfigSafe } from './shared.js';

const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'];
const REDIRECT_PORT = 3000;
const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}`;

/** Expand ~ to the user's home directory. */
function expandPath(p: string): string {
  if (p.startsWith('~/') || p === '~') {
    return resolve(homedir(), p.slice(2));
  }
  return resolve(p);
}

/** Read and parse the OAuth2 credentials file. */
export function readCredentials(credentialsPath: string): {
  client_id: string;
  client_secret: string;
  redirect_uris?: string[];
} {
  if (!existsSync(credentialsPath)) {
    throw new Error(`Credentials file not found: ${credentialsPath}`);
  }

  const credentials = JSON.parse(readFileSync(credentialsPath, 'utf-8'));
  const { client_id, client_secret, redirect_uris } = credentials.installed ?? credentials.web ?? {};

  if (!client_id || !client_secret) {
    throw new Error('Invalid credentials file — missing client_id or client_secret');
  }

  return { client_id, client_secret, redirect_uris };
}

/** Generate the OAuth2 authorization URL. */
export function generateAuthUrl(clientId: string, clientSecret: string, redirectUri: string): string {
  const params = new URLSearchParams({
    client_id: clientId,
    redirect_uri: redirectUri,
    response_type: 'code',
    scope: SCOPES.join(' '),
    access_type: 'offline',
    prompt: 'consent',
  });
  return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
}

/** Exchange authorization code for tokens using Google's token endpoint. */
async function exchangeCodeForTokens(
  code: string,
  clientId: string,
  clientSecret: string,
  redirectUri: string,
): Promise<Record<string, unknown>> {
  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      code,
      client_id: clientId,
      client_secret: clientSecret,
      redirect_uri: redirectUri,
      grant_type: 'authorization_code',
    }),
  });

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`Token exchange failed (${response.status}): ${body}`);
  }

  return response.json() as Promise<Record<string, unknown>>;
}

/** Save token to disk with restrictive permissions (0o600). */
export function saveToken(tokenPath: string, token: unknown): void {
  const dir = dirname(tokenPath);
  if (!existsSync(dir)) {
    mkdirSync(dir, { recursive: true });
  }
  writeFileSync(tokenPath, JSON.stringify(token, null, 2), 'utf-8');
  try {
    chmodSync(tokenPath, 0o600);
  } catch {
    // chmod may fail on some filesystems — not critical
  }
}

/** Start a temporary HTTP server to receive the OAuth callback. */
function waitForCallback(port: number): Promise<{ code: string; server: Server }> {
  return new Promise((resolve, reject) => {
    const server = createServer((req, res) => {
      const url = new URL(req.url ?? '/', `http://localhost:${port}`);
      const code = url.searchParams.get('code');
      const error = url.searchParams.get('error');

      if (error) {
        res.writeHead(400, { 'Content-Type': 'text/html' });
        res.end(`<h1>Authorization failed</h1><p>${error}</p>`);
        reject(new Error(`OAuth error: ${error}`));
        server.close();
        return;
      }

      if (code) {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end('<h1>Authorization successful!</h1><p>You can close this tab and return to the terminal.</p>');
        resolve({ code, server });
        return;
      }

      res.writeHead(400, { 'Content-Type': 'text/html' });
      res.end('<h1>Missing authorization code</h1>');
    });

    server.listen(port, () => {});
    server.on('error', reject);
  });
}

/** Try to open a URL in the user's browser. */
async function openBrowser(url: string): Promise<boolean> {
  const { exec } = await import('child_process');
  const command = process.platform === 'darwin' ? 'open' : 'xdg-open';
  return new Promise((resolve) => {
    exec(`${command} ${JSON.stringify(url)}`, (error) => {
      resolve(!error);
    });
  });
}

/** Manual code entry via stdin. */
async function promptForCode(): Promise<string> {
  const readline = await import('readline');
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  return new Promise((resolve) => {
    rl.question('Enter the authorization code: ', (answer) => {
      rl.close();
      resolve(answer.trim());
    });
  });
}

export function registerGmailAuthCommand(program: Command): void {
  program
    .command('gmail-auth')
    .description('Authenticate with Gmail via OAuth2')
    .option('-c, --config <path>', 'Config file path')
    .option('--manual', 'Manually paste the authorization code instead of using a local server')
    .action(async (opts: { config?: string; manual?: boolean }) => {
      // 1. Load config
      const { config, error } = loadConfigSafe(opts.config);
      if (error || !config) {
        console.error(`Error: ${error ?? 'Could not load config'}`);
        process.exit(1);
      }

      const gmailConfig = config.automation.gmail;
      if (!gmailConfig) {
        console.error('Error: automation.gmail is not configured in config.yaml');
        process.exit(1);
      }

      // 2. Read credentials
      const credentialsPath = expandPath(gmailConfig.credentials_file ?? '~/.config/flynn/gmail-credentials.json');
      let creds: ReturnType<typeof readCredentials>;
      try {
        creds = readCredentials(credentialsPath);
      } catch (err) {
        console.error(`Error: ${err instanceof Error ? err.message : err}`);
        process.exit(1);
      }

      const tokenPath = expandPath(gmailConfig.token_file ?? '~/.config/flynn/gmail-token.json');

      // 3. Check if already authenticated
      if (existsSync(tokenPath)) {
        console.log(`Token already exists at ${tokenPath}`);
        console.log('Delete it first if you want to re-authenticate.');
        process.exit(0);
      }

      const redirectUri = opts.manual
        ? (creds.redirect_uris?.[0] ?? 'urn:ietf:wg:oauth:2.0:oob')
        : REDIRECT_URI;

      // 4. Generate auth URL
      const authUrl = generateAuthUrl(creds.client_id, creds.client_secret, redirectUri);

      if (opts.manual) {
        // Manual flow
        console.log('\nOpen this URL in your browser:\n');
        console.log(authUrl);
        console.log('');
        const code = await promptForCode();
        const token = await exchangeCodeForTokens(code, creds.client_id, creds.client_secret, redirectUri);
        saveToken(tokenPath, token);
        console.log(`\nToken saved to ${tokenPath}`);
      } else {
        // Local server flow
        console.log('Starting local server for OAuth callback...');

        let callbackResult: { code: string; server: Server };
        try {
          const callbackPromise = waitForCallback(REDIRECT_PORT);

          const opened = await openBrowser(authUrl);
          if (!opened) {
            console.log('\nCould not open browser. Open this URL manually:\n');
            console.log(authUrl);
          } else {
            console.log('\nBrowser opened. Complete the authorization flow...');
          }

          console.log(`\nWaiting for callback on http://localhost:${REDIRECT_PORT}...`);
          callbackResult = await callbackPromise;
        } catch (err) {
          console.error(`\nError: ${err instanceof Error ? err.message : err}`);
          console.log('\nTry again with --manual flag: flynn gmail-auth --manual');
          process.exit(1);
        }

        try {
          const token = await exchangeCodeForTokens(
            callbackResult.code,
            creds.client_id,
            creds.client_secret,
            redirectUri,
          );
          saveToken(tokenPath, token);
          console.log(`\nToken saved to ${tokenPath}`);
        } finally {
          callbackResult.server.close();
        }
      }

      console.log('Gmail authentication complete!');
    });
}
