import { z } from "zod";

const DEFAULT_SCOPES = ["openid", "email", "profile"] as const;
const DISCOVERY_TIMEOUT_MS = 10000;

const discoverySchema = z.object({
  issuer: z.string().url().optional(),
  authorization_endpoint: z.string().url().optional(),
  token_endpoint: z.string().url().optional(),
  userinfo_endpoint: z.string().url().optional(),
  jwks_uri: z.string().url().optional(),
  scopes_supported: z.array(z.string()).optional(),
});

export class OidcConfigError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "OidcConfigError";
  }
}

export type RawOidcConfig = {
  clientId?: string;
  clientSecret?: string;
  authorizationEndpoint?: string;
  tokenEndpoint?: string;
  jwksEndpoint?: string;
  userInfoEndpoint?: string;
  discoveryEndpoint?: string;
  scopes?: string[];
  pkce?: boolean;
  mapping?: ProviderMapping;
};

export type ProviderMapping = {
  id: string;
  email: string;
  emailVerified?: string;
  name?: string;
  image?: string;
  firstName?: string;
  lastName?: string;
};

export type NormalizedOidcConfig = {
  oidcConfig: {
    clientId?: string;
    clientSecret?: string;
    authorizationEndpoint: string;
    tokenEndpoint: string;
    jwksEndpoint?: string;
    userInfoEndpoint?: string;
    discoveryEndpoint: string;
    scopes: string[];
    pkce: boolean;
  };
  mapping: ProviderMapping;
};

type FetchFn = typeof fetch;

function cleanUrl(value: string | undefined, field: string): string | undefined {
  if (!value || typeof value !== "string") return undefined;
  const trimmed = value.trim();
  if (!trimmed) return undefined;
  try {
    return new URL(trimmed).toString();
  } catch {
    throw new OidcConfigError(`Invalid ${field} URL: ${value}`);
  }
}

function sanitizeScopes(scopes: string[] | undefined, fallback: readonly string[]): string[] {
  const candidates = Array.isArray(scopes) ? scopes : [];
  const sanitized = candidates
    .map(scope => scope?.trim())
    .filter((scope): scope is string => Boolean(scope));

  if (sanitized.length === 0) {
    return [...fallback];
  }

  return Array.from(new Set(sanitized));
}

async function fetchDiscoveryDocument(url: string, fetchFn: FetchFn): Promise<z.infer<typeof discoverySchema>> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT_MS);

  try {
    const response = await fetchFn(url, {
      signal: controller.signal,
      headers: { Accept: "application/json" },
    });

    if (!response.ok) {
      throw new OidcConfigError(`OIDC discovery request failed (${response.status} ${response.statusText})`);
    }

    let payload: unknown;
    try {
      payload = await response.json();
    } catch {
      throw new OidcConfigError("OIDC discovery response is not valid JSON");
    }

    const parsed = discoverySchema.parse(payload);
    if (!parsed.authorization_endpoint || !parsed.token_endpoint) {
      throw new OidcConfigError("OIDC discovery document is missing required endpoints");
    }
    return parsed;
  } catch (error) {
    if (error instanceof OidcConfigError) {
      throw error;
    }
    if (error instanceof Error && error.name === "AbortError") {
      throw new OidcConfigError(`OIDC discovery timed out after ${DISCOVERY_TIMEOUT_MS / 1000}s`);
    }
    throw new OidcConfigError(`Failed to fetch OIDC discovery document: ${error instanceof Error ? error.message : "unknown error"}`);
  } finally {
    clearTimeout(timeoutId);
  }
}

export async function normalizeOidcProviderConfig(
  issuer: string,
  rawConfig: RawOidcConfig,
  fetchFn: FetchFn = fetch,
): Promise<NormalizedOidcConfig> {
  if (!issuer || typeof issuer !== "string") {
    throw new OidcConfigError("Issuer is required");
  }

  let normalizedIssuer: string;
  try {
    const issuerUrl = new URL(issuer.trim());
    normalizedIssuer = issuerUrl.toString().replace(/\/$/, "");
  } catch {
    throw new OidcConfigError(`Invalid issuer URL: ${issuer}`);
  }

  const discoveryEndpoint = cleanUrl(
    rawConfig.discoveryEndpoint,
    "discovery endpoint",
  ) ?? `${normalizedIssuer}/.well-known/openid-configuration`;

  const authorizationEndpoint = cleanUrl(rawConfig.authorizationEndpoint, "authorization endpoint");
  const tokenEndpoint = cleanUrl(rawConfig.tokenEndpoint, "token endpoint");
  const jwksEndpoint = cleanUrl(rawConfig.jwksEndpoint, "JWKS endpoint");
  const userInfoEndpoint = cleanUrl(rawConfig.userInfoEndpoint, "userinfo endpoint");
  const providedScopes = Array.isArray(rawConfig.scopes) ? rawConfig.scopes : undefined;
  let scopes = sanitizeScopes(providedScopes, DEFAULT_SCOPES);

  const shouldFetchDiscovery =
    !authorizationEndpoint ||
    !tokenEndpoint ||
    !jwksEndpoint ||
    !userInfoEndpoint ||
    !providedScopes ||
    providedScopes.length === 0;

  let resolvedAuthorization = authorizationEndpoint;
  let resolvedToken = tokenEndpoint;
  let resolvedJwks = jwksEndpoint;
  let resolvedUserInfo = userInfoEndpoint;

  if (shouldFetchDiscovery) {
    const discovery = await fetchDiscoveryDocument(discoveryEndpoint, fetchFn);
    resolvedAuthorization = resolvedAuthorization ?? discovery.authorization_endpoint;
    resolvedToken = resolvedToken ?? discovery.token_endpoint;
    resolvedJwks = resolvedJwks ?? discovery.jwks_uri;
    resolvedUserInfo = resolvedUserInfo ?? discovery.userinfo_endpoint;
    if (!providedScopes || providedScopes.length === 0) {
      scopes = sanitizeScopes(discovery.scopes_supported, DEFAULT_SCOPES);
    }
  }

  if (!resolvedAuthorization || !resolvedToken) {
    throw new OidcConfigError("OIDC configuration must include authorization and token endpoints");
  }

  return {
    oidcConfig: {
      clientId: rawConfig.clientId,
      clientSecret: rawConfig.clientSecret,
      authorizationEndpoint: resolvedAuthorization,
      tokenEndpoint: resolvedToken,
      jwksEndpoint: resolvedJwks,
      userInfoEndpoint: resolvedUserInfo,
      discoveryEndpoint,
      scopes,
      pkce: rawConfig.pkce !== false,
    },
    mapping: rawConfig.mapping ?? {
      id: "sub",
      email: "email",
      emailVerified: "email_verified",
      name: "name",
      image: "picture",
    },
  };
}
