/**
 * Microsoft Identity Platform — OAuth2 PKCE utilities.
 *
 * Tenant: 'common' — covers consumer Outlook/Hotmail/Live/MSN accounts
 * AND Microsoft 365 work/school accounts. Decision: common > consumers because
 * the Azure App Registration was created as Multi-Tenant + Personal Accounts.
 *
 * Public Client / No Client-Secret — PKCE (S256) is the security mechanism.
 * Microsoft explicitly supports PKCE without client_secret for public clients
 * (mobile/native apps). See: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
 */

import { createHash, randomBytes } from "crypto";

const MS_TENANT = "common";
const MS_TOKEN_ENDPOINT = `https://login.microsoftonline.com/${MS_TENANT}/oauth2/v2.0/token`;
export const MS_AUTH_BASE = `https://login.microsoftonline.com/${MS_TENANT}/oauth2/v2.0/authorize`;

/**
 * OAuth scopes requested.
 * Matches the DSGVO-Memo Section 4.3 (Datenminimierung).
 *
 * Single-resource constraint: Microsoft V2.0 erlaubt im /token-Exchange nur
 * Scopes EINES Resource-Servers. IMAP.AccessAsUser.All zielt auf
 * outlook.office.com, User.Read auf graph.microsoft.com — die Kombination
 * wirft `AADSTS70011: scopes are not compatible with each other`.
 *
 * Daher: KEIN User.Read. Email-Adresse kommt aus dem id_token-Claim
 * `preferred_username` (openid-Scope reicht). Falls künftig der Display-Name
 * gebraucht wird → separater Microsoft-Graph-Token-Exchange (OBO-Pattern).
 */
export const MS_OAUTH_SCOPES = [
  "https://outlook.office.com/IMAP.AccessAsUser.All",
  "offline_access",
  "openid",
  // Standard-OIDC-Scope: liefert `email`-Claim ins id_token. Microsoft Personal-
  // Accounts geben mit `openid` allein keinen Email-Claim raus (preferred_username
  // ist Display-Identifier, kein garantiertes Email-Feld). `email` ist ein OIDC-
  // "identity scope" und damit per Definition kompatibel mit jeder Resource-API
  // — verursacht KEIN AADSTS70011 mit dem IMAP-Scope.
  "email",
].join(" ");

/**
 * The redirect_uri registered in the Azure App Registration for the native client.
 * Must match exactly what the client sends — scheme registered in app.json.
 */
export const MS_REDIRECT_URI = "rebreak://auth/mail-oauth-callback";

// ── PKCE Helpers ─────────────────────────────────────────────────────────────

/** Generates a cryptographically random code_verifier (43-128 chars, URL-safe). */
export function generateCodeVerifier(): string {
  // 96 random bytes → 128 base64url chars (well within PKCE 43-128 range)
  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 MicrosoftTokenResponse {
  access_token: string;
  /** Microsoft ALWAYS returns a refresh_token when offline_access scope is included. */
  refresh_token: string;
  /** Seconds until access_token expires (typically 3600 for MS). */
  expires_in: number;
  scope: string;
  token_type: string;
  /** JWT containing user claims (email, sub, etc.). Returned because openid scope is included. */
  id_token?: string;
}

/**
 * Exchanges an authorization_code for tokens.
 * Called from callback endpoint after state validation.
 */
export async function exchangeCodeForTokens(params: {
  clientId: string;
  code: string;
  codeVerifier: string;
}): Promise<MicrosoftTokenResponse> {
  const body = new URLSearchParams({
    grant_type: "authorization_code",
    client_id: params.clientId,
    code: params.code,
    redirect_uri: MS_REDIRECT_URI,
    code_verifier: params.codeVerifier,
    scope: MS_OAUTH_SCOPES,
  });

  const res = await fetch(MS_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(`MS token exchange failed (${res.status}): ${errText}`);
  }

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

  if (!data.access_token || !data.refresh_token) {
    throw new Error("MS token response missing access_token or refresh_token");
  }

  return data;
}

/**
 * Refreshes a Microsoft access_token using a refresh_token.
 *
 * CRITICAL: Microsoft rotates refresh_tokens — the response may contain a NEW
 * refresh_token that invalidates the old one. Always persist the new refresh_token
 * if present, otherwise the next refresh will fail with AADSTS70043.
 */
export async function refreshMicrosoftTokens(params: {
  clientId: string;
  refreshToken: string;
}): Promise<{
  access_token: string;
  /** May be a NEW token (MS refresh token rotation). Persist this immediately. */
  refresh_token: string;
  expires_in: number;
}> {
  const body = new URLSearchParams({
    grant_type: "refresh_token",
    client_id: params.clientId,
    refresh_token: params.refreshToken,
    scope: MS_OAUTH_SCOPES,
  });

  const res = await fetch(MS_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(`MS token refresh failed (${res.status}): ${errText}`);
  }

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

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

  return {
    access_token: data.access_token,
    // MS may omit refresh_token if it didn't rotate — fall back to original
    refresh_token: data.refresh_token ?? params.refreshToken,
    expires_in: data.expires_in,
  };
}

/**
 * Extracts the email claim from a Microsoft ID-token (JWT).
 * The ID-token is a standard JWT — we only need the payload, no signature
 * verification required here because:
 *   1. We received it directly from Microsoft's token endpoint over HTTPS.
 *   2. We don't rely on it for auth decisions — just for extracting the email
 *      to store in mail_connections.email.
 *
 * Claims checked (in order): email → preferred_username → upn
 */
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"));
    return (
      payload.email ??
      payload.preferred_username ??
      payload.upn ??
      null
    );
  } catch {
    return null;
  }
}
