import crypto from 'crypto';
import * as http from 'node:http';
import * as net from 'node:net';
import { Auth } from './index';
import { Log } from '../util/log';
import { createVerboseFetch } from '../util/verbose-fetch';

/**
 * Auth Plugins Module
 *
 * Provides OAuth and API authentication methods for various providers.
 * Based on OpenCode's plugin system (opencode-anthropic-auth, opencode-copilot-auth).
 */

const log = Log.create({ service: 'auth-plugins' });
const verboseFetch = createVerboseFetch(fetch, { caller: 'auth-plugins' });

/**
 * OAuth callback result types
 */
export type AuthResult =
  | { type: 'failed' }
  | {
      type: 'success';
      provider?: string;
      refresh: string;
      access: string;
      expires: number;
      enterpriseUrl?: string;
    }
  | { type: 'success'; provider?: string; key: string };

/**
 * Auth method prompt configuration
 */
export interface AuthPrompt {
  type: 'text' | 'select';
  key: string;
  message: string;
  placeholder?: string;
  options?: Array<{ label: string; value: string; hint?: string }>;
  condition?: (inputs: Record<string, string>) => boolean;
  validate?: (value: string) => string | undefined;
}

/**
 * OAuth authorization result
 */
export interface AuthorizeResult {
  url?: string;
  instructions?: string;
  method: 'code' | 'auto';
  callback: (code?: string) => Promise<AuthResult>;
}

/**
 * Auth method definition
 */
export interface AuthMethod {
  label: string;
  type: 'oauth' | 'api';
  prompts?: AuthPrompt[];
  authorize?: (
    inputs: Record<string, string>
  ) => Promise<AuthorizeResult | AuthResult>;
}

/**
 * Auth plugin definition
 */
export interface AuthPlugin {
  provider: string;
  methods: AuthMethod[];
  loader?: (
    getAuth: () => Promise<Auth.Info | undefined>,
    provider: any
  ) => Promise<{
    apiKey?: string;
    baseURL?: string;
    fetch?: typeof fetch;
  }>;
}

/**
 * PKCE utilities
 */
function generateRandomString(length: number): string {
  return crypto.randomBytes(length).toString('base64url');
}

function generateCodeChallenge(verifier: string): string {
  return crypto.createHash('sha256').update(verifier).digest('base64url');
}

async function generatePKCE() {
  const verifier = generateRandomString(32);
  const challenge = generateCodeChallenge(verifier);
  return { verifier, challenge };
}

/**
 * Anthropic OAuth Configuration
 * Used for Claude Pro/Max subscription authentication
 */
const ANTHROPIC_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';

/**
 * Anthropic OAuth Plugin
 * Supports:
 * - Claude Pro/Max OAuth login
 * - API key creation via OAuth
 * - Manual API key entry
 */
const AnthropicPlugin: AuthPlugin = {
  provider: 'anthropic',
  methods: [
    {
      label: 'Claude Pro/Max',
      type: 'oauth',
      async authorize() {
        const pkce = await generatePKCE();

        const url = new URL('https://claude.ai/oauth/authorize');
        url.searchParams.set('code', 'true');
        url.searchParams.set('client_id', ANTHROPIC_CLIENT_ID);
        url.searchParams.set('response_type', 'code');
        url.searchParams.set(
          'redirect_uri',
          'https://console.anthropic.com/oauth/code/callback'
        );
        url.searchParams.set(
          'scope',
          'org:create_api_key user:profile user:inference'
        );
        url.searchParams.set('code_challenge', pkce.challenge);
        url.searchParams.set('code_challenge_method', 'S256');
        url.searchParams.set('state', pkce.verifier);

        return {
          url: url.toString(),
          instructions: 'Paste the authorization code here: ',
          method: 'code' as const,
          async callback(code?: string): Promise<AuthResult> {
            if (!code) return { type: 'failed' };

            const splits = code.split('#');
            const result = await verboseFetch(
              'https://console.anthropic.com/v1/oauth/token',
              {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                  code: splits[0],
                  state: splits[1],
                  grant_type: 'authorization_code',
                  client_id: ANTHROPIC_CLIENT_ID,
                  redirect_uri:
                    'https://console.anthropic.com/oauth/code/callback',
                  code_verifier: pkce.verifier,
                }),
              }
            );

            if (!result.ok) {
              log.error(() => ({
                message: 'anthropic oauth token exchange failed',
                status: result.status,
              }));
              return { type: 'failed' };
            }

            const json = await result.json();
            return {
              type: 'success',
              refresh: json.refresh_token,
              access: json.access_token,
              expires: Date.now() + json.expires_in * 1000,
            };
          },
        };
      },
    },
    {
      label: 'Create an API Key',
      type: 'oauth',
      async authorize() {
        const pkce = await generatePKCE();

        const url = new URL('https://console.anthropic.com/oauth/authorize');
        url.searchParams.set('code', 'true');
        url.searchParams.set('client_id', ANTHROPIC_CLIENT_ID);
        url.searchParams.set('response_type', 'code');
        url.searchParams.set(
          'redirect_uri',
          'https://console.anthropic.com/oauth/code/callback'
        );
        url.searchParams.set(
          'scope',
          'org:create_api_key user:profile user:inference'
        );
        url.searchParams.set('code_challenge', pkce.challenge);
        url.searchParams.set('code_challenge_method', 'S256');
        url.searchParams.set('state', pkce.verifier);

        return {
          url: url.toString(),
          instructions: 'Paste the authorization code here: ',
          method: 'code' as const,
          async callback(code?: string): Promise<AuthResult> {
            if (!code) return { type: 'failed' };

            const splits = code.split('#');
            const tokenResult = await verboseFetch(
              'https://console.anthropic.com/v1/oauth/token',
              {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                  code: splits[0],
                  state: splits[1],
                  grant_type: 'authorization_code',
                  client_id: ANTHROPIC_CLIENT_ID,
                  redirect_uri:
                    'https://console.anthropic.com/oauth/code/callback',
                  code_verifier: pkce.verifier,
                }),
              }
            );

            if (!tokenResult.ok) {
              log.error(() => ({
                message: 'anthropic oauth token exchange failed',
                status: tokenResult.status,
              }));
              return { type: 'failed' };
            }

            const credentials = await tokenResult.json();

            // Create API key using the access token
            const apiKeyResult = await verboseFetch(
              'https://api.anthropic.com/api/oauth/claude_cli/create_api_key',
              {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/json',
                  Authorization: `Bearer ${credentials.access_token}`,
                },
              }
            ).then((r) => r.json());

            return { type: 'success', key: apiKeyResult.raw_key };
          },
        };
      },
    },
    {
      label: 'Manually enter API Key',
      type: 'api',
    },
  ],
  async loader(getAuth, provider) {
    const auth = await getAuth();
    if (!auth || auth.type !== 'oauth') return {};

    // Zero out cost for max plan users
    if (provider?.models) {
      for (const model of Object.values(provider.models)) {
        (model as any).cost = {
          input: 0,
          output: 0,
          cache: {
            read: 0,
            write: 0,
          },
        };
      }
    }

    return {
      apiKey: 'oauth-token-used-via-custom-fetch',
      async fetch(input: RequestInfo | URL, init?: RequestInit) {
        let currentAuth = await getAuth();
        if (!currentAuth || currentAuth.type !== 'oauth')
          return fetch(input, init);

        // Refresh token if expired
        if (!currentAuth.access || currentAuth.expires < Date.now()) {
          log.info(() => ({
            message: 'refreshing anthropic oauth token',
          }));
          const response = await verboseFetch(
            'https://console.anthropic.com/v1/oauth/token',
            {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                grant_type: 'refresh_token',
                refresh_token: currentAuth.refresh,
                client_id: ANTHROPIC_CLIENT_ID,
              }),
            }
          );

          if (!response.ok) {
            throw new Error(`Token refresh failed: ${response.status}`);
          }

          const json = await response.json();
          await Auth.set('anthropic', {
            type: 'oauth',
            refresh: json.refresh_token,
            access: json.access_token,
            expires: Date.now() + json.expires_in * 1000,
          });
          currentAuth = {
            type: 'oauth',
            refresh: json.refresh_token,
            access: json.access_token,
            expires: Date.now() + json.expires_in * 1000,
          };
        }

        // Add oauth beta and other required betas
        const incomingBeta =
          (init?.headers as Record<string, string>)?.['anthropic-beta'] || '';
        const incomingBetasList = incomingBeta
          .split(',')
          .map((b) => b.trim())
          .filter(Boolean);

        const mergedBetas = [
          ...new Set([
            'oauth-2025-04-20',
            'claude-code-20250219',
            'interleaved-thinking-2025-05-14',
            'fine-grained-tool-streaming-2025-05-14',
            ...incomingBetasList,
          ]),
        ].join(',');

        const headers: Record<string, string> = {
          ...(init?.headers as Record<string, string>),
          authorization: `Bearer ${currentAuth.access}`,
          'anthropic-beta': mergedBetas,
        };
        delete headers['x-api-key'];

        return fetch(input, {
          ...init,
          headers,
        });
      },
    };
  },
};

/**
 * GitHub Copilot OAuth Configuration
 */
const COPILOT_CLIENT_ID = 'Iv1.b507a08c87ecfe98';
const COPILOT_HEADERS = {
  'User-Agent': 'GitHubCopilotChat/0.32.4',
  'Editor-Version': 'vscode/1.105.1',
  'Editor-Plugin-Version': 'copilot-chat/0.32.4',
  'Copilot-Integration-Id': 'vscode-chat',
};

function normalizeDomain(url: string): string {
  return url.replace(/^https?:\/\//, '').replace(/\/$/, '');
}

function getCopilotUrls(domain: string) {
  return {
    DEVICE_CODE_URL: `https://${domain}/login/device/code`,
    ACCESS_TOKEN_URL: `https://${domain}/login/oauth/access_token`,
    COPILOT_API_KEY_URL: `https://api.${domain}/copilot_internal/v2/token`,
  };
}

/**
 * GitHub Copilot OAuth Plugin
 * Supports:
 * - GitHub.com Copilot
 * - GitHub Enterprise Copilot
 */
const GitHubCopilotPlugin: AuthPlugin = {
  provider: 'github-copilot',
  methods: [
    {
      type: 'oauth',
      label: 'Login with GitHub Copilot',
      prompts: [
        {
          type: 'select',
          key: 'deploymentType',
          message: 'Select GitHub deployment type',
          options: [
            {
              label: 'GitHub.com',
              value: 'github.com',
              hint: 'Public',
            },
            {
              label: 'GitHub Enterprise',
              value: 'enterprise',
              hint: 'Data residency or self-hosted',
            },
          ],
        },
        {
          type: 'text',
          key: 'enterpriseUrl',
          message: 'Enter your GitHub Enterprise URL or domain',
          placeholder: 'company.ghe.com or https://company.ghe.com',
          condition: (inputs) => inputs.deploymentType === 'enterprise',
          validate: (value) => {
            if (!value) return 'URL or domain is required';
            try {
              const url = value.includes('://')
                ? new URL(value)
                : new URL(`https://${value}`);
              if (!url.hostname) return 'Please enter a valid URL or domain';
              return undefined;
            } catch {
              return 'Please enter a valid URL (e.g., company.ghe.com or https://company.ghe.com)';
            }
          },
        },
      ],
      async authorize(inputs = {}): Promise<AuthorizeResult> {
        const deploymentType = inputs.deploymentType || 'github.com';

        let domain = 'github.com';
        let actualProvider = 'github-copilot';

        if (deploymentType === 'enterprise') {
          const enterpriseUrl = inputs.enterpriseUrl;
          domain = normalizeDomain(enterpriseUrl);
          actualProvider = 'github-copilot-enterprise';
        }

        const urls = getCopilotUrls(domain);

        const deviceResponse = await verboseFetch(urls.DEVICE_CODE_URL, {
          method: 'POST',
          headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json',
            'User-Agent': 'GitHubCopilotChat/0.35.0',
          },
          body: JSON.stringify({
            client_id: COPILOT_CLIENT_ID,
            scope: 'read:user',
          }),
        });

        if (!deviceResponse.ok) {
          throw new Error('Failed to initiate device authorization');
        }

        const deviceData = (await deviceResponse.json()) as {
          verification_uri: string;
          user_code: string;
          device_code: string;
          interval: number;
        };

        return {
          url: deviceData.verification_uri,
          instructions: `Enter code: ${deviceData.user_code}`,
          method: 'auto',
          async callback(): Promise<AuthResult> {
            while (true) {
              const response = await verboseFetch(urls.ACCESS_TOKEN_URL, {
                method: 'POST',
                headers: {
                  Accept: 'application/json',
                  'Content-Type': 'application/json',
                  'User-Agent': 'GitHubCopilotChat/0.35.0',
                },
                body: JSON.stringify({
                  client_id: COPILOT_CLIENT_ID,
                  device_code: deviceData.device_code,
                  grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
                }),
              });

              if (!response.ok) return { type: 'failed' };

              const data = (await response.json()) as {
                access_token?: string;
                error?: string;
              };

              if (data.access_token) {
                const result: AuthResult = {
                  type: 'success',
                  refresh: data.access_token,
                  access: '',
                  expires: 0,
                };

                if (actualProvider === 'github-copilot-enterprise') {
                  (result as any).provider = 'github-copilot-enterprise';
                  (result as any).enterpriseUrl = domain;
                }

                return result;
              }

              if (data.error === 'authorization_pending') {
                await new Promise((resolve) =>
                  setTimeout(resolve, deviceData.interval * 1000)
                );
                continue;
              }

              if (data.error) return { type: 'failed' };

              await new Promise((resolve) =>
                setTimeout(resolve, deviceData.interval * 1000)
              );
            }
          },
        };
      },
    },
  ],
  async loader(getAuth, provider) {
    const info = await getAuth();
    if (!info || info.type !== 'oauth') return {};

    // Zero out cost for copilot users
    if (provider?.models) {
      for (const model of Object.values(provider.models)) {
        (model as any).cost = {
          input: 0,
          output: 0,
          cache: {
            read: 0,
            write: 0,
          },
        };
      }
    }

    // Set baseURL based on deployment type
    const enterpriseUrl = (info as any).enterpriseUrl;
    const baseURL = enterpriseUrl
      ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}`
      : 'https://api.githubcopilot.com';

    return {
      baseURL,
      apiKey: 'oauth-token-used-via-custom-fetch',
      async fetch(input: RequestInfo | URL, init?: RequestInit) {
        let currentInfo = await getAuth();
        if (!currentInfo || currentInfo.type !== 'oauth')
          return fetch(input, init);

        // Refresh token if expired
        if (!currentInfo.access || currentInfo.expires < Date.now()) {
          const domain = (currentInfo as any).enterpriseUrl
            ? normalizeDomain((currentInfo as any).enterpriseUrl)
            : 'github.com';
          const urls = getCopilotUrls(domain);

          log.info(() => ({ message: 'refreshing github copilot token' }));
          const response = await verboseFetch(urls.COPILOT_API_KEY_URL, {
            headers: {
              Accept: 'application/json',
              Authorization: `Bearer ${currentInfo.refresh}`,
              ...COPILOT_HEADERS,
            },
          });

          if (!response.ok) {
            throw new Error(`Token refresh failed: ${response.status}`);
          }

          const tokenData = (await response.json()) as {
            token: string;
            expires_at: number;
          };

          const saveProviderID = (currentInfo as any).enterpriseUrl
            ? 'github-copilot-enterprise'
            : 'github-copilot';
          await Auth.set(saveProviderID, {
            type: 'oauth',
            refresh: currentInfo.refresh,
            access: tokenData.token,
            expires: tokenData.expires_at * 1000,
            ...((currentInfo as any).enterpriseUrl && {
              enterpriseUrl: (currentInfo as any).enterpriseUrl,
            }),
          } as Auth.Info);

          currentInfo = {
            type: 'oauth',
            refresh: currentInfo.refresh,
            access: tokenData.token,
            expires: tokenData.expires_at * 1000,
          };
        }

        // Detect agent calls and vision requests
        let isAgentCall = false;
        let isVisionRequest = false;
        try {
          const body =
            typeof init?.body === 'string' ? JSON.parse(init.body) : init?.body;
          if (body?.messages) {
            isAgentCall = body.messages.some(
              (msg: any) => msg.role && ['tool', 'assistant'].includes(msg.role)
            );
            isVisionRequest = body.messages.some(
              (msg: any) =>
                Array.isArray(msg.content) &&
                msg.content.some((part: any) => part.type === 'image_url')
            );
          }
        } catch {}

        const headers: Record<string, string> = {
          ...(init?.headers as Record<string, string>),
          ...COPILOT_HEADERS,
          Authorization: `Bearer ${currentInfo.access}`,
          'Openai-Intent': 'conversation-edits',
          'X-Initiator': isAgentCall ? 'agent' : 'user',
        };

        if (isVisionRequest) {
          headers['Copilot-Vision-Request'] = 'true';
        }

        delete headers['x-api-key'];
        delete headers['authorization'];

        return fetch(input, {
          ...init,
          headers,
        });
      },
    };
  },
};

/**
 * OpenAI ChatGPT OAuth Configuration
 * Used for ChatGPT Plus/Pro subscription authentication via Codex backend
 */
const OPENAI_CLIENT_ID = 'app_EMoamEEEZ73f0CkXaXp7hrann';
const OPENAI_AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
const OPENAI_TOKEN_URL = 'https://auth.openai.com/oauth/token';
const OPENAI_REDIRECT_URI = 'http://localhost:1455/auth/callback';
const OPENAI_SCOPE = 'openid profile email offline_access';

/**
 * OpenAI ChatGPT OAuth Plugin
 * Supports:
 * - ChatGPT Plus/Pro OAuth login
 * - Manual API key entry
 *
 * Note: This is a simplified implementation that uses manual code entry.
 * The full opencode-openai-codex-auth plugin uses a local server on port 1455.
 */
const OpenAIPlugin: AuthPlugin = {
  provider: 'openai',
  methods: [
    {
      label: 'ChatGPT Plus/Pro (OAuth)',
      type: 'oauth',
      async authorize() {
        const pkce = await generatePKCE();
        const state = generateRandomString(16);

        const url = new URL(OPENAI_AUTHORIZE_URL);
        url.searchParams.set('response_type', 'code');
        url.searchParams.set('client_id', OPENAI_CLIENT_ID);
        url.searchParams.set('redirect_uri', OPENAI_REDIRECT_URI);
        url.searchParams.set('scope', OPENAI_SCOPE);
        url.searchParams.set('code_challenge', pkce.challenge);
        url.searchParams.set('code_challenge_method', 'S256');
        url.searchParams.set('state', state);
        url.searchParams.set('id_token_add_organizations', 'true');
        url.searchParams.set('codex_cli_simplified_flow', 'true');
        url.searchParams.set('originator', 'codex_cli_rs');

        return {
          url: url.toString(),
          instructions:
            'After authorizing, copy the URL from your browser address bar and paste it here (or just the code parameter): ',
          method: 'code' as const,
          async callback(input?: string): Promise<AuthResult> {
            if (!input) return { type: 'failed' };

            // Parse authorization input - can be full URL, code#state, or just code
            let code: string | undefined;
            let receivedState: string | undefined;

            try {
              const inputUrl = new URL(input.trim());
              code = inputUrl.searchParams.get('code') ?? undefined;
              receivedState = inputUrl.searchParams.get('state') ?? undefined;
            } catch {
              // Not a URL, try other formats
              if (input.includes('#')) {
                const [c, s] = input.split('#', 2);
                code = c;
                receivedState = s;
              } else if (input.includes('code=')) {
                const params = new URLSearchParams(input);
                code = params.get('code') ?? undefined;
                receivedState = params.get('state') ?? undefined;
              } else {
                code = input.trim();
              }
            }

            if (!code) {
              log.error(() => ({
                message: 'openai oauth no code provided',
              }));
              return { type: 'failed' };
            }

            // Exchange authorization code for tokens
            const tokenResult = await verboseFetch(OPENAI_TOKEN_URL, {
              method: 'POST',
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
              },
              body: new URLSearchParams({
                grant_type: 'authorization_code',
                client_id: OPENAI_CLIENT_ID,
                code,
                code_verifier: pkce.verifier,
                redirect_uri: OPENAI_REDIRECT_URI,
              }),
            });

            if (!tokenResult.ok) {
              log.error(() => ({
                message: 'openai oauth token exchange failed',
                status: tokenResult.status,
              }));
              return { type: 'failed' };
            }

            const json = await tokenResult.json();
            if (
              !json.access_token ||
              !json.refresh_token ||
              typeof json.expires_in !== 'number'
            ) {
              log.error(() => ({
                message: 'openai oauth token response missing fields',
              }));
              return { type: 'failed' };
            }

            return {
              type: 'success',
              refresh: json.refresh_token,
              access: json.access_token,
              expires: Date.now() + json.expires_in * 1000,
            };
          },
        };
      },
    },
    {
      label: 'Manually enter API Key',
      type: 'api',
    },
  ],
  async loader(getAuth, provider) {
    const auth = await getAuth();
    if (!auth || auth.type !== 'oauth') return {};

    // Note: Full OpenAI Codex support would require additional request transformations
    // For now, this provides basic OAuth token management
    return {
      apiKey: 'oauth-token-used-via-custom-fetch',
      baseURL: 'https://chatgpt.com/backend-api',
      async fetch(input: RequestInfo | URL, init?: RequestInit) {
        let currentAuth = await getAuth();
        if (!currentAuth || currentAuth.type !== 'oauth')
          return fetch(input, init);

        // Refresh token if expired
        if (!currentAuth.access || currentAuth.expires < Date.now()) {
          log.info(() => ({ message: 'refreshing openai oauth token' }));
          const response = await verboseFetch(OPENAI_TOKEN_URL, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: new URLSearchParams({
              grant_type: 'refresh_token',
              refresh_token: currentAuth.refresh,
              client_id: OPENAI_CLIENT_ID,
            }),
          });

          if (!response.ok) {
            throw new Error(`Token refresh failed: ${response.status}`);
          }

          const json = await response.json();
          await Auth.set('openai', {
            type: 'oauth',
            refresh: json.refresh_token,
            access: json.access_token,
            expires: Date.now() + json.expires_in * 1000,
          });
          currentAuth = {
            type: 'oauth',
            refresh: json.refresh_token,
            access: json.access_token,
            expires: Date.now() + json.expires_in * 1000,
          };
        }

        const headers: Record<string, string> = {
          ...(init?.headers as Record<string, string>),
          authorization: `Bearer ${currentAuth.access}`,
        };
        delete headers['x-api-key'];

        return fetch(input, {
          ...init,
          headers,
        });
      },
    };
  },
};

/**
 * Google OAuth Configuration
 * Used for Google AI Pro/Ultra subscription authentication
 *
 * These credentials are from the official Gemini CLI (google-gemini/gemini-cli)
 * and are public for installed applications as per Google OAuth documentation:
 * https://developers.google.com/identity/protocols/oauth2#installed
 */
const GOOGLE_OAUTH_CLIENT_ID =
  '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
const GOOGLE_OAUTH_CLIENT_SECRET = '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';
const GOOGLE_OAUTH_SCOPES = [
  // Note: We intentionally do NOT include generative-language.* scopes here
  // because they are not registered for the Gemini CLI OAuth client (see issue #93).
  // Instead, we rely on the fallback mechanism to use API keys when OAuth fails
  // with scope errors (see issue #100).
  'https://www.googleapis.com/auth/cloud-platform',
  'https://www.googleapis.com/auth/userinfo.email',
  'https://www.googleapis.com/auth/userinfo.profile',
];

// Google OAuth endpoints
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';

/**
 * Get an available port for the OAuth callback server.
 * Supports configurable port via OAUTH_CALLBACK_PORT or GOOGLE_OAUTH_CALLBACK_PORT
 * environment variable. Falls back to automatic port discovery (port 0) if not configured.
 *
 * Based on Gemini CLI implementation:
 * https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
 */
async function getGoogleOAuthPort(): Promise<number> {
  // Check for environment variable override (useful for containers/firewalls)
  // Support both OAUTH_CALLBACK_PORT (Gemini CLI style) and GOOGLE_OAUTH_CALLBACK_PORT
  const portStr =
    process.env['OAUTH_CALLBACK_PORT'] ||
    process.env['GOOGLE_OAUTH_CALLBACK_PORT'];
  if (portStr) {
    const port = parseInt(portStr, 10);
    if (!isNaN(port) && port > 0 && port <= 65535) {
      log.info(() => ({
        message: 'using configured oauth callback port',
        port,
      }));
      return port;
    }
    log.warn(() => ({
      message: 'invalid OAUTH_CALLBACK_PORT, using auto discovery',
      value: portStr,
    }));
  }

  // Discover an available port by binding to port 0
  return new Promise((resolve, reject) => {
    const server = net.createServer();
    server.listen(0, () => {
      const address = server.address() as net.AddressInfo;
      const port = address.port;
      server.close(() => resolve(port));
    });
    server.on('error', reject);
  });
}

/**
 * Check if browser launch should be suppressed.
 * When NO_BROWSER=true, use manual code entry flow instead of localhost redirect.
 *
 * Based on Gemini CLI's config.isBrowserLaunchSuppressed() functionality.
 */
function isBrowserSuppressed(): boolean {
  const noBrowser = process.env['NO_BROWSER'];
  return noBrowser === 'true' || noBrowser === '1';
}

/**
 * Get the OAuth callback host for server binding.
 * Defaults to 'localhost' but can be configured via OAUTH_CALLBACK_HOST.
 * Use '0.0.0.0' in Docker containers to allow external connections.
 */
function getOAuthCallbackHost(): string {
  return process.env['OAUTH_CALLBACK_HOST'] || 'localhost';
}

/**
 * Google Code Assist redirect URI for manual code entry flow
 * This is used when NO_BROWSER=true or in headless environments
 * Based on Gemini CLI implementation
 */
const GOOGLE_CODEASSIST_REDIRECT_URI = 'https://codeassist.google.com/authcode';

/**
 * Google OAuth Plugin
 * Supports:
 * - Google AI Pro/Ultra OAuth login (browser mode with localhost redirect)
 * - Google AI Pro/Ultra OAuth login (manual code entry for NO_BROWSER mode)
 * - Manual API key entry
 *
 * Note: This plugin uses OAuth 2.0 with PKCE for Google AI subscription authentication.
 * After authenticating, you can use Gemini models with subscription benefits.
 *
 * The OAuth flow supports two modes:
 * 1. Browser mode (default): Opens browser, uses localhost redirect server
 * 2. Manual code entry (NO_BROWSER=true): Shows URL, user pastes authorization code
 *
 * Based on Gemini CLI implementation:
 * https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
 */
const GooglePlugin: AuthPlugin = {
  provider: 'google',
  methods: [
    {
      label: 'Google AI Pro/Ultra (OAuth - Browser)',
      type: 'oauth',
      async authorize() {
        // Check if browser is suppressed - if so, recommend manual method
        if (isBrowserSuppressed()) {
          log.info(() => ({
            message: 'NO_BROWSER is set, use manual code entry method instead',
          }));
        }

        const pkce = await generatePKCE();
        const state = generateRandomString(16);

        // Get an available port BEFORE starting the server
        // This fixes the race condition where port was 0 when building redirect URI
        const serverPort = await getGoogleOAuthPort();
        const host = getOAuthCallbackHost();
        // The redirect URI sent to Google must use localhost (loopback IP)
        // even if we bind to a different host (like 0.0.0.0 in Docker)
        const redirectUri = `http://localhost:${serverPort}/oauth/callback`;

        log.info(() => ({
          message: 'starting google oauth server',
          port: serverPort,
          host,
          redirectUri,
        }));

        // Create server to handle OAuth redirect
        const server = http.createServer();

        const authPromise = new Promise<{ code: string; state: string }>(
          (resolve, reject) => {
            server.on('request', (req, res) => {
              const url = new URL(req.url!, `http://localhost:${serverPort}`);
              const code = url.searchParams.get('code');
              const receivedState = url.searchParams.get('state');
              const error = url.searchParams.get('error');

              if (error) {
                res.writeHead(400, { 'Content-Type': 'text/html' });
                res.end(`
                <html>
                  <body>
                    <h1>Authentication Failed</h1>
                    <p>Error: ${error}</p>
                    <p>You can close this window.</p>
                  </body>
                </html>
              `);
                server.close();
                reject(new Error(`OAuth error: ${error}`));
                return;
              }

              if (code && receivedState) {
                if (receivedState !== state) {
                  res.writeHead(400, { 'Content-Type': 'text/html' });
                  res.end('Invalid state parameter');
                  server.close();
                  reject(new Error('State mismatch - possible CSRF attack'));
                  return;
                }

                res.writeHead(200, { 'Content-Type': 'text/html' });
                res.end(`
                <html>
                  <body>
                    <h1>Authentication Successful!</h1>
                    <p>You can close this window and return to the terminal.</p>
                    <script>window.close();</script>
                  </body>
                </html>
              `);
                server.close();
                resolve({ code, state: receivedState });
                return;
              }

              res.writeHead(400, { 'Content-Type': 'text/html' });
              res.end('Missing code or state parameter');
            });

            // Listen on the configured host and pre-determined port
            server.listen(serverPort, host, () => {
              log.info(() => ({
                message: 'google oauth server listening',
                port: serverPort,
                host,
              }));
            });

            server.on('error', (err) => {
              log.error(() => ({
                message: 'google oauth server error',
                error: err,
              }));
              reject(err);
            });

            // Timeout after 5 minutes
            setTimeout(
              () => {
                server.close();
                reject(new Error('OAuth timeout'));
              },
              5 * 60 * 1000
            );
          }
        );

        // Build authorization URL with the redirect URI
        const url = new URL(GOOGLE_AUTH_URL);
        url.searchParams.set('client_id', GOOGLE_OAUTH_CLIENT_ID);
        url.searchParams.set('redirect_uri', redirectUri);
        url.searchParams.set('response_type', 'code');
        url.searchParams.set('scope', GOOGLE_OAUTH_SCOPES.join(' '));
        url.searchParams.set('access_type', 'offline');
        url.searchParams.set('code_challenge', pkce.challenge);
        url.searchParams.set('code_challenge_method', 'S256');
        url.searchParams.set('state', state);
        url.searchParams.set('prompt', 'consent');

        return {
          url: url.toString(),
          instructions:
            'Your browser will open for authentication. Complete the login and return to the terminal.',
          method: 'auto' as const,
          async callback(): Promise<AuthResult> {
            try {
              const { code } = await authPromise;

              // Exchange authorization code for tokens
              const tokenResult = await verboseFetch(GOOGLE_TOKEN_URL, {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: new URLSearchParams({
                  code: code,
                  client_id: GOOGLE_OAUTH_CLIENT_ID,
                  client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
                  redirect_uri: redirectUri,
                  grant_type: 'authorization_code',
                  code_verifier: pkce.verifier,
                }),
              });

              if (!tokenResult.ok) {
                log.error(() => ({
                  message: 'google oauth token exchange failed',
                  status: tokenResult.status,
                }));
                return { type: 'failed' };
              }

              const json = await tokenResult.json();
              if (
                !json.access_token ||
                !json.refresh_token ||
                typeof json.expires_in !== 'number'
              ) {
                log.error(() => ({
                  message: 'google oauth token response missing fields',
                }));
                return { type: 'failed' };
              }

              return {
                type: 'success',
                refresh: json.refresh_token,
                access: json.access_token,
                expires: Date.now() + json.expires_in * 1000,
              };
            } catch (error) {
              log.error(() => ({ message: 'google oauth failed', error }));
              return { type: 'failed' };
            }
          },
        };
      },
    },
    {
      label: 'Google AI Pro/Ultra (OAuth - Manual Code Entry)',
      type: 'oauth',
      async authorize() {
        /**
         * Manual code entry flow for headless environments or when NO_BROWSER=true
         * Uses Google's Code Assist redirect URI which displays the auth code to the user
         *
         * Based on Gemini CLI's authWithUserCode function:
         * https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
         */
        const pkce = await generatePKCE();
        const state = generateRandomString(16);
        const redirectUri = GOOGLE_CODEASSIST_REDIRECT_URI;

        log.info(() => ({
          message: 'using manual code entry oauth flow',
          redirectUri,
        }));

        // Build authorization URL with the Code Assist redirect URI
        const url = new URL(GOOGLE_AUTH_URL);
        url.searchParams.set('client_id', GOOGLE_OAUTH_CLIENT_ID);
        url.searchParams.set('redirect_uri', redirectUri);
        url.searchParams.set('response_type', 'code');
        url.searchParams.set('scope', GOOGLE_OAUTH_SCOPES.join(' '));
        url.searchParams.set('access_type', 'offline');
        url.searchParams.set('code_challenge', pkce.challenge);
        url.searchParams.set('code_challenge_method', 'S256');
        url.searchParams.set('state', state);
        url.searchParams.set('prompt', 'consent');

        return {
          url: url.toString(),
          instructions:
            'Visit the URL above, complete authorization, then paste the authorization code here: ',
          method: 'code' as const,
          async callback(code?: string): Promise<AuthResult> {
            if (!code) {
              log.error(() => ({
                message: 'google oauth no code provided',
              }));
              return { type: 'failed' };
            }

            try {
              // Exchange authorization code for tokens
              const tokenResult = await verboseFetch(GOOGLE_TOKEN_URL, {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: new URLSearchParams({
                  code: code.trim(),
                  client_id: GOOGLE_OAUTH_CLIENT_ID,
                  client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
                  redirect_uri: redirectUri,
                  grant_type: 'authorization_code',
                  code_verifier: pkce.verifier,
                }),
              });

              if (!tokenResult.ok) {
                const errorText = await tokenResult.text();
                log.error(() => ({
                  message: 'google oauth token exchange failed',
                  status: tokenResult.status,
                  error: errorText,
                }));
                return { type: 'failed' };
              }

              const json = await tokenResult.json();
              if (
                !json.access_token ||
                !json.refresh_token ||
                typeof json.expires_in !== 'number'
              ) {
                log.error(() => ({
                  message: 'google oauth token response missing fields',
                }));
                return { type: 'failed' };
              }

              return {
                type: 'success',
                refresh: json.refresh_token,
                access: json.access_token,
                expires: Date.now() + json.expires_in * 1000,
              };
            } catch (error) {
              log.error(() => ({
                message: 'google oauth manual code entry failed',
                error,
              }));
              return { type: 'failed' };
            }
          },
        };
      },
    },
    {
      label: 'Manually enter API Key',
      type: 'api',
    },
  ],
  async loader(getAuth, provider) {
    const auth = await getAuth();
    if (!auth || auth.type !== 'oauth') return {};

    // Zero out cost for subscription users
    if (provider?.models) {
      for (const model of Object.values(provider.models)) {
        (model as any).cost = {
          input: 0,
          output: 0,
          cache: {
            read: 0,
            write: 0,
          },
        };
      }
    }

    /**
     * Cloud Code API Configuration
     *
     * The official Gemini CLI uses Google's Cloud Code API (cloudcode-pa.googleapis.com)
     * instead of the standard Generative Language API (generativelanguage.googleapis.com).
     *
     * The Cloud Code API:
     * 1. Accepts `cloud-platform` OAuth scope (unlike generativelanguage.googleapis.com)
     * 2. Handles subscription tier validation (FREE, STANDARD, etc.)
     * 3. Proxies requests to the Generative Language API internally
     *
     * @see https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/server.ts
     * @see https://github.com/link-assistant/agent/issues/100
     * @see https://github.com/link-assistant/agent/issues/102
     */
    const CLOUD_CODE_ENDPOINT =
      process.env['CODE_ASSIST_ENDPOINT'] ||
      'https://cloudcode-pa.googleapis.com';
    const CLOUD_CODE_API_VERSION =
      process.env['CODE_ASSIST_API_VERSION'] || 'v1internal';

    /**
     * Synthetic thought signature used for Gemini 3+ function calls.
     * The Cloud Code API requires function call parts in the active loop
     * to have a thoughtSignature. This bypass value is accepted by the API.
     *
     * @see https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/geminiChat.ts
     */
    const SYNTHETIC_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';

    /**
     * Retry configuration for transient Cloud Code API errors.
     * Matches patterns from Gemini CLI and opencode-gemini-auth plugin.
     */
    const MAX_RETRIES = 2;
    const RETRY_BASE_DELAY_MS = 800;
    const RETRY_MAX_DELAY_MS = 8000;

    /**
     * Cached project context from Cloud Code API onboarding.
     * Persists across requests to avoid repeated loadCodeAssist calls.
     * Keyed by refresh token to invalidate when auth changes.
     */
    let cachedProjectContext: {
      projectId?: string;
      refreshToken: string;
    } | null = null;

    log.debug(() => ({
      message: 'google oauth loader initialized',
      cloudCodeEndpoint: CLOUD_CODE_ENDPOINT,
      apiVersion: CLOUD_CODE_API_VERSION,
    }));

    /**
     * Ensure project context is available for Cloud Code API requests.
     * Calls loadCodeAssist to check user tier and onboards if necessary.
     * Results are cached to avoid repeated API calls.
     *
     * @see https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/setup.ts
     */
    const ensureProjectContext = async (
      accessToken: string,
      refreshToken: string
    ): Promise<string | undefined> => {
      // Check for explicit project ID from environment
      const envProjectId =
        process.env['GOOGLE_CLOUD_PROJECT'] ||
        process.env['GOOGLE_CLOUD_PROJECT_ID'];
      if (envProjectId) {
        return envProjectId;
      }

      // Return cached context if still valid (same refresh token)
      if (
        cachedProjectContext &&
        cachedProjectContext.refreshToken === refreshToken
      ) {
        return cachedProjectContext.projectId;
      }

      // Call loadCodeAssist to discover project and tier
      try {
        const loadUrl = `${CLOUD_CODE_ENDPOINT}/${CLOUD_CODE_API_VERSION}:loadCodeAssist`;
        const loadRes = await verboseFetch(loadUrl, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${accessToken}`,
          },
          body: JSON.stringify({
            metadata: {
              ideType: 'IDE_UNSPECIFIED',
              platform: 'PLATFORM_UNSPECIFIED',
              pluginType: 'GEMINI',
            },
          }),
        });

        if (!loadRes.ok) {
          const errorText = await loadRes.text().catch(() => 'unknown');
          log.warn(() => ({
            message: 'loadCodeAssist failed, proceeding without project',
            status: loadRes.status,
            error: errorText.substring(0, 200),
          }));
          // Cache empty result to avoid retrying on every request
          cachedProjectContext = { refreshToken };
          return undefined;
        }

        const loadData = await loadRes.json();
        log.debug(() => ({
          message: 'loadCodeAssist response',
          hasCurrentTier: !!loadData.currentTier,
          tierId: loadData.currentTier?.id,
          hasProject: !!loadData.cloudaicompanionProject,
        }));

        // If user already has a tier and project, use it
        if (loadData.currentTier && loadData.cloudaicompanionProject) {
          cachedProjectContext = {
            projectId: loadData.cloudaicompanionProject,
            refreshToken,
          };
          log.info(() => ({
            message: 'user already onboarded',
            tier: loadData.currentTier.id,
            projectId: loadData.cloudaicompanionProject,
          }));
          return loadData.cloudaicompanionProject;
        }

        // If user has a tier but no project, and paidTier exists
        if (loadData.currentTier) {
          cachedProjectContext = { refreshToken };
          return undefined;
        }

        // User needs onboarding - find default tier
        let targetTierId = 'free-tier';
        for (const tier of loadData.allowedTiers || []) {
          if (tier.isDefault) {
            targetTierId = tier.id;
            break;
          }
        }

        log.info(() => ({
          message: 'onboarding user to tier',
          tier: targetTierId,
        }));

        // Call onboardUser
        const onboardUrl = `${CLOUD_CODE_ENDPOINT}/${CLOUD_CODE_API_VERSION}:onboardUser`;
        const onboardReq =
          targetTierId === 'free-tier'
            ? {
                tierId: targetTierId,
                metadata: {
                  ideType: 'IDE_UNSPECIFIED',
                  platform: 'PLATFORM_UNSPECIFIED',
                  pluginType: 'GEMINI',
                },
              }
            : {
                tierId: targetTierId,
                cloudaicompanionProject: envProjectId,
                metadata: {
                  ideType: 'IDE_UNSPECIFIED',
                  platform: 'PLATFORM_UNSPECIFIED',
                  pluginType: 'GEMINI',
                  duetProject: envProjectId,
                },
              };

        let lroRes = await verboseFetch(onboardUrl, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${accessToken}`,
          },
          body: JSON.stringify(onboardReq),
        }).then((r) => r.json());

        // Poll until onboarding completes (max 10 attempts)
        let attempts = 0;
        while (!lroRes.done && attempts < 10) {
          await new Promise((resolve) => setTimeout(resolve, 5000));
          if (lroRes.name) {
            // Poll operation status
            const opUrl = `${CLOUD_CODE_ENDPOINT}/${CLOUD_CODE_API_VERSION}/${lroRes.name}`;
            lroRes = await verboseFetch(opUrl, {
              headers: { Authorization: `Bearer ${accessToken}` },
            }).then((r) => r.json());
          } else {
            lroRes = await verboseFetch(onboardUrl, {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${accessToken}`,
              },
              body: JSON.stringify(onboardReq),
            }).then((r) => r.json());
          }
          attempts++;
        }

        const projectId =
          lroRes.response?.cloudaicompanionProject?.id || undefined;
        cachedProjectContext = { projectId, refreshToken };

        log.info(() => ({
          message: 'user onboarding complete',
          projectId,
          attempts,
        }));

        return projectId;
      } catch (error) {
        log.warn(() => ({
          message: 'project context setup failed, proceeding without project',
          error: String(error),
        }));
        cachedProjectContext = { refreshToken };
        return undefined;
      }
    };

    /**
     * Check if we have a fallback API key available.
     * This allows trying API key authentication if Cloud Code API fails.
     * See: https://github.com/link-assistant/agent/issues/100
     */
    const getFallbackApiKey = (): string | undefined => {
      const envKey =
        process.env['GOOGLE_GENERATIVE_AI_API_KEY'] ||
        process.env['GEMINI_API_KEY'];

      if (envKey) {
        log.debug(() => ({
          message: 'fallback api key available',
          source: process.env['GOOGLE_GENERATIVE_AI_API_KEY']
            ? 'GOOGLE_GENERATIVE_AI_API_KEY'
            : 'GEMINI_API_KEY',
          keyLength: envKey.length,
        }));
        return envKey;
      }

      return undefined;
    };

    /**
     * Detect if an error is a scope-related authentication error.
     */
    const isScopeError = (response: Response): boolean => {
      if (response.status !== 403) return false;
      const wwwAuth = response.headers.get('www-authenticate') || '';
      const isScope =
        wwwAuth.includes('insufficient_scope') ||
        wwwAuth.includes('ACCESS_TOKEN_SCOPE_INSUFFICIENT');

      if (isScope) {
        log.debug(() => ({
          message: 'detected oauth scope error',
          status: response.status,
          wwwAuthenticate: wwwAuth.substring(0, 200),
        }));
      }

      return isScope;
    };

    /**
     * Check if a response status is retryable (transient error).
     * Includes 500/502 for intermittent server errors (#231).
     */
    const isRetryableStatus = (status: number): boolean => {
      return (
        status === 429 || status === 500 || status === 502 || status === 503
      );
    };

    /**
     * Extract retry delay from response headers or error body.
     */
    const getRetryDelay = (response: Response, attempt: number): number => {
      // Check Retry-After header
      const retryAfter = response.headers.get('retry-after');
      if (retryAfter) {
        const seconds = parseInt(retryAfter, 10);
        if (!isNaN(seconds)) return seconds * 1000;
      }

      // Check retry-after-ms header
      const retryAfterMs = response.headers.get('retry-after-ms');
      if (retryAfterMs) {
        const ms = parseInt(retryAfterMs, 10);
        if (!isNaN(ms)) return ms;
      }

      // Exponential backoff
      const delay = Math.min(
        RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
        RETRY_MAX_DELAY_MS
      );
      return delay;
    };

    /**
     * Transform a Generative Language API URL to Cloud Code API URL
     *
     * Input:  https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent
     * Output: https://cloudcode-pa.googleapis.com/v1internal:generateContent
     */
    const transformToCloudCodeUrl = (url: string): string | null => {
      try {
        const parsed = new URL(url);
        if (!parsed.hostname.includes('generativelanguage.googleapis.com')) {
          log.debug(() => ({
            message:
              'url is not generativelanguage api, skipping cloud code transform',
            hostname: parsed.hostname,
          }));
          return null;
        }

        const pathMatch = parsed.pathname.match(/:(\w+)$/);
        if (!pathMatch) {
          log.debug(() => ({
            message: 'could not extract method from url path',
            pathname: parsed.pathname,
          }));
          return null;
        }

        const method = pathMatch[1];
        const cloudCodeUrl = `${CLOUD_CODE_ENDPOINT}/${CLOUD_CODE_API_VERSION}:${method}`;

        log.debug(() => ({
          message: 'transformed url to cloud code api',
          originalUrl: url.substring(0, 100),
          method,
          cloudCodeUrl,
        }));

        return cloudCodeUrl;
      } catch (error) {
        log.debug(() => ({
          message: 'failed to parse url for cloud code transform',
          url: url.substring(0, 100),
          error: String(error),
        }));
        return null;
      }
    };

    /**
     * Check if a URL is for a streaming request.
     * The AI SDK appends alt=sse for streaming or uses streamGenerateContent method.
     */
    const isStreamingRequest = (url: string): boolean => {
      return url.includes('streamGenerateContent') || url.includes('alt=sse');
    };

    /**
     * Extract model name from Generative Language API URL
     *
     * Input:  https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent
     * Output: gemini-2.0-flash
     */
    const extractModelFromUrl = (url: string): string | null => {
      try {
        const parsed = new URL(url);
        const pathMatch = parsed.pathname.match(/\/models\/([^:]+):/);
        const model = pathMatch ? pathMatch[1] : null;

        log.debug(() => ({
          message: 'extracted model from url',
          pathname: parsed.pathname,
          model,
        }));

        return model;
      } catch (error) {
        log.debug(() => ({
          message: 'failed to extract model from url',
          url: url.substring(0, 100),
          error: String(error),
        }));
        return null;
      }
    };

    /**
     * Inject thoughtSignature into function call parts for Gemini 3+ models.
     *
     * The Cloud Code API requires function call parts in model turns within
     * the active loop to have a `thoughtSignature` property. Without this,
     * requests with function calls will fail with 400 errors.
     *
     * @see https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/geminiChat.ts
     * @see https://github.com/sst/opencode/issues/4832
     */
    const injectThoughtSignatures = (request: any): any => {
      if (!request?.contents || !Array.isArray(request.contents)) {
        return request;
      }

      // Find the start of the active loop (last user turn with text)
      let activeLoopStartIndex = -1;
      for (let i = request.contents.length - 1; i >= 0; i--) {
        const content = request.contents[i];
        if (
          content.role === 'user' &&
          content.parts?.some((p: any) => p.text)
        ) {
          activeLoopStartIndex = i;
          break;
        }
      }

      if (activeLoopStartIndex === -1) {
        return request;
      }

      // Inject thoughtSignature into the first functionCall in each model turn
      const newContents = [...request.contents];
      let modified = false;
      for (let i = activeLoopStartIndex; i < newContents.length; i++) {
        const content = newContents[i];
        if (content.role === 'model' && content.parts) {
          const newParts = [...content.parts];
          for (let j = 0; j < newParts.length; j++) {
            const part = newParts[j];
            if (part.functionCall && !part.thoughtSignature) {
              newParts[j] = {
                ...part,
                thoughtSignature: SYNTHETIC_THOUGHT_SIGNATURE,
              };
              newContents[i] = { ...content, parts: newParts };
              modified = true;
              break; // Only the first functionCall in each turn
            }
          }
        }
      }

      if (modified) {
        log.debug(() => ({
          message: 'injected thoughtSignature into function call parts',
        }));
      }

      return { ...request, contents: newContents };
    };

    /**
     * Transform request body for Cloud Code API
     *
     * The Cloud Code API expects requests in this format:
     * {
     *   model: "gemini-2.0-flash",
     *   project: "optional-project-id",
     *   request: { contents: [...], generationConfig: {...}, ... }
     * }
     */
    const transformRequestBody = (
      body: string,
      model: string,
      projectId?: string
    ): string => {
      try {
        let parsed = JSON.parse(body);

        // Inject thoughtSignature for function calls
        parsed = injectThoughtSignatures(parsed);

        // Wrap in Cloud Code API format
        const cloudCodeRequest: Record<string, unknown> = {
          model,
          request: parsed,
        };

        if (projectId) {
          cloudCodeRequest.project = projectId;
        }

        log.debug(() => ({
          message: 'transformed request body for cloud code api',
          model,
          hasProjectId: !!projectId,
          originalBodyLength: body.length,
          transformedBodyLength: JSON.stringify(cloudCodeRequest).length,
        }));

        return JSON.stringify(cloudCodeRequest);
      } catch (error) {
        log.debug(() => ({
          message: 'failed to transform request body, using original',
          error: String(error),
        }));
        return body;
      }
    };

    /**
     * Transform Cloud Code API response to standard format
     *
     * Cloud Code API returns:
     * { response: { candidates: [...], ... }, traceId: "..." }
     *
     * Standard API returns:
     * { candidates: [...], ... }
     */
    const transformResponseBody = async (
      response: Response
    ): Promise<Response> => {
      const contentType = response.headers.get('content-type');
      const isStreaming = contentType?.includes('text/event-stream');

      log.debug(() => ({
        message: 'transforming cloud code response',
        status: response.status,
        contentType,
        isStreaming,
      }));

      if (isStreaming) {
        const reader = response.body?.getReader();
        if (!reader) {
          log.debug(() => ({
            message: 'no response body reader available for streaming',
          }));
          return response;
        }

        const encoder = new TextEncoder();
        const decoder = new TextDecoder();
        let chunkCount = 0;

        const transformedStream = new ReadableStream({
          async start(controller) {
            try {
              while (true) {
                const { done, value } = await reader.read();
                if (done) {
                  log.debug(() => ({
                    message: 'streaming response complete',
                    totalChunks: chunkCount,
                  }));
                  controller.close();
                  break;
                }

                const text = decoder.decode(value, { stream: true });
                chunkCount++;

                const events = text.split('\n\n');
                for (const event of events) {
                  if (!event.trim()) continue;

                  if (event.startsWith('data: ')) {
                    try {
                      const jsonStr = event.slice(6).trim();
                      if (jsonStr === '[DONE]') {
                        controller.enqueue(encoder.encode(event + '\n\n'));
                        continue;
                      }

                      const parsed = JSON.parse(jsonStr);
                      const unwrapped = parsed.response || parsed;
                      controller.enqueue(
                        encoder.encode(
                          'data: ' + JSON.stringify(unwrapped) + '\n\n'
                        )
                      );
                    } catch {
                      controller.enqueue(encoder.encode(event + '\n\n'));
                    }
                  } else {
                    controller.enqueue(encoder.encode(event + '\n\n'));
                  }
                }
              }
            } catch (error) {
              log.debug(() => ({
                message: 'error during streaming response transformation',
                error: String(error),
              }));
              controller.error(error);
            }
          },
        });

        return new Response(transformedStream, {
          status: response.status,
          statusText: response.statusText,
          headers: response.headers,
        });
      }

      // For non-streaming responses, parse and unwrap
      try {
        const json = await response.json();
        const unwrapped = json.response || json;

        log.debug(() => ({
          message: 'unwrapped non-streaming cloud code response',
          hasResponseWrapper: !!json.response,
          hasTraceId: !!json.traceId,
        }));

        return new Response(JSON.stringify(unwrapped), {
          status: response.status,
          statusText: response.statusText,
          headers: response.headers,
        });
      } catch (error) {
        log.debug(() => ({
          message: 'failed to parse non-streaming response, returning original',
          error: String(error),
        }));
        return response;
      }
    };

    return {
      apiKey: 'oauth-token-used-via-custom-fetch',
      async fetch(input: RequestInfo | URL, init?: RequestInit) {
        let currentAuth = await getAuth();
        if (!currentAuth || currentAuth.type !== 'oauth') {
          log.debug(() => ({
            message: 'no google oauth credentials, using standard fetch',
          }));
          return fetch(input, init);
        }

        log.debug(() => ({
          message: 'google oauth fetch initiated',
          hasAccessToken: !!currentAuth?.access,
          tokenExpiresIn: currentAuth
            ? Math.round((currentAuth.expires - Date.now()) / 1000)
            : 0,
        }));

        // Refresh token if expired (with 5 minute buffer)
        const FIVE_MIN_MS = 5 * 60 * 1000;
        if (
          !currentAuth.access ||
          currentAuth.expires < Date.now() + FIVE_MIN_MS
        ) {
          log.info(() => ({
            message: 'refreshing google oauth token',
            reason: !currentAuth.access
              ? 'no access token'
              : 'token expiring soon',
          }));

          // Invalidate project cache when token changes
          cachedProjectContext = null;

          const response = await verboseFetch(GOOGLE_TOKEN_URL, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: new URLSearchParams({
              client_id: GOOGLE_OAUTH_CLIENT_ID,
              client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
              refresh_token: currentAuth.refresh,
              grant_type: 'refresh_token',
            }),
          });

          if (!response.ok) {
            const errorText = await response.text().catch(() => 'unknown');
            log.error(() => ({
              message: 'google oauth token refresh failed',
              status: response.status,
              error: errorText.substring(0, 200),
            }));
            throw new Error(`Token refresh failed: ${response.status}`);
          }

          const json = await response.json();
          log.debug(() => ({
            message: 'google oauth token refreshed successfully',
            expiresIn: json.expires_in,
          }));

          await Auth.set('google', {
            type: 'oauth',
            // Google doesn't return a new refresh token on refresh
            refresh: currentAuth.refresh,
            access: json.access_token,
            expires: Date.now() + json.expires_in * 1000,
          });
          currentAuth = {
            type: 'oauth',
            refresh: currentAuth.refresh,
            access: json.access_token,
            expires: Date.now() + json.expires_in * 1000,
          };
        }

        // Get the original URL
        const originalUrl =
          typeof input === 'string'
            ? input
            : input instanceof URL
              ? input.toString()
              : (input as Request).url;

        log.debug(() => ({
          message: 'processing google api request',
          originalUrl: originalUrl.substring(0, 100),
          method: init?.method || 'GET',
        }));

        // Try to transform to Cloud Code API URL
        const cloudCodeUrl = transformToCloudCodeUrl(originalUrl);
        const model = extractModelFromUrl(originalUrl);

        // If this is a Generative Language API request, route through Cloud Code API
        if (cloudCodeUrl && model) {
          // Ensure project context is available (onboard if needed)
          const projectId = await ensureProjectContext(
            currentAuth.access,
            currentAuth.refresh
          );

          log.info(() => ({
            message: 'routing google oauth request through cloud code api',
            originalUrl: originalUrl.substring(0, 100) + '...',
            cloudCodeUrl,
            model,
            projectId: projectId || '(none)',
          }));

          // Transform request body to Cloud Code format
          let body = init?.body;
          if (typeof body === 'string') {
            body = transformRequestBody(body, model, projectId);
          }

          // Build Cloud Code API URL with alt=sse for streaming
          let finalCloudCodeUrl = cloudCodeUrl;
          if (isStreamingRequest(originalUrl)) {
            const separator = finalCloudCodeUrl.includes('?') ? '&' : '?';
            finalCloudCodeUrl = `${finalCloudCodeUrl}${separator}alt=sse`;
          }

          // Make request to Cloud Code API with Bearer token and retry logic
          const headers: Record<string, string> = {
            ...(init?.headers as Record<string, string>),
            Authorization: `Bearer ${currentAuth.access}`,
            'x-goog-api-client': `agent/${process.env['npm_package_version'] || '0.7.0'}`,
          };
          delete headers['x-goog-api-key'];

          log.debug(() => ({
            message: 'sending request to cloud code api',
            url: finalCloudCodeUrl,
            hasBody: !!body,
          }));

          // Retry loop for transient errors
          let lastResponse: Response | null = null;
          for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
            if (attempt > 0) {
              const delay = getRetryDelay(lastResponse!, attempt - 1);
              log.info(() => ({
                message: 'retrying cloud code api request',
                attempt,
                delayMs: delay,
                previousStatus: lastResponse?.status,
              }));
              await new Promise((resolve) => setTimeout(resolve, delay));
            }

            const cloudCodeResponse = await verboseFetch(finalCloudCodeUrl, {
              ...init,
              body,
              headers,
            });

            log.debug(() => ({
              message: 'cloud code api response received',
              status: cloudCodeResponse.status,
              statusText: cloudCodeResponse.statusText,
              contentType: cloudCodeResponse.headers.get('content-type'),
              attempt,
            }));

            // Success - transform and return
            if (cloudCodeResponse.ok) {
              log.debug(() => ({
                message:
                  'cloud code api request successful, transforming response',
              }));
              return transformResponseBody(cloudCodeResponse);
            }

            // Retryable error
            if (
              isRetryableStatus(cloudCodeResponse.status) &&
              attempt < MAX_RETRIES
            ) {
              lastResponse = cloudCodeResponse;
              continue;
            }

            // Non-retryable error or max retries reached
            const errorBody = await cloudCodeResponse
              .clone()
              .text()
              .catch(() => 'unknown');
            log.warn(() => ({
              message: 'cloud code api returned error',
              status: cloudCodeResponse.status,
              statusText: cloudCodeResponse.statusText,
              errorBody: errorBody.substring(0, 500),
              attempt,
            }));

            const fallbackApiKey = getFallbackApiKey();
            if (fallbackApiKey) {
              log.warn(() => ({
                message:
                  'cloud code api error, falling back to api key with standard api',
                status: cloudCodeResponse.status,
                fallbackTarget: originalUrl.substring(0, 100),
              }));

              const apiKeyHeaders: Record<string, string> = {
                ...(init?.headers as Record<string, string>),
                'x-goog-api-key': fallbackApiKey,
              };
              delete apiKeyHeaders['Authorization'];

              return fetch(originalUrl, {
                ...init,
                headers: apiKeyHeaders,
              });
            }

            log.error(() => ({
              message: 'cloud code api error and no api key fallback available',
              status: cloudCodeResponse.status,
              hint: 'Set GOOGLE_GENERATIVE_AI_API_KEY or GEMINI_API_KEY environment variable for fallback',
            }));

            return cloudCodeResponse;
          }

          // Should not reach here, but return last response as safety net
          return lastResponse!;
        }

        // Not a Generative Language API request, use standard OAuth flow
        log.debug(() => ({
          message:
            'not a generative language api request, using standard oauth',
          url: originalUrl.substring(0, 100),
        }));

        const headers: Record<string, string> = {
          ...(init?.headers as Record<string, string>),
          Authorization: `Bearer ${currentAuth.access}`,
        };
        delete headers['x-goog-api-key'];

        const oauthResponse = await verboseFetch(input, {
          ...init,
          headers,
        });

        log.debug(() => ({
          message: 'standard oauth response received',
          status: oauthResponse.status,
        }));

        // Check if OAuth failed due to insufficient scopes
        if (isScopeError(oauthResponse)) {
          const fallbackApiKey = getFallbackApiKey();
          if (fallbackApiKey) {
            log.warn(() => ({
              message:
                'oauth scope error, falling back to api key authentication',
              hint: 'This should not happen with Cloud Code API routing',
              url: originalUrl.substring(0, 100),
            }));

            const apiKeyHeaders: Record<string, string> = {
              ...(init?.headers as Record<string, string>),
              'x-goog-api-key': fallbackApiKey,
            };
            delete apiKeyHeaders['Authorization'];

            return fetch(input, {
              ...init,
              headers: apiKeyHeaders,
            });
          } else {
            log.error(() => ({
              message: 'oauth scope error and no api key fallback available',
              hint: 'Set GOOGLE_GENERATIVE_AI_API_KEY or GEMINI_API_KEY environment variable',
              url: originalUrl.substring(0, 100),
            }));
          }
        }

        return oauthResponse;
      },
    };
  },
};

/**
 * Qwen OAuth Configuration
 * Used for Qwen Coder subscription authentication via chat.qwen.ai
 *
 * Based on the official Qwen Code CLI (QwenLM/qwen-code)
 * and qwen-auth-opencode reference implementation:
 * https://github.com/QwenLM/Qwen3-Coder
 * https://github.com/lion-lef/qwen-auth-opencode
 */
const QWEN_OAUTH_CLIENT_ID = 'f0304373b74a44d2b584a3fb70ca9e56';
const QWEN_OAUTH_SCOPE = 'openid profile email model.completion';
const QWEN_OAUTH_DEVICE_CODE_ENDPOINT =
  'https://chat.qwen.ai/api/v1/oauth2/device/code';
const QWEN_OAUTH_TOKEN_ENDPOINT = 'https://chat.qwen.ai/api/v1/oauth2/token';
const QWEN_OAUTH_DEFAULT_API_URL = 'https://portal.qwen.ai/v1';

/**
 * Detect if running in a headless environment (no GUI)
 */
function isHeadlessEnvironment(): boolean {
  // Check common headless indicators
  if (!process.stdout.isTTY) return true;
  if (process.env.SSH_CLIENT || process.env.SSH_TTY) return true;
  if (process.env.CI) return true;
  if (!process.env.DISPLAY && process.platform === 'linux') return true;
  return false;
}

/**
 * Open URL in the default browser
 */
function openBrowser(url: string): void {
  const platform = process.platform;
  let command: string;

  if (platform === 'darwin') {
    command = 'open';
  } else if (platform === 'win32') {
    command = 'start';
  } else {
    command = 'xdg-open';
  }

  Bun.spawn([command, url], { stdout: 'ignore', stderr: 'ignore' });
}

/**
 * Qwen OAuth Plugin
 * Supports Qwen Coder subscription via OAuth device flow.
 *
 * Uses OAuth 2.0 Device Authorization Grant (RFC 8628) with PKCE (RFC 7636),
 * matching the official Qwen Code CLI implementation.
 *
 * @see https://github.com/QwenLM/Qwen3-Coder
 */
const QwenPlugin: AuthPlugin = {
  provider: 'qwen-coder',
  methods: [
    {
      label: 'Qwen Coder Subscription (OAuth)',
      type: 'oauth',
      async authorize() {
        // Generate PKCE pair
        const codeVerifier = generateRandomString(32);
        const codeChallenge = generateCodeChallenge(codeVerifier);

        // Request device code
        const deviceResponse = await verboseFetch(
          QWEN_OAUTH_DEVICE_CODE_ENDPOINT,
          {
            method: 'POST',
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded',
              Accept: 'application/json',
            },
            body: new URLSearchParams({
              client_id: QWEN_OAUTH_CLIENT_ID,
              scope: QWEN_OAUTH_SCOPE,
              code_challenge: codeChallenge,
              code_challenge_method: 'S256',
            }).toString(),
          }
        );

        if (!deviceResponse.ok) {
          const errorText = await deviceResponse.text();
          log.error(() => ({
            message: 'qwen oauth device code request failed',
            status: deviceResponse.status,
            error: errorText,
          }));
          throw new Error(
            `Device authorization failed: ${deviceResponse.status}`
          );
        }

        const deviceData = (await deviceResponse.json()) as {
          device_code: string;
          user_code: string;
          verification_uri: string;
          verification_uri_complete: string;
          expires_in: number;
          interval?: number;
        };

        const pollInterval = (deviceData.interval || 2) * 1000;
        const maxPollAttempts = Math.ceil(
          deviceData.expires_in / (pollInterval / 1000)
        );

        // Try to open browser in non-headless environments
        if (!isHeadlessEnvironment()) {
          try {
            openBrowser(deviceData.verification_uri_complete);
          } catch {
            // Ignore browser open errors
          }
        }

        const instructions = isHeadlessEnvironment()
          ? `Visit: ${deviceData.verification_uri}\nEnter code: ${deviceData.user_code}`
          : `Opening browser for authentication...\nIf browser doesn't open, visit: ${deviceData.verification_uri}\nEnter code: ${deviceData.user_code}`;

        return {
          url: deviceData.verification_uri_complete,
          instructions,
          method: 'auto' as const,
          async callback(): Promise<AuthResult> {
            // Poll for authorization completion
            for (let attempt = 0; attempt < maxPollAttempts; attempt++) {
              const tokenResponse = await verboseFetch(
                QWEN_OAUTH_TOKEN_ENDPOINT,
                {
                  method: 'POST',
                  headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    Accept: 'application/json',
                  },
                  body: new URLSearchParams({
                    client_id: QWEN_OAUTH_CLIENT_ID,
                    device_code: deviceData.device_code,
                    grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
                    code_verifier: codeVerifier,
                  }).toString(),
                }
              );

              if (!tokenResponse.ok) {
                const errorText = await tokenResponse.text();
                try {
                  const errorJson = JSON.parse(errorText);
                  if (
                    errorJson.error === 'authorization_pending' ||
                    errorJson.error === 'slow_down'
                  ) {
                    await new Promise((resolve) =>
                      setTimeout(
                        resolve,
                        errorJson.error === 'slow_down'
                          ? pollInterval * 1.5
                          : pollInterval
                      )
                    );
                    continue;
                  }
                } catch {
                  // JSON parse failed, treat as regular error
                }

                log.error(() => ({
                  message: 'qwen oauth token poll failed',
                  status: tokenResponse.status,
                  error: errorText,
                }));
                return { type: 'failed' };
              }

              const tokenData = (await tokenResponse.json()) as {
                access_token: string;
                refresh_token?: string;
                token_type: string;
                expires_in: number;
                resource_url?: string;
              };

              return {
                type: 'success',
                refresh: tokenData.refresh_token || '',
                access: tokenData.access_token,
                expires: Date.now() + tokenData.expires_in * 1000,
              };
            }

            log.error(() => ({
              message: 'qwen oauth authorization timeout',
            }));
            return { type: 'failed' };
          },
        };
      },
    },
  ],
  async loader(getAuth, provider) {
    const auth = await getAuth();
    if (!auth || auth.type !== 'oauth') return {};

    // Zero out cost for subscription users (free tier)
    if (provider?.models) {
      for (const model of Object.values(provider.models)) {
        (model as any).cost = {
          input: 0,
          output: 0,
          cache: {
            read: 0,
            write: 0,
          },
        };
      }
    }

    return {
      apiKey: 'oauth-token-used-via-custom-fetch',
      baseURL: QWEN_OAUTH_DEFAULT_API_URL,
      async fetch(input: RequestInfo | URL, init?: RequestInit) {
        let currentAuth = await getAuth();
        if (!currentAuth || currentAuth.type !== 'oauth')
          return fetch(input, init);

        // Refresh token if expired (with 5 minute buffer)
        const FIVE_MIN_MS = 5 * 60 * 1000;
        if (
          !currentAuth.access ||
          currentAuth.expires < Date.now() + FIVE_MIN_MS
        ) {
          if (!currentAuth.refresh) {
            log.error(() => ({
              message:
                'qwen oauth token expired and no refresh token available',
            }));
            throw new Error(
              'Qwen OAuth token expired. Please re-authenticate with: agent auth login'
            );
          }

          log.info(() => ({
            message: 'refreshing qwen oauth token',
            reason: !currentAuth.access
              ? 'no access token'
              : 'token expiring soon',
          }));

          const response = await verboseFetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded',
              Accept: 'application/json',
            },
            body: new URLSearchParams({
              grant_type: 'refresh_token',
              refresh_token: currentAuth.refresh,
              client_id: QWEN_OAUTH_CLIENT_ID,
            }),
          });

          if (!response.ok) {
            const errorText = await response.text().catch(() => 'unknown');
            log.error(() => ({
              message: 'qwen oauth token refresh failed',
              status: response.status,
              error: errorText.substring(0, 200),
            }));
            throw new Error(
              `Qwen token refresh failed: ${response.status}. Please re-authenticate with: agent auth login`
            );
          }

          const json = await response.json();
          log.info(() => ({
            message: 'qwen oauth token refreshed successfully',
            expiresIn: json.expires_in,
          }));

          await Auth.set('qwen-coder', {
            type: 'oauth',
            refresh: json.refresh_token || currentAuth.refresh,
            access: json.access_token,
            expires: Date.now() + json.expires_in * 1000,
          });
          currentAuth = {
            type: 'oauth',
            refresh: json.refresh_token || currentAuth.refresh,
            access: json.access_token,
            expires: Date.now() + json.expires_in * 1000,
          };
        }

        const headers: Record<string, string> = {
          ...(init?.headers as Record<string, string>),
          Authorization: `Bearer ${currentAuth.access}`,
        };
        delete headers['x-api-key'];

        return fetch(input, {
          ...init,
          headers,
        });
      },
    };
  },
};

/**
 * Alibaba Plugin (alias for Qwen Coder)
 * This provides a separate menu entry for Alibaba
 * with the same Qwen Coder subscription authentication.
 */
const AlibabaPlugin: AuthPlugin = {
  provider: 'alibaba',
  methods: [
    {
      label: 'Qwen Coder Subscription (OAuth)',
      type: 'oauth',
      async authorize() {
        // Delegate to QwenPlugin's OAuth method
        const qwenMethod = QwenPlugin.methods[0];
        if (qwenMethod?.authorize) {
          const result = await qwenMethod.authorize({});
          // Override the callback to save as alibaba provider
          if ('callback' in result) {
            const originalCallback = result.callback;
            return {
              ...result,
              async callback(code?: string): Promise<AuthResult> {
                const authResult = await originalCallback(code);
                if (authResult.type === 'success' && 'refresh' in authResult) {
                  return {
                    ...authResult,
                    provider: 'alibaba',
                  };
                }
                return authResult;
              },
            };
          }
        }
        return {
          method: 'auto' as const,
          async callback(): Promise<AuthResult> {
            return { type: 'failed' };
          },
        };
      },
    },
  ],
  async loader(getAuth, provider) {
    const auth = await getAuth();
    if (!auth || auth.type !== 'oauth') return {};

    // Zero out cost for subscription users (free tier)
    if (provider?.models) {
      for (const model of Object.values(provider.models)) {
        (model as any).cost = {
          input: 0,
          output: 0,
          cache: {
            read: 0,
            write: 0,
          },
        };
      }
    }

    return {
      apiKey: 'oauth-token-used-via-custom-fetch',
      baseURL: QWEN_OAUTH_DEFAULT_API_URL,
      async fetch(input: RequestInfo | URL, init?: RequestInit) {
        let currentAuth = await getAuth();
        if (!currentAuth || currentAuth.type !== 'oauth')
          return fetch(input, init);

        // Refresh token if expired (with 5 minute buffer)
        const FIVE_MIN_MS = 5 * 60 * 1000;
        if (
          !currentAuth.access ||
          currentAuth.expires < Date.now() + FIVE_MIN_MS
        ) {
          if (!currentAuth.refresh) {
            log.error(() => ({
              message:
                'qwen oauth token expired and no refresh token available (alibaba)',
            }));
            throw new Error(
              'Qwen OAuth token expired. Please re-authenticate with: agent auth login'
            );
          }

          log.info(() => ({
            message: 'refreshing qwen oauth token (alibaba provider)',
          }));

          const response = await verboseFetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded',
              Accept: 'application/json',
            },
            body: new URLSearchParams({
              grant_type: 'refresh_token',
              refresh_token: currentAuth.refresh,
              client_id: QWEN_OAUTH_CLIENT_ID,
            }),
          });

          if (!response.ok) {
            const errorText = await response.text().catch(() => 'unknown');
            log.error(() => ({
              message: 'qwen oauth token refresh failed (alibaba)',
              status: response.status,
              error: errorText.substring(0, 200),
            }));
            throw new Error(
              `Qwen token refresh failed: ${response.status}. Please re-authenticate with: agent auth login`
            );
          }

          const json = await response.json();
          await Auth.set('alibaba', {
            type: 'oauth',
            refresh: json.refresh_token || currentAuth.refresh,
            access: json.access_token,
            expires: Date.now() + json.expires_in * 1000,
          });
          currentAuth = {
            type: 'oauth',
            refresh: json.refresh_token || currentAuth.refresh,
            access: json.access_token,
            expires: Date.now() + json.expires_in * 1000,
          };
        }

        const headers: Record<string, string> = {
          ...(init?.headers as Record<string, string>),
          Authorization: `Bearer ${currentAuth.access}`,
        };
        delete headers['x-api-key'];

        return fetch(input, {
          ...init,
          headers,
        });
      },
    };
  },
};

/**
 * Kilo Gateway constants
 * @see https://github.com/Kilo-Org/kilo/blob/main/packages/kilo-gateway/src/api/constants.ts
 */
const KILO_API_BASE = 'https://api.kilo.ai';
const KILO_POLL_INTERVAL_MS = 3000;

/**
 * Kilo Gateway Auth Plugin
 * Supports device authorization flow for Kilo Gateway
 *
 * @see https://github.com/Kilo-Org/kilo/blob/main/packages/kilo-gateway/src/auth/device-auth.ts
 */
const KiloPlugin: AuthPlugin = {
  provider: 'kilo',
  methods: [
    {
      label: 'Kilo Gateway (Device Authorization)',
      type: 'oauth',
      async authorize() {
        // Initiate device authorization
        const initResponse = await verboseFetch(
          `${KILO_API_BASE}/api/device-auth/codes`,
          {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
          }
        );

        if (!initResponse.ok) {
          if (initResponse.status === 429) {
            log.error(() => ({
              message:
                'kilo device auth rate limited - too many pending requests',
            }));
            return {
              method: 'auto' as const,
              async callback(): Promise<AuthResult> {
                return { type: 'failed' };
              },
            };
          }
          log.error(() => ({
            message: 'kilo device auth initiation failed',
            status: initResponse.status,
          }));
          return {
            method: 'auto' as const,
            async callback(): Promise<AuthResult> {
              return { type: 'failed' };
            },
          };
        }

        const authData = (await initResponse.json()) as {
          code: string;
          verificationUrl: string;
          expiresIn: number;
        };

        return {
          url: authData.verificationUrl,
          instructions: `Enter code: ${authData.code}\nWaiting for authorization...`,
          method: 'auto' as const,
          async callback(): Promise<AuthResult> {
            const maxAttempts = Math.ceil(
              (authData.expiresIn * 1000) / KILO_POLL_INTERVAL_MS
            );

            for (let attempt = 0; attempt < maxAttempts; attempt++) {
              await new Promise((resolve) =>
                setTimeout(resolve, KILO_POLL_INTERVAL_MS)
              );

              const pollResponse = await verboseFetch(
                `${KILO_API_BASE}/api/device-auth/codes/${authData.code}`
              );

              if (pollResponse.status === 202) {
                // Still pending
                continue;
              }

              if (pollResponse.status === 403) {
                log.error(() => ({
                  message: 'kilo device auth denied by user',
                }));
                return { type: 'failed' };
              }

              if (pollResponse.status === 410) {
                log.error(() => ({
                  message: 'kilo device auth code expired',
                }));
                return { type: 'failed' };
              }

              if (!pollResponse.ok) {
                log.error(() => ({
                  message: 'kilo device auth poll failed',
                  status: pollResponse.status,
                }));
                return { type: 'failed' };
              }

              const data = (await pollResponse.json()) as {
                status: string;
                token?: string;
                userEmail?: string;
              };

              if (data.status === 'approved' && data.token) {
                log.info(() => ({
                  message: 'kilo device auth approved',
                  email: data.userEmail,
                }));

                // Token from Kilo device auth is long-lived (1 year)
                const TOKEN_EXPIRATION_MS = 365 * 24 * 60 * 60 * 1000;
                return {
                  type: 'success',
                  provider: 'kilo',
                  refresh: data.token,
                  access: data.token,
                  expires: Date.now() + TOKEN_EXPIRATION_MS,
                };
              }
            }

            log.error(() => ({
              message: 'kilo device auth timed out',
            }));
            return { type: 'failed' };
          },
        };
      },
    },
    {
      label: 'API Key',
      type: 'api',
      async authorize(inputs: Record<string, string>) {
        const key = inputs['key'];
        if (!key) return { type: 'failed' };
        return {
          type: 'success',
          provider: 'kilo',
          key,
        };
      },
    },
  ],
  async loader(getAuth) {
    const auth = await getAuth();
    if (!auth) return {};

    if (auth.type === 'api') {
      return { apiKey: auth.key };
    }

    if (auth.type === 'oauth') {
      return { apiKey: auth.access };
    }

    return {};
  },
};

/**
 * Registry of all auth plugins
 */
const plugins: Record<string, AuthPlugin> = {
  anthropic: AnthropicPlugin,
  'github-copilot': GitHubCopilotPlugin,
  openai: OpenAIPlugin,
  google: GooglePlugin,
  'qwen-coder': QwenPlugin,
  alibaba: AlibabaPlugin,
  kilo: KiloPlugin,
};

/**
 * Auth Plugins namespace
 */
export namespace AuthPlugins {
  /**
   * Get a plugin by provider ID
   */
  export function getPlugin(providerId: string): AuthPlugin | undefined {
    return plugins[providerId];
  }

  /**
   * Get all plugins
   */
  export function getAllPlugins(): AuthPlugin[] {
    return Object.values(plugins);
  }

  /**
   * Get the loader for a provider
   */
  export async function getLoader(providerId: string) {
    const plugin = plugins[providerId];
    if (!plugin?.loader) return undefined;

    return async (
      getAuth: () => Promise<Auth.Info | undefined>,
      provider: any
    ) => {
      return plugin.loader!(getAuth, provider);
    };
  }
}
