import {
  createHash,
  createPublicKey,
  randomBytes,
  timingSafeEqual,
  verify,
} from "node:crypto";

import { getOidcConfiguration } from "@/lib/sso/config";

type OidcTokenResponse = {
  token_type?: string;
  scope?: string;
  expires_in?: number;
  id_token?: string;
  access_token?: string;
  error?: string;
  error_description?: string;
};

type OidcIdTokenClaims = {
  aud?: string | string[];
  exp?: number;
  iat?: number;
  iss?: string;
  nbf?: number;
  nonce?: string;
  oid?: string;
  sub?: string;
  tid?: string;
  name?: string;
  email?: string;
  preferred_username?: string;
  upn?: string;
};

type JwtHeader = {
  alg?: string;
  kid?: string;
  typ?: string;
};

type MicrosoftJwk = {
  kid?: string;
  kty?: string;
  use?: string;
  n?: string;
  e?: string;
  x5c?: string[];
};

type MicrosoftJwksResponse = {
  keys?: MicrosoftJwk[];
};

export type ValidatedOidcProfile = {
  email: string;
  name: string | null;
  provider: "microsoft";
  providerId: string;
};

function randomBase64Url(size = 32) {
  return randomBytes(size).toString("base64url");
}

function parseJwtPart<T>(part: string): T {
  try {
    return JSON.parse(
      Buffer.from(part, "base64url").toString("utf8"),
    ) as T;
  } catch {
    throw new Error("OIDC_INVALID_ID_TOKEN");
  }
}

function audienceMatches(
  audience: string | string[] | undefined,
  clientId: string,
) {
  if (typeof audience === "string") {
    return audience === clientId;
  }

  return Array.isArray(audience) && audience.includes(clientId);
}

export function safeOidcValueEquals(
  left: string | null | undefined,
  right: string | null | undefined,
) {
  if (!left || !right) return false;

  const leftBuffer = Buffer.from(left);
  const rightBuffer = Buffer.from(right);

  return (
    leftBuffer.length === rightBuffer.length &&
    timingSafeEqual(leftBuffer, rightBuffer)
  );
}

export function createOidcAuthorizationRequest() {
  const configuration = getOidcConfiguration();
  const state = randomBase64Url();
  const nonce = randomBase64Url();
  const codeVerifier = randomBase64Url(48);
  const codeChallenge = createHash("sha256")
    .update(codeVerifier)
    .digest("base64url");

  const url = new URL(configuration.authorizationEndpoint);
  url.searchParams.set("client_id", configuration.clientId);
  url.searchParams.set("response_type", "code");
  url.searchParams.set("redirect_uri", configuration.redirectUri);
  url.searchParams.set("response_mode", "query");
  url.searchParams.set("scope", configuration.scopes);
  url.searchParams.set("state", state);
  url.searchParams.set("nonce", nonce);
  url.searchParams.set("code_challenge", codeChallenge);
  url.searchParams.set("code_challenge_method", "S256");

  return {
    url,
    state,
    nonce,
    codeVerifier,
  };
}

async function exchangeAuthorizationCode(
  code: string,
  codeVerifier: string,
) {
  const configuration = getOidcConfiguration();
  const body = new URLSearchParams({
    client_id: configuration.clientId,
    client_secret: configuration.clientSecret,
    code,
    redirect_uri: configuration.redirectUri,
    grant_type: "authorization_code",
    scope: configuration.scopes,
    code_verifier: codeVerifier,
  });

  const response = await fetch(configuration.tokenEndpoint, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body,
    cache: "no-store",
  });

  const data = (await response.json().catch(() => null)) as
    | OidcTokenResponse
    | null;

  if (!response.ok || !data?.id_token) {
    const details = data?.error_description || data?.error;
    throw new Error(
      details
        ? `OIDC_TOKEN_EXCHANGE_FAILED:${details}`
        : "OIDC_TOKEN_EXCHANGE_FAILED",
    );
  }

  return data.id_token;
}

async function getSigningKey(kid: string) {
  const configuration = getOidcConfiguration();

  const response = await fetch(configuration.jwksUri, {
    cache: "no-store",
  });

  if (!response.ok) {
    throw new Error("OIDC_JWKS_UNAVAILABLE");
  }

  const data = (await response.json()) as MicrosoftJwksResponse;
  const jwk = data.keys?.find((key) => key.kid === kid);

  if (!jwk) {
    throw new Error("OIDC_SIGNING_KEY_NOT_FOUND");
  }

  return createPublicKey({
    key: jwk as unknown as import("node:crypto").JsonWebKey,
    format: "jwk",
  });
}



async function validateIdToken(idToken: string, expectedNonce: string) {
  const configuration = getOidcConfiguration();
  const parts = idToken.split(".");

  if (parts.length !== 3) {
    throw new Error("OIDC_INVALID_ID_TOKEN");
  }

  const [encodedHeader, encodedPayload, encodedSignature] = parts;
  const header = parseJwtPart<JwtHeader>(encodedHeader);
  const claims = parseJwtPart<OidcIdTokenClaims>(encodedPayload);

  if (header.alg !== "RS256" || !header.kid) {
    throw new Error("OIDC_UNSUPPORTED_ID_TOKEN");
  }

  const publicKey = await getSigningKey(header.kid);
  const signatureIsValid = verify(
    "RSA-SHA256",
    Buffer.from(`${encodedHeader}.${encodedPayload}`),
    publicKey,
    Buffer.from(encodedSignature, "base64url"),
  );

  if (!signatureIsValid) {
    throw new Error("OIDC_INVALID_ID_TOKEN_SIGNATURE");
  }

  const now = Math.floor(Date.now() / 1000);
  const tolerance = 60;

  if (
    claims.iss !== configuration.issuer ||
    claims.tid !== configuration.tenantId ||
    !audienceMatches(claims.aud, configuration.clientId) ||
    !claims.exp ||
    claims.exp < now - tolerance ||
    (claims.nbf !== undefined && claims.nbf > now + tolerance) ||
    !safeOidcValueEquals(claims.nonce, expectedNonce)
  ) {
    throw new Error("OIDC_INVALID_ID_TOKEN_CLAIMS");
  }

  const email = (
    claims.preferred_username ||
    claims.email ||
    claims.upn ||
    ""
  )
    .trim()
    .toLowerCase();
  const providerId = claims.oid || claims.sub;

  if (!email || !providerId) {
    throw new Error("OIDC_MISSING_USER_IDENTITY");
  }

  return {
    email,
    name: claims.name?.trim() || null,
    provider: "microsoft" as const,
    providerId,
  };
}

export async function exchangeAndValidateOidcCode(input: {
  code: string;
  codeVerifier: string;
  expectedNonce: string;
}): Promise<ValidatedOidcProfile> {
  const idToken = await exchangeAuthorizationCode(
    input.code,
    input.codeVerifier,
  );

  return validateIdToken(idToken, input.expectedNonce);
}