import Anthropic, { type ClientOptions } from '@sandcode/sdk'
import { randomUUID } from 'crypto'
import type { GoogleAuth } from 'google-auth-library'
import {
  checkAndRefreshOAuthTokenIfNeeded,
  getAnthropicApiKey,
  getApiKeyFromApiKeyHelper,
  getClaudeAIOAuthTokens,
  isClaudeAISubscriber,
  refreshAndGetAwsCredentials,
  refreshGcpCredentialsIfNeeded,
} from 'src/utils/auth.js'
import { getUserAgent } from 'src/utils/http.js'
import { getSmallFastModel } from 'src/utils/model/model.js'
import {
  getAPIProvider,
  getCustomProviderConfig,
  isFirstPartyAnthropicBaseUrl,
} from 'src/utils/model/providers.js'
import { getGlobalConfig } from 'src/utils/config.js'
import { getProxyFetchOptions } from 'src/utils/proxy.js'
import { getIsNonInteractiveSession, getSessionId } from '../../bootstrap/state.js'
import { getOauthConfig } from '../../constants/oauth.js'
import { isDebugToStdErr, logForDebugging } from '../../utils/debug.js'
import { createOpenAICompatClient, type OpenAICompatProviderConfig } from './openaiCompatAdapter.js'
import { getAWSRegion, getVertexRegionForModel, isEnvTruthy } from '../../utils/envUtils.js'

/**
 * Environment variables for different client types:
 *
 * Direct API:
 * - ANTHROPIC_API_KEY: Required for direct API access
 *
 * AWS Bedrock:
 * - AWS credentials configured via aws-sdk defaults
 * - AWS_REGION or AWS_DEFAULT_REGION: Sets the AWS region for all models (default: us-east-1)
 * - ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION: Optional. Override AWS region specifically for the small fast model (Haiku)
 *
 * Foundry (Azure):
 * - ANTHROPIC_FOUNDRY_RESOURCE: Your Azure resource name (e.g., 'my-resource')
 *   For the full endpoint: https://{resource}.services.ai.azure.com/anthropic/v1/messages
 * - ANTHROPIC_FOUNDRY_BASE_URL: Optional. Alternative to resource - provide full base URL directly
 *   (e.g., 'https://my-resource.services.ai.azure.com')
 *
 * Authentication (one of the following):
 * - ANTHROPIC_FOUNDRY_API_KEY: Your Microsoft Foundry API key (if using API key auth)
 * - Azure AD authentication: If no API key is provided, uses DefaultAzureCredential
 *   which supports multiple auth methods (environment variables, managed identity,
 *   Azure CLI, etc.). See: https://docs.microsoft.com/en-us/javascript/api/@azure/identity
 *
 * Vertex AI:
 * - Model-specific region variables (highest priority):
 *   - VERTEX_REGION_CLAUDE_3_5_HAIKU: Region for the 3.5 Haiku model
 *   - VERTEX_REGION_CLAUDE_HAIKU_4_5: Region for the Haiku 4.5 model
 *   - VERTEX_REGION_CLAUDE_3_5_SONNET: Region for the 3.5 Sonnet model
 *   - VERTEX_REGION_CLAUDE_3_7_SONNET: Region for the 3.7 Sonnet model
 * - CLOUD_ML_REGION: Optional. The default GCP region to use for all models
 *   If specific model region not specified above
 * - ANTHROPIC_VERTEX_PROJECT_ID: Required. Your GCP project ID
 * - Standard GCP credentials configured via google-auth-library
 *
 * Priority for determining region:
 * 1. Hardcoded model-specific environment variables
 * 2. Global CLOUD_ML_REGION variable
 * 3. Default region from config
 * 4. Fallback region (us-east5)
 */

function createStderrLogger(): ClientOptions['logger'] {
  return {
    error: (msg, ...args) =>
      // biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console
      console.error('[Anthropic SDK ERROR]', msg, ...args),
    // biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console
    warn: (msg, ...args) => console.error('[Anthropic SDK WARN]', msg, ...args),
    // biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console
    info: (msg, ...args) => console.error('[Anthropic SDK INFO]', msg, ...args),
    debug: (msg, ...args) =>
      // biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console
      console.error('[Anthropic SDK DEBUG]', msg, ...args),
  }
}

export async function getAnthropicClient({
  apiKey,
  maxRetries,
  model,
  fetchOverride,
  source,
}: {
  apiKey?: string
  maxRetries: number
  model?: string
  fetchOverride?: ClientOptions['fetch']
  source?: string
}): Promise<Anthropic> {
  const containerId = process.env.SANDCODE_CONTAINER_ID
  const remoteSessionId = process.env.SANDCODE_REMOTE_SESSION_ID
  const clientApp = process.env.SANDCODE_AGENT_SDK_CLIENT_APP
  const customHeaders = getCustomHeaders()
  const defaultHeaders: { [key: string]: string } = {
    'x-app': 'cli',
    'User-Agent': getUserAgent(),
    'X-Claude-Code-Session-Id': getSessionId(),
    ...customHeaders,
    ...(containerId ? { 'x-claude-remote-container-id': containerId } : {}),
    ...(remoteSessionId ? { 'x-claude-remote-session-id': remoteSessionId } : {}),
    // SDK consumers can identify their app/library for backend analytics
    ...(clientApp ? { 'x-client-app': clientApp } : {}),
  }

  // Log API client configuration for HFI debugging
  logForDebugging(
    `[API:request] Creating client, ANTHROPIC_CUSTOM_HEADERS present: ${!!process.env.ANTHROPIC_CUSTOM_HEADERS}, has Authorization header: ${!!customHeaders['Authorization']}`,
  )

  // Add additional protection header if enabled via env var
  const additionalProtectionEnabled = isEnvTruthy(process.env.SANDCODE_ADDITIONAL_PROTECTION)
  if (additionalProtectionEnabled) {
    defaultHeaders['x-anthropic-additional-protection'] = 'true'
  }

  logForDebugging('[API:auth] OAuth token check starting')
  await checkAndRefreshOAuthTokenIfNeeded()
  logForDebugging('[API:auth] OAuth token check complete')

  if (!isClaudeAISubscriber()) {
    await configureApiKeyHeaders(defaultHeaders, getIsNonInteractiveSession())
  }

  const resolvedFetch = buildFetch(fetchOverride, source)

  // z.ai Coding Plan extended-operations timeout per docs (50 min); standard
  // path keeps 10 min. Both honor API_TIMEOUT_MS override.
  const defaultTimeoutMs = getAPIProvider() === 'zai' ? 3_000_000 : 600 * 1000
  const ARGS = {
    defaultHeaders,
    maxRetries,
    timeout: parseInt(process.env.API_TIMEOUT_MS || String(defaultTimeoutMs), 10),
    dangerouslyAllowBrowser: true,
    fetchOptions: getProxyFetchOptions({
      forAnthropicAPI: true,
    }) as ClientOptions['fetchOptions'],
    ...(resolvedFetch && {
      fetch: resolvedFetch,
    }),
  }
  if (isEnvTruthy(process.env.SANDCODE_USE_BEDROCK)) {
    const { AnthropicBedrock } = await import('@sandcode/bedrock-sdk')
    // Use region override for small fast model if specified
    const awsRegion =
      model === getSmallFastModel() && process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION
        ? process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION
        : getAWSRegion()

    const bedrockArgs: ConstructorParameters<typeof AnthropicBedrock>[0] = {
      ...ARGS,
      awsRegion,
      ...(isEnvTruthy(process.env.SANDCODE_SKIP_BEDROCK_AUTH) && {
        skipAuth: true,
      }),
      ...(isDebugToStdErr() && { logger: createStderrLogger() }),
    }

    // Add API key authentication if available
    if (process.env.AWS_BEARER_TOKEN_BEDROCK) {
      bedrockArgs.skipAuth = true
      // Add the Bearer token for Bedrock API key authentication
      bedrockArgs.defaultHeaders = {
        ...bedrockArgs.defaultHeaders,
        Authorization: `Bearer ${process.env.AWS_BEARER_TOKEN_BEDROCK}`,
      }
    } else if (!isEnvTruthy(process.env.SANDCODE_SKIP_BEDROCK_AUTH)) {
      // Refresh auth and get credentials with cache clearing
      const cachedCredentials = await refreshAndGetAwsCredentials()
      if (cachedCredentials) {
        bedrockArgs.awsAccessKey = cachedCredentials.accessKeyId
        bedrockArgs.awsSecretKey = cachedCredentials.secretAccessKey
        bedrockArgs.awsSessionToken = cachedCredentials.sessionToken
      }
    }
    // we have always been lying about the return type - this doesn't support batching or models
    return new AnthropicBedrock(bedrockArgs) as unknown as Anthropic
  }
  if (isEnvTruthy(process.env.SANDCODE_USE_FOUNDRY)) {
    const { AnthropicFoundry } = await import('@sandcode/foundry-sdk')
    // Determine Azure AD token provider based on configuration
    // SDK reads ANTHROPIC_FOUNDRY_API_KEY by default
    let azureADTokenProvider: (() => Promise<string>) | undefined
    if (!process.env.ANTHROPIC_FOUNDRY_API_KEY) {
      if (isEnvTruthy(process.env.SANDCODE_SKIP_FOUNDRY_AUTH)) {
        // Mock token provider for testing/proxy scenarios (similar to Vertex mock GoogleAuth)
        azureADTokenProvider = () => Promise.resolve('')
      } else {
        // Use real Azure AD authentication with DefaultAzureCredential
        const { DefaultAzureCredential: AzureCredential, getBearerTokenProvider } =
          await import('@azure/identity')
        azureADTokenProvider = getBearerTokenProvider(
          new AzureCredential(),
          'https://cognitiveservices.azure.com/.default',
        )
      }
    }

    const foundryArgs: ConstructorParameters<typeof AnthropicFoundry>[0] = {
      ...ARGS,
      ...(azureADTokenProvider && { azureADTokenProvider }),
      ...(isDebugToStdErr() && { logger: createStderrLogger() }),
    }
    // we have always been lying about the return type - this doesn't support batching or models
    return new AnthropicFoundry(foundryArgs) as unknown as Anthropic
  }
  if (isEnvTruthy(process.env.SANDCODE_USE_VERTEX)) {
    // Refresh GCP credentials if gcpAuthRefresh is configured and credentials are expired
    // This is similar to how we handle AWS credential refresh for Bedrock
    if (!isEnvTruthy(process.env.SANDCODE_SKIP_VERTEX_AUTH)) {
      await refreshGcpCredentialsIfNeeded()
    }

    const [{ AnthropicVertex }, { GoogleAuth }] = await Promise.all([
      import('@sandcode/vertex-sdk'),
      import('google-auth-library'),
    ])
    // TODO: Cache either GoogleAuth instance or AuthClient to improve performance
    // Currently we create a new GoogleAuth instance for every getAnthropicClient() call
    // This could cause repeated authentication flows and metadata server checks
    // However, caching needs careful handling of:
    // - Credential refresh/expiration
    // - Environment variable changes (GOOGLE_APPLICATION_CREDENTIALS, project vars)
    // - Cross-request auth state management
    // See: https://github.com/googleapis/google-auth-library-nodejs/issues/390 for caching challenges

    // Prevent metadata server timeout by providing projectId as fallback
    // google-auth-library checks project ID in this order:
    // 1. Environment variables (GCLOUD_PROJECT, GOOGLE_CLOUD_PROJECT, etc.)
    // 2. Credential files (service account JSON, ADC file)
    // 3. gcloud config
    // 4. GCE metadata server (causes 12s timeout outside GCP)
    //
    // We only set projectId if user hasn't configured other discovery methods
    // to avoid interfering with their existing auth setup

    // Check project environment variables in same order as google-auth-library
    // See: https://github.com/googleapis/google-auth-library-nodejs/blob/main/src/auth/googleauth.ts
    const hasProjectEnvVar =
      process.env['GCLOUD_PROJECT'] ||
      process.env['GOOGLE_CLOUD_PROJECT'] ||
      process.env['gcloud_project'] ||
      process.env['google_cloud_project']

    // Check for credential file paths (service account or ADC)
    // Note: We're checking both standard and lowercase variants to be safe,
    // though we should verify what google-auth-library actually checks
    const hasKeyFile =
      process.env['GOOGLE_APPLICATION_CREDENTIALS'] || process.env['google_application_credentials']

    const googleAuth = isEnvTruthy(process.env.SANDCODE_SKIP_VERTEX_AUTH)
      ? ({
          // Mock GoogleAuth for testing/proxy scenarios
          getClient: () => ({
            getRequestHeaders: () => ({}),
          }),
        } as unknown as GoogleAuth)
      : new GoogleAuth({
          scopes: ['https://www.googleapis.com/auth/cloud-platform'],
          // Only use ANTHROPIC_VERTEX_PROJECT_ID as last resort fallback
          // This prevents the 12-second metadata server timeout when:
          // - No project env vars are set AND
          // - No credential keyfile is specified AND
          // - ADC file exists but lacks project_id field
          //
          // Risk: If auth project != API target project, this could cause billing/audit issues
          // Mitigation: Users can set GOOGLE_CLOUD_PROJECT to override
          ...(hasProjectEnvVar || hasKeyFile
            ? {}
            : {
                projectId: process.env.ANTHROPIC_VERTEX_PROJECT_ID,
              }),
        })

    const vertexArgs: ConstructorParameters<typeof AnthropicVertex>[0] = {
      ...ARGS,
      region: getVertexRegionForModel(model),
      googleAuth,
      ...(isDebugToStdErr() && { logger: createStderrLogger() }),
    }
    // we have always been lying about the return type - this doesn't support batching or models
    return new AnthropicVertex(vertexArgs) as unknown as Anthropic
  }

  // ── OpenAI-compatible providers ──────────────────────────────
  // All of these use the universal adapter from openaiCompatAdapter.ts

  const provider = getAPIProvider()

  // Custom providers (user-added via /provider add) — check FIRST so they
  // take priority over built-in provider names (e.g., user might add "OpenRouter"
  // via /provider add, which stores the key in customProviders, not openrouterApiKey)
  const customProviderCfg = getCustomProviderConfig()
  if (customProviderCfg) {
    const config: OpenAICompatProviderConfig = {
      name: customProviderCfg.name,
      baseUrl: customProviderCfg.baseUrl,
      apiKey: customProviderCfg.apiKey,
      defaultModel: process.env.SANDCODE_DEFAULT_MODEL ?? customProviderCfg.defaultModel,
      extraHeaders: customProviderCfg.extraHeaders,
    }
    logForDebugging(
      `[API:custom:${config.name}] apiKey=${config.apiKey ? 'set' : 'missing'}, baseURL=${config.baseUrl}`,
    )
    return createOpenAICompatClient(config)
  }

  if (provider === 'openai') {
    const config: OpenAICompatProviderConfig = {
      name: 'openai',
      baseUrl: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1',
      apiKey: process.env.OPENAI_API_KEY ?? getGlobalConfig().openaiApiKey ?? '',
      defaultModel: process.env.SANDCODE_DEFAULT_MODEL ?? 'gpt-4o',
      hasReasoningContent: true, // o-series models return reasoning
      maxTokensParam: 'max_tokens',
    }
    logForDebugging(
      `[API:openai] apiKey=${config.apiKey ? 'set' : 'missing'}, baseURL=${config.baseUrl}`,
    )
    return createOpenAICompatClient(config)
  }

  if (provider === 'deepseek') {
    const config: OpenAICompatProviderConfig = {
      name: 'deepseek',
      baseUrl: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com',
      apiKey: process.env.DEEPSEEK_API_KEY ?? getGlobalConfig().deepseekApiKey ?? '',
      defaultModel: process.env.SANDCODE_DEFAULT_MODEL ?? 'deepseek-chat',
      hasReasoningContent: true,
      preserveThinking: true, // DeepSeek requires reasoning_content preserved in multi-turn
    }
    logForDebugging(
      `[API:deepseek] apiKey=${config.apiKey ? 'set' : 'missing'}, baseURL=${config.baseUrl}`,
    )
    return createOpenAICompatClient(config)
  }

  if (provider === 'mistral') {
    const config: OpenAICompatProviderConfig = {
      name: 'mistral',
      baseUrl: process.env.MISTRAL_BASE_URL ?? 'https://api.mistral.ai/v1',
      apiKey: process.env.MISTRAL_API_KEY ?? getGlobalConfig().mistralApiKey ?? '',
      defaultModel: process.env.SANDCODE_DEFAULT_MODEL ?? 'mistral-large-latest',
    }
    logForDebugging(
      `[API:mistral] apiKey=${config.apiKey ? 'set' : 'missing'}, baseURL=${config.baseUrl}`,
    )
    return createOpenAICompatClient(config)
  }

  if (provider === 'gemini') {
    const config: OpenAICompatProviderConfig = {
      name: 'gemini',
      baseUrl:
        process.env.GEMINI_BASE_URL ?? 'https://generativelanguage.googleapis.com/v1beta/openai',
      apiKey: process.env.GEMINI_API_KEY ?? getGlobalConfig().geminiApiKey ?? '',
      defaultModel: process.env.SANDCODE_DEFAULT_MODEL ?? 'gemini-2.5-pro',
    }
    logForDebugging(
      `[API:gemini] apiKey=${config.apiKey ? 'set' : 'missing'}, baseURL=${config.baseUrl}`,
    )
    return createOpenAICompatClient(config)
  }

  if (provider === 'openrouter') {
    const orKey = process.env.OPENROUTER_API_KEY || getGlobalConfig().openrouterApiKey || ''
    if (!orKey) {
      logForDebugging('[API:openrouter] ERROR: No API key found in env or config')
    }
    const config: OpenAICompatProviderConfig = {
      name: 'openrouter',
      baseUrl: 'https://openrouter.ai/api/v1',
      apiKey: orKey,
      defaultModel: process.env.SANDCODE_DEFAULT_MODEL ?? 'anthropic/claude-sonnet-4',
      extraHeaders: {
        'HTTP-Referer': 'https://sandcode.sh',
        'X-Title': 'SANDCODE',
      },
    }
    logForDebugging(
      `[API:openrouter] apiKey=${orKey ? `${orKey.slice(0, 8)}...` : 'MISSING'}, baseURL=${config.baseUrl}`,
    )
    return createOpenAICompatClient(config)
  }

  if (provider === 'openai-compat') {
    const config: OpenAICompatProviderConfig = {
      name: 'openai-compat',
      baseUrl:
        process.env.OPENAI_COMPAT_BASE_URL ??
        getGlobalConfig().openaiCompatBaseUrl ??
        'http://localhost:11434/v1',
      apiKey: process.env.OPENAI_COMPAT_API_KEY ?? getGlobalConfig().openaiCompatApiKey ?? 'ollama',
      defaultModel:
        process.env.SANDCODE_DEFAULT_MODEL ??
        getGlobalConfig().openaiCompatDefaultModel ??
        'default',
    }
    logForDebugging(
      `[API:openai-compat] apiKey=${config.apiKey ? 'set' : 'missing'}, baseURL=${config.baseUrl}`,
    )
    return createOpenAICompatClient(config)
  }

  if (provider === 'zai') {
    const zaiKey =
      process.env.ZAI_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || getGlobalConfig().zaiApiKey
    // z.ai's Anthropic-compat shim (/api/anthropic) has a broken tool-streaming
    // path — content_block_stop never arrives for tool_use blocks, forcing
    // every tool-using turn through a failed-stream + non-streaming-retry
    // flow. Switch to native OpenAI protocol per z.ai docs:
    //   Coding Plan: https://api.z.ai/api/coding/paas/v4
    //   General API: https://api.z.ai/api/paas/v4
    const isCodingPlan = isEnvTruthy(process.env.ZAI_CODING_PLAN ?? '1') // default on
    const defaultBaseUrl = isCodingPlan
      ? 'https://api.z.ai/api/coding/paas/v4'
      : 'https://api.z.ai/api/paas/v4'
    const config: OpenAICompatProviderConfig = {
      name: 'zai',
      baseUrl: process.env.ZAI_BASE_URL ?? process.env.ANTHROPIC_BASE_URL ?? defaultBaseUrl,
      apiKey: zaiKey ?? '',
      defaultModel: process.env.SANDCODE_DEFAULT_MODEL ?? 'GLM-5.1',
      hasReasoningContent: true,
      alwaysIncludesUsage: true,
      supportsToolStream: true,
      preserveThinking: true,
    }
    logForDebugging(
      `[API:zai] apiKey=${zaiKey ? 'set' : 'missing'}, baseURL=${config.baseUrl} (codingPlan=${isCodingPlan})`,
    )
    return createOpenAICompatClient(config)
  }

  // Determine authentication method based on available tokens
  const clientConfig: ConstructorParameters<typeof Anthropic>[0] = {
    apiKey: isClaudeAISubscriber() ? null : apiKey || getAnthropicApiKey(),
    authToken: isClaudeAISubscriber() ? getClaudeAIOAuthTokens()?.accessToken : undefined,
    ...(process.env.USER_TYPE === 'ant' && isEnvTruthy(process.env.USE_STAGING_OAUTH)
      ? { baseURL: getOauthConfig().BASE_API_URL }
      : {}),
    ...ARGS,
    ...(isDebugToStdErr() && { logger: createStderrLogger() }),
  }

  return new Anthropic(clientConfig)
}

async function configureApiKeyHeaders(
  headers: Record<string, string>,
  isNonInteractiveSession: boolean,
): Promise<void> {
  const token =
    process.env.ANTHROPIC_AUTH_TOKEN || (await getApiKeyFromApiKeyHelper(isNonInteractiveSession))
  if (token) {
    headers['Authorization'] = `Bearer ${token}`
  }
}

function getCustomHeaders(): Record<string, string> {
  const customHeaders: Record<string, string> = {}
  const customHeadersEnv = process.env.ANTHROPIC_CUSTOM_HEADERS

  if (!customHeadersEnv) return customHeaders

  // Split by newlines to support multiple headers
  const headerStrings = customHeadersEnv.split(/\n|\r\n/)

  for (const headerString of headerStrings) {
    if (!headerString.trim()) continue

    // Parse header in format "Name: Value" (curl style). Split on first `:`
    // then trim — avoids regex backtracking on malformed long header lines.
    const colonIdx = headerString.indexOf(':')
    if (colonIdx === -1) continue
    const name = headerString.slice(0, colonIdx).trim()
    const value = headerString.slice(colonIdx + 1).trim()
    if (name) {
      customHeaders[name] = value
    }
  }

  return customHeaders
}

export const CLIENT_REQUEST_ID_HEADER = 'x-client-request-id'

function buildFetch(
  fetchOverride: ClientOptions['fetch'],
  source: string | undefined,
): ClientOptions['fetch'] {
  // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
  const inner = fetchOverride ?? globalThis.fetch
  // Only send to the first-party API — Bedrock/Vertex/Foundry don't log it
  // and unknown headers risk rejection by strict proxies (inc-4029 class).
  const injectClientRequestId = getAPIProvider() === 'firstParty' && isFirstPartyAnthropicBaseUrl()
  return (input, init) => {
    // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
    const headers = new Headers(init?.headers)
    // Generate a client-side request ID so timeouts (which return no server
    // request ID) can still be correlated with server logs by the API team.
    // Callers that want to track the ID themselves can pre-set the header.
    if (injectClientRequestId && !headers.has(CLIENT_REQUEST_ID_HEADER)) {
      headers.set(CLIENT_REQUEST_ID_HEADER, randomUUID())
    }
    try {
      // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
      const url = input instanceof Request ? input.url : String(input)
      const id = headers.get(CLIENT_REQUEST_ID_HEADER)
      logForDebugging(
        `[API REQUEST] ${new URL(url).pathname}${id ? ` ${CLIENT_REQUEST_ID_HEADER}=${id}` : ''} source=${source ?? 'unknown'}`,
      )
    } catch {
      // never let logging crash the fetch
    }
    return inner(input, { ...init, headers })
  }
}
