/**
 * Google Identity Platform — OAuth2 PKCE utilities.
 *
 * Scope: https://mail.google.com/ (full IMAP access, XOAUTH2)
 * Public Client / No Client-Secret — PKCE (S256) same as MS-OAuth.
 *
 * Google-Specifics vs. Microsoft:
 *   - Token endpoint: accounts.google.com (not login.microsoftonline.com)
 *   - refresh_token kommt NUR beim ersten Consent zurück. Daher: prompt=consent
 *     + access_type=offline PFLICHT bei jedem init. Ohne das: kein refresh_token
 *     → Connection lebt nur 1h (access_token TTL).
 *   - Google rotiert refresh_tokens NICHT bei Nutzung (anders als MS). Ein einmal
 *     ausgestelltes refresh_token gilt bis es manuell revoked wird.
 *   - Email kommt aus id_token Claim `email` (immer gesetzt wenn openid-Scope vorhanden).
 *   - IMAP: imap.gmail.com:993, XOAUTH2 via ImapFlow { user, accessToken }.
 *
 * See: https://developers.google.com/identity/protocols/oauth2/native-app
 */

import { createHash, randomBytes } from "crypto";

const GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
export const GOOGLE_AUTH_BASE = "https://accounts.google.com/o/oauth2/v2/auth";

/**
 * Scopes angefragt.
 * https://mail.google.com/ = full IMAP access (READ + DELETE + EXPUNGE).
 * openid + email = id_token mit email-Claim für User-Identifikation.
 */
export const GOOGLE_OAUTH_SCOPES = [
  "https://mail.google.com/",
  "openid",
  "email",
].join(" ");

/**
 * Redirect-URI für Google iOS-OAuth-Client.
 *
 * Google verlangt für iOS-Client-Type eines von zwei Formaten:
 *   a) Reverse-Client-ID: `com.googleusercontent.apps.<id>:/oauth2redirect` (gewählt)
 *   b) Bundle-ID: `org.rebreak.app:/oauth2redirect`
 *
 * Custom-Schemes wie `rebreak://...` (Microsoft-Pattern) werden mit
 * `invalid_request — doesn't comply with OAuth 2.0 policy` rejected.
 * Empirisch verifiziert 2026-05-28.
 *
 * iOS-App muss diesen URL-Scheme in CFBundleURLTypes registrieren.
 */
export const GOOGLE_REDIRECT_URI =
  "com.googleusercontent.apps.864178840836-i09oblmcel5q4rgggq9dids17mv9560u:/oauth2redirect";

// ── PKCE Helpers ──────────────────────────────────────────────────────────────
// Identisch zu ms-oauth.ts — ausgelagert weil beide Provider PKCE S256 nutzen.

/** Generates a cryptographically random code_verifier (43-128 chars, URL-safe). */
export function generateCodeVerifier(): string {
  return randomBytes(96).toString("base64url");
}

/** Computes S256 code_challenge from the verifier. */
export function computeCodeChallenge(verifier: string): string {
  return createHash("sha256").update(verifier).digest("base64url");
}

/** Generates a random state ID for CSRF protection (hex, 32 chars = 128 bits). */
export function generateStateId(): string {
  return randomBytes(16).toString("hex");
}

// ── Token Exchange ─────────────────────────────────────────────────────────────

export interface GoogleTokenResponse {
  access_token: string;
  /**
   * KRITISCH: Google gibt refresh_token NUR beim ersten Consent zurück
   * (prompt=consent in init). Bei nachfolgendem Re-Connect ohne prompt=consent:
   * refresh_token fehlt in der Response. Daher immer prompt=consent verwenden.
   */
  refresh_token?: string;
  /** Sekunden bis access_token abläuft (typisch 3600 für Google). */
  expires_in: number;
  scope: string;
  token_type: string;
  /** JWT mit user-claims (email, sub). Kommt weil openid-Scope angefragt. */
  id_token?: string;
}

/**
 * Exchanges an authorization_code for tokens.
 * Wirft wenn: HTTP-Fehler, access_token fehlt, oder refresh_token fehlt
 * (letzteres ist ein Config-Fehler — prompt=consent + access_type=offline vergessen).
 */
export async function exchangeCodeForTokens(params: {
  clientId: string;
  code: string;
  codeVerifier: string;
}): Promise<GoogleTokenResponse & { refresh_token: string }> {
  const body = new URLSearchParams({
    grant_type: "authorization_code",
    client_id: params.clientId,
    code: params.code,
    redirect_uri: GOOGLE_REDIRECT_URI,
    code_verifier: params.codeVerifier,
  });

  const res = await fetch(GOOGLE_TOKEN_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: body.toString(),
  });

  if (!res.ok) {
    const errText = await res.text().catch(() => "unknown error");
    throw new Error(`Google token exchange failed (${res.status}): ${errText}`);
  }

  const data = await res.json() as GoogleTokenResponse;

  if (!data.access_token) {
    throw new Error("Google token response missing access_token");
  }

  if (!data.refresh_token) {
    // Passiert wenn init NICHT mit prompt=consent aufgerufen wurde.
    // Oder wenn der User die App-Permission schon mal erteilt hat und
    // prompt=consent übersprungen wurde. Lösung: User muss in Google
    // Account-Settings die App-Berechtigung revoken und neu connecten.
    throw new Error(
      "Google token response missing refresh_token. " +
      "Ensure init uses prompt=consent and access_type=offline. " +
      "If user previously granted access, they must revoke and re-authorize.",
    );
  }

  return data as GoogleTokenResponse & { refresh_token: string };
}

/**
 * Refreshes a Google access_token using a refresh_token.
 *
 * Google-Spezifik: Google rotiert refresh_tokens NICHT bei normalem Einsatz.
 * Das ausgestellte refresh_token ist langlebig. Ausnahmen:
 *   - User revoked App-Permission in Google-Account-Settings
 *   - 6-Monate Inaktivität (Google's policy)
 *   - Max. 50 refresh_tokens pro Client+User (älteste werden invalidiert)
 *
 * Im Gegensatz zu MS: response.refresh_token kann absent sein — dann den
 * bestehenden behalten (kein Rotations-Problem wie bei MS AADSTS70043).
 */
export async function refreshGoogleTokens(params: {
  clientId: string;
  refreshToken: string;
}): Promise<{
  access_token: string;
  /** Google gibt bei Refresh keinen neuen refresh_token — Original behalten. */
  refresh_token: string;
  expires_in: number;
}> {
  const body = new URLSearchParams({
    grant_type: "refresh_token",
    client_id: params.clientId,
    refresh_token: params.refreshToken,
  });

  const res = await fetch(GOOGLE_TOKEN_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: body.toString(),
  });

  if (!res.ok) {
    const errText = await res.text().catch(() => "unknown error");
    throw new Error(`Google token refresh failed (${res.status}): ${errText}`);
  }

  const data = await res.json() as GoogleTokenResponse;

  if (!data.access_token) {
    throw new Error("Google refresh response missing access_token");
  }

  return {
    access_token: data.access_token,
    // Google gibt bei Refresh keinen neuen refresh_token — behalte Original.
    refresh_token: params.refreshToken,
    expires_in: data.expires_in,
  };
}

/**
 * Extracts the email claim from a Google ID-token (JWT).
 * Google garantiert `email`-Claim wenn openid+email scope vorhanden.
 * Keine Signatur-Verifikation nötig (direkt vom Token-Endpoint über HTTPS empfangen).
 */
export function extractEmailFromIdToken(idToken: string): string | null {
  try {
    const [, payloadB64] = idToken.split(".");
    if (!payloadB64) return null;
    const payload = JSON.parse(
      Buffer.from(payloadB64, "base64url").toString("utf8"),
    );
    // Google setzt `email` direkt — kein preferred_username-Fallback nötig.
    return payload.email ?? null;
  } catch {
    return null;
  }
}
