/**
 * OAuth Provider Configurations and Handlers
 *
 * This file re-exports from the modular providers/ directory.
 * Each provider is now in its own file for maintainability.
 *
 * @see ./providers/index.js for the registry
 */

import { generatePKCE, generateState } from "./utils/pkce";
import { PROVIDERS } from "./providers/index";
import { resolvePublicCred } from "@omniroute/open-sse/utils/publicCreds.ts";

const GOOGLE_BROWSER_PROVIDERS = new Set(["antigravity", "agy", "gemini-cli"]);

type OAuthRedirectEnv = Record<string, string | undefined>;

function hasValue(value: string | undefined): boolean {
  return typeof value === "string" && value.trim().length > 0;
}

function firstValue(...values: Array<string | undefined>): string | undefined {
  return values.find(hasValue);
}

function normalizeBaseUrl(value: unknown): string {
  const trimmed = typeof value === "string" ? value.trim() : "";
  if (!trimmed) return "";
  return trimmed.replace(/\/+$/, "");
}

function hasCustomGoogleOAuthCredentials(
  providerName: string,
  env: OAuthRedirectEnv | null | undefined = process.env
): boolean {
  if (providerName === "antigravity" || providerName === "agy") {
    // `agy` reuses the antigravity OAuth client + env overrides.
    const clientId = env?.ANTIGRAVITY_OAUTH_CLIENT_ID;
    const clientSecret = env?.ANTIGRAVITY_OAUTH_CLIENT_SECRET;
    return (
      hasValue(clientId) &&
      hasValue(clientSecret) &&
      clientId !== resolvePublicCred("antigravity_id")
    );
  }

  if (providerName === "gemini-cli") {
    const clientId = firstValue(env?.GEMINI_CLI_OAUTH_CLIENT_ID, env?.GEMINI_OAUTH_CLIENT_ID);
    const clientSecret = firstValue(
      env?.GEMINI_CLI_OAUTH_CLIENT_SECRET,
      env?.GEMINI_OAUTH_CLIENT_SECRET
    );
    return (
      hasValue(clientId) &&
      hasValue(clientSecret) &&
      clientId !== resolvePublicCred("gemini_id")
    );
  }

  return false;
}

function isLoopbackHostname(hostname: string): boolean {
  return /^(localhost|127\.0\.0\.1|\[::1\]|::1)$/i.test(hostname);
}

/**
 * Google providers default to loopback redirects so the embedded public
 * credentials keep working on out-of-the-box local installs. When operators
 * provide their own Google OAuth client IDs for a remote deployment, prefer the
 * public callback URL documented in .env.example / docs/README so the popup can
 * navigate back to OmniRoute instead of stalling on localhost.
 */
export function resolveBrowserOAuthRedirectUri(
  providerName: string,
  redirectUri: string,
  env: OAuthRedirectEnv | null | undefined = process.env
): string {
  if (!GOOGLE_BROWSER_PROVIDERS.has(providerName)) {
    return redirectUri;
  }

  if (!hasCustomGoogleOAuthCredentials(providerName, env)) {
    return redirectUri;
  }

  const publicBaseUrl =
    normalizeBaseUrl(env.NEXT_PUBLIC_BASE_URL) || normalizeBaseUrl(env.OMNIROUTE_PUBLIC_BASE_URL);

  if (!publicBaseUrl) {
    return redirectUri;
  }

  try {
    const requested = new URL(redirectUri);
    if (!isLoopbackHostname(requested.hostname)) {
      return redirectUri;
    }

    const callbackPath =
      requested.pathname && requested.pathname !== "/" ? requested.pathname : "/callback";
    return `${publicBaseUrl}${callbackPath}${requested.search}`;
  } catch {
    return redirectUri;
  }
}

/**
 * Get provider handler
 */
export function getProvider(name) {
  const provider = PROVIDERS[name];
  if (!provider) {
    throw new Error(`Unknown provider: ${name}`);
  }
  return provider;
}

/**
 * Get all provider names
 */
export function getProviderNames() {
  return Object.keys(PROVIDERS);
}

/**
 * Generate auth data for a provider.
 *
 * Returns `{ supported: false, error }` (no `authUrl`) for providers whose
 * browser-OAuth flow is currently disabled — e.g. windsurf / devin-cli post
 * 2026-05 rebrand, where the legacy PKCE endpoint at app.devin.ai returns 404.
 * Callers (UI / API route) should surface the `error` string and route the
 * user to the import-token flow instead.
 */
export function generateAuthData(providerName, redirectUri) {
  const provider = getProvider(providerName);
  const { codeVerifier, codeChallenge, state } = generatePKCE();

  if (provider.flowType === "import_token") {
    const error =
      providerName === "windsurf" || providerName === "devin-cli"
        ? "Browser login disabled — paste token from https://windsurf.com/show-auth-token instead. Phase 2 will restore Firebase OAuth via app.devin.ai successor."
        : `Browser login is disabled for ${providerName}. Use the import-token flow instead.`;
    return {
      authUrl: undefined,
      state: undefined,
      codeVerifier: undefined,
      codeChallenge: undefined,
      redirectUri,
      flowType: provider.flowType,
      fixedPort: provider.fixedPort,
      callbackPath: provider.callbackPath || "/callback",
      supported: false,
      error,
    };
  }

  let authUrl;
  if (provider.flowType === "device_code") {
    authUrl = null;
  } else if (provider.flowType === "authorization_code_pkce") {
    authUrl = provider.buildAuthUrl(provider.config, redirectUri, state, codeChallenge);
  } else {
    authUrl = provider.buildAuthUrl(provider.config, redirectUri, state);
  }

  return {
    authUrl,
    state,
    codeVerifier,
    codeChallenge,
    redirectUri,
    flowType: provider.flowType,
    fixedPort: provider.fixedPort,
    callbackPath: provider.callbackPath || "/callback",
  };
}

/**
 * Exchange code for tokens
 */
export async function exchangeTokens(providerName, code, redirectUri, codeVerifier, state) {
  const provider = getProvider(providerName);

  const tokens = await provider.exchangeToken(
    provider.config,
    code,
    redirectUri,
    codeVerifier,
    state
  );

  let extra = null;
  if (provider.postExchange) {
    extra = await provider.postExchange(tokens);
  }

  return provider.mapTokens(tokens, extra);
}

/**
 * Finalize tokens obtained out-of-band (e.g. the browser-driven Codex device
 * flow, where the browser performs the auth.openai.com exchange because the
 * server's datacenter IP is blocked). Runs the provider's postExchange +
 * mapTokens — the same tail as exchangeTokens — without an HTTP token exchange.
 */
export async function finalizeTokens(providerName, tokens) {
  const provider = getProvider(providerName);

  let extra = null;
  if (provider.postExchange) {
    extra = await provider.postExchange(tokens);
  }

  return provider.mapTokens(tokens, extra);
}

/**
 * Request device code (for device_code flow)
 */
export async function requestDeviceCode(providerName, codeChallenge, configOverride = null) {
  const provider = getProvider(providerName);
  if (provider.flowType !== "device_code") {
    throw new Error(`Provider ${providerName} does not support device code flow`);
  }
  return await provider.requestDeviceCode(configOverride || provider.config, codeChallenge);
}

/**
 * Poll for token (for device_code flow)
 * @param {string} providerName - Provider name
 * @param {string} deviceCode - Device code from requestDeviceCode
 * @param {string} codeVerifier - PKCE code verifier (optional for some providers)
 * @param {object} extraData - Extra data from device code response (e.g. clientId/clientSecret for Kiro)
 */
export async function pollForToken(providerName, deviceCode, codeVerifier, extraData) {
  const provider = getProvider(providerName);
  if (provider.flowType !== "device_code") {
    throw new Error(`Provider ${providerName} does not support device code flow`);
  }

  const result = await provider.pollToken(provider.config, deviceCode, codeVerifier, extraData);

  if (result.ok) {
    if (result.data.access_token) {
      let extra = null;
      if (provider.postExchange) {
        extra = await provider.postExchange(result.data);
      }
      return { success: true, tokens: provider.mapTokens(result.data, extra) };
    } else {
      if (result.data.error === "authorization_pending" || result.data.error === "slow_down") {
        return {
          success: false,
          error: result.data.error,
          errorDescription: result.data.error_description || result.data.message,
          pending: result.data.error === "authorization_pending",
        };
      } else {
        return {
          success: false,
          error: result.data.error || "no_access_token",
          errorDescription:
            result.data.error_description || result.data.message || "No access token received",
        };
      }
    }
  }

  return {
    success: false,
    error: result.data.error,
    errorDescription: result.data.error_description,
  };
}
