/**
 * @wrnexus/oauth — OAuth 2.0 sign-in with any provider. Ships presets for Google,
 * GitHub, and Discord, and `defineProvider` for a custom one. Dependency-free
 * (uses `fetch` + WebCrypto for PKCE). Pairs with @wrnexus/core's `logIn`.
 *
 *   const provider = google({ clientId, clientSecret });
 *   // 1. send the user to the provider:
 *   const { url, state, verifier } = await startAuth(provider, { redirectUri });
 *   // (store `state` + `verifier` in the session, then 302 to `url`)
 *   // 2. on the callback:
 *   const { profile } = await completeAuth(provider, { code, redirectUri, verifier });
 *   logIn(ctx, { id: profile.id, email: profile.email });
 */

export interface OAuthTokens {
  access_token: string;
  token_type?: string;
  refresh_token?: string;
  expires_in?: number;
  id_token?: string;
  scope?: string;
}

export interface OAuthProfile {
  id: string;
  email?: string;
  name?: string;
  avatar?: string;
  raw: Record<string, unknown>;
}

export interface OAuthProvider {
  name: string;
  authorizeUrl: string;
  tokenUrl: string;
  userInfoUrl: string;
  scopes: string[];
  clientId: string;
  clientSecret: string;
  /** Extra params for the authorize request (e.g. `access_type`, `prompt`). */
  authorizeParams?: Record<string, string>;
  /** Normalize the provider's raw userinfo into an OAuthProfile. */
  mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
}

export interface ProviderCredentials {
  clientId: string;
  clientSecret: string;
  scopes?: string[];
}

type FetchLike = typeof fetch;

// --- Presets ---------------------------------------------------------------

export function google(creds: ProviderCredentials): OAuthProvider {
  return {
    name: "google",
    authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
    tokenUrl: "https://oauth2.googleapis.com/token",
    userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
    scopes: creds.scopes ?? ["openid", "email", "profile"],
    clientId: creds.clientId,
    clientSecret: creds.clientSecret,
    authorizeParams: { access_type: "offline" },
    mapProfile: (raw) => ({
      id: String(raw.sub),
      email: raw.email as string | undefined,
      name: raw.name as string | undefined,
      avatar: raw.picture as string | undefined,
      raw,
    }),
  };
}

export function github(creds: ProviderCredentials): OAuthProvider {
  return {
    name: "github",
    authorizeUrl: "https://github.com/login/oauth/authorize",
    tokenUrl: "https://github.com/login/oauth/access_token",
    userInfoUrl: "https://api.github.com/user",
    scopes: creds.scopes ?? ["read:user", "user:email"],
    clientId: creds.clientId,
    clientSecret: creds.clientSecret,
    mapProfile: (raw) => ({
      id: String(raw.id),
      email: raw.email as string | undefined,
      name: (raw.name as string) || (raw.login as string),
      avatar: raw.avatar_url as string | undefined,
      raw,
    }),
  };
}

export function discord(creds: ProviderCredentials): OAuthProvider {
  return {
    name: "discord",
    authorizeUrl: "https://discord.com/api/oauth2/authorize",
    tokenUrl: "https://discord.com/api/oauth2/token",
    userInfoUrl: "https://discord.com/api/users/@me",
    scopes: creds.scopes ?? ["identify", "email"],
    clientId: creds.clientId,
    clientSecret: creds.clientSecret,
    mapProfile: (raw) => ({
      id: String(raw.id),
      email: raw.email as string | undefined,
      name: raw.username as string | undefined,
      avatar: raw.avatar
        ? `https://cdn.discordapp.com/avatars/${raw.id}/${raw.avatar}.png`
        : undefined,
      raw,
    }),
  };
}

/** Define a custom OAuth2 provider. */
export function defineProvider(config: OAuthProvider): OAuthProvider {
  return config;
}

// --- PKCE ------------------------------------------------------------------

function b64url(bytes: Uint8Array): string {
  let bin = "";
  for (const b of bytes) bin += String.fromCharCode(b);
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

/** A random URL-safe token (for `state` and the PKCE verifier). */
export function randomToken(bytes = 32): string {
  const buf = new Uint8Array(bytes);
  crypto.getRandomValues(buf);
  return b64url(buf);
}

async function pkceChallenge(verifier: string): Promise<string> {
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
  return b64url(new Uint8Array(digest));
}

// --- Flow ------------------------------------------------------------------

export interface StartAuthOptions {
  redirectUri: string;
  /** Provide to reuse a state (else one is generated). */
  state?: string;
  /** Extra authorize params (merged over the provider's). */
  params?: Record<string, string>;
}

export interface StartAuthResult {
  /** The full authorize URL to redirect the user to. */
  url: string;
  /** CSRF state — store it (session/cookie) and verify on callback. */
  state: string;
  /** PKCE code verifier — store it and pass to `completeAuth`. */
  verifier: string;
}

/** Build the authorize redirect (with PKCE + state). */
export async function startAuth(
  provider: OAuthProvider,
  options: StartAuthOptions,
): Promise<StartAuthResult> {
  const state = options.state ?? randomToken();
  const verifier = randomToken();
  const challenge = await pkceChallenge(verifier);
  const url = new URL(provider.authorizeUrl);
  const params: Record<string, string> = {
    response_type: "code",
    client_id: provider.clientId,
    redirect_uri: options.redirectUri,
    scope: provider.scopes.join(" "),
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
    ...provider.authorizeParams,
    ...options.params,
  };
  for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
  return { url: url.toString(), state, verifier };
}

export interface CompleteAuthOptions {
  code: string;
  redirectUri: string;
  /** The PKCE verifier from `startAuth`. */
  verifier?: string;
  /** Inject a fetch implementation (tests). */
  fetch?: FetchLike;
}

/** Exchange the authorization code for tokens, then fetch the user profile. */
export async function completeAuth(
  provider: OAuthProvider,
  options: CompleteAuthOptions,
): Promise<{ tokens: OAuthTokens; profile: OAuthProfile }> {
  const tokens = await exchangeCode(provider, options);
  const profile = await fetchProfile(provider, tokens, options.fetch);
  return { tokens, profile };
}

/** Exchange an authorization code for tokens. */
export async function exchangeCode(
  provider: OAuthProvider,
  options: CompleteAuthOptions,
): Promise<OAuthTokens> {
  const doFetch = options.fetch ?? fetch;
  const body = new URLSearchParams({
    grant_type: "authorization_code",
    client_id: provider.clientId,
    client_secret: provider.clientSecret,
    code: options.code,
    redirect_uri: options.redirectUri,
  });
  if (options.verifier) body.set("code_verifier", options.verifier);

  const res = await doFetch(provider.tokenUrl, {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
    body,
  });
  if (!res.ok) throw new Error(`${provider.name} token exchange failed (${res.status})`);
  return (await res.json()) as OAuthTokens;
}

/** Fetch + normalize the user's profile from the provider. */
export async function fetchProfile(
  provider: OAuthProvider,
  tokens: OAuthTokens,
  fetchImpl?: FetchLike,
): Promise<OAuthProfile> {
  const doFetch = fetchImpl ?? fetch;
  const res = await doFetch(provider.userInfoUrl, {
    headers: {
      authorization: `${tokens.token_type ?? "Bearer"} ${tokens.access_token}`,
      accept: "application/json",
      "user-agent": "wrnexus-oauth",
    },
  });
  if (!res.ok) throw new Error(`${provider.name} userinfo failed (${res.status})`);
  return provider.mapProfile((await res.json()) as Record<string, unknown>);
}
export {
  memoryOAuthStateStore,
  createOAuthState,
  refreshOAuthTokens,
  discoverOidc,
  validateOidcClaims,
  verifyOidcIdToken,
  validateOAuthReturnTo,
} from "./advanced.ts";
export type {
  OAuthStateRecord,
  OAuthStateStore,
  OidcDiscovery,
  OidcIdTokenClaims,
  VerifyOidcIdTokenOptions,
} from "./advanced.ts";
