/**
 * Step: login — Sign in to Claude (Pro/Max) via OAuth PKCE manual flow.
 *
 * Mirrors the URL Claude Code's `claude auth login --claudeai` actually
 * builds (captured from the official CLI). This is the manual / paste-code
 * variant: the user visits the authorize URL, completes login in their
 * browser, and pastes the code back. We exchange it for tokens and write
 * `~/.claude/.credentials.json`.
 *
 * Usage:
 *   bun setup/index.ts --step login            # phase 1: print authorize URL
 *   bun setup/index.ts --step login --code <c> # phase 2: exchange the code
 *
 * Phase 1 stashes the PKCE verifier + state in /tmp/ejclaw-claude-login.json
 * (mode 0600). Phase 2 reads it back, exchanges the code, writes credentials.
 *
 * This step is run by hand for re-auth. Once `.credentials.json` exists, the
 * existing token-refresh loop (src/token-refresh.ts) keeps it fresh.
 */

import crypto from 'crypto';
import fs from 'fs';
import os from 'os';
import path from 'path';

import { logger } from '../src/logger.js';
import { emitStatus } from './status.js';

const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
const AUTHORIZE_URL = 'https://claude.com/cai/oauth/authorize';
const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
const REDIRECT_URI = 'https://platform.claude.com/oauth/code/callback';
const SCOPES = [
  'org:create_api_key',
  'user:profile',
  'user:inference',
  'user:sessions:claude_code',
  'user:mcp_servers',
  'user:file_upload',
];

const STATE_FILE = path.join(os.tmpdir(), 'ejclaw-claude-login.json');
const CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');

interface PendingState {
  verifier: string;
  state: string;
  createdAt: number;
}

interface ExchangeResponse {
  access_token: string;
  refresh_token?: string;
  expires_in: number;
  scope?: string;
  account?: {
    email_address?: string;
    has_claude_max?: boolean;
    has_claude_pro?: boolean;
  };
}

function base64url(buf: Buffer): string {
  return buf
    .toString('base64')
    .replace(/=+$/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
}

function generatePkce(): { verifier: string; challenge: string } {
  const verifier = base64url(crypto.randomBytes(32));
  const challenge = base64url(
    crypto.createHash('sha256').update(verifier).digest(),
  );
  return { verifier, challenge };
}

function buildAuthorizeUrl(state: string, challenge: string): string {
  const url = new URL(AUTHORIZE_URL);
  url.searchParams.append('code', 'true');
  url.searchParams.append('client_id', CLIENT_ID);
  url.searchParams.append('response_type', 'code');
  url.searchParams.append('redirect_uri', REDIRECT_URI);
  url.searchParams.append('scope', SCOPES.join(' '));
  url.searchParams.append('code_challenge', challenge);
  url.searchParams.append('code_challenge_method', 'S256');
  url.searchParams.append('state', state);
  return url.toString();
}

function writePendingState(p: PendingState): void {
  fs.writeFileSync(STATE_FILE, JSON.stringify(p), { mode: 0o600 });
}

function readPendingState(): PendingState | null {
  if (!fs.existsSync(STATE_FILE)) return null;
  try {
    return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as PendingState;
  } catch {
    return null;
  }
}

async function exchangeCode(
  code: string,
  verifier: string,
  state: string,
): Promise<ExchangeResponse> {
  // The success page can hand the user a value of `code#state` or just
  // `code` — accept both.
  const [bareCode, embeddedState] = code.split('#');
  const stateForExchange = embeddedState || state;
  const body = JSON.stringify({
    grant_type: 'authorization_code',
    code: bareCode,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: verifier,
    state: stateForExchange,
  });

  const res = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body,
  });

  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(
      `Token exchange failed (${res.status}): ${text.slice(0, 400)}`,
    );
  }
  return (await res.json()) as ExchangeResponse;
}

function writeCredentials(resp: ExchangeResponse): void {
  const expiresAt = Date.now() + resp.expires_in * 1000;
  const creds = {
    claudeAiOauth: {
      accessToken: resp.access_token,
      refreshToken: resp.refresh_token || '',
      expiresAt,
      scopes: resp.scope ? resp.scope.split(' ') : SCOPES,
      subscriptionType: resp.account?.has_claude_max
        ? 'max'
        : resp.account?.has_claude_pro
          ? 'pro'
          : '',
    },
  };
  const dir = path.dirname(CREDS_PATH);
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
  const tmp = `${CREDS_PATH}.tmp`;
  fs.writeFileSync(tmp, JSON.stringify(creds, null, 2), { mode: 0o600 });
  fs.renameSync(tmp, CREDS_PATH);
}

interface Args {
  code: string | null;
}

function parseArgs(args: string[]): Args {
  let code: string | null = null;
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--code' && args[i + 1]) {
      code = args[++i];
    }
  }
  return { code };
}

export async function run(args: string[]): Promise<void> {
  const { code } = parseArgs(args);

  if (!code) {
    // Phase 1: produce the authorize URL.
    const { verifier, challenge } = generatePkce();
    const state = base64url(crypto.randomBytes(32));
    writePendingState({ verifier, state, createdAt: Date.now() });
    const url = buildAuthorizeUrl(state, challenge);

    console.log('AUTHORIZE_URL=' + url);
    console.log(
      'NEXT: open the URL above in your browser, complete the login,' +
        ' and re-run this command with --code <pasted_code>.',
    );
    emitStatus('LOGIN_URL', { STATUS: 'awaiting_code', URL: url });
    return;
  }

  // Phase 2: exchange the code.
  const pending = readPendingState();
  if (!pending) {
    emitStatus('LOGIN', {
      STATUS: 'failed',
      ERROR: 'no_pending_state — run phase 1 first',
    });
    process.exit(4);
  }
  if (Date.now() - pending.createdAt > 30 * 60 * 1000) {
    logger.warn(
      'Pending login state is older than 30 minutes — proceeding anyway',
    );
  }

  try {
    const resp = await exchangeCode(code, pending.verifier, pending.state);
    writeCredentials(resp);
    fs.unlinkSync(STATE_FILE);
    const newScopes = (resp.scope || SCOPES.join(' ')).split(' ').sort();
    logger.info(
      { scopes: newScopes, expiresInMin: Math.round(resp.expires_in / 60) },
      'Wrote ~/.claude/.credentials.json',
    );
    emitStatus('LOGIN', {
      STATUS: 'success',
      CREDENTIALS_PATH: CREDS_PATH,
      SCOPES: newScopes.join(','),
    });
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    logger.error({ err: message }, 'Token exchange failed');
    emitStatus('LOGIN', { STATUS: 'failed', ERROR: message });
    process.exit(4);
  }
}
