/** OIDC settings for local dev (Authentik blueprints in ulti-backend). */

function trimSlash(url: string) {
  return url.endsWith("/") ? url : `${url}/`
}

/** Public app origin for OAuth redirects and post-login navigation (never 0.0.0.0). */
export function getAppOrigin(): string {
  const raw = (
    process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3004"
  ).replace(/\/$/, "")

  try {
    const url = new URL(raw)
    if (url.hostname === "0.0.0.0") {
      url.hostname = "localhost"
    }
    return url.origin
  } catch {
    return "http://localhost:3004"
  }
}

/** Base URL Authentik (sans segment application OIDC). */
export function getAuthentikBase(): string {
  const issuer = trimSlash(
    process.env.NEXT_PUBLIC_OIDC_ISSUER ??
      "http://localhost/auth/application/o/ulti/"
  )
  return issuer.replace(/application\/o\/[^/]+\/?$/, "")
}

export function getAuthentikEnrollmentUrl(): string {
  return `${getAuthentikBase()}if/flow/ulti-enrollment/`
}

type OidcDiscovery = {
  authorization_endpoint: string
  token_endpoint: string
  end_session_endpoint?: string
}

type OidcConfig = {
  issuer: string
  clientId: string
  appUrl: string
  redirectUri: string
  authorizationEndpoint: string
  tokenEndpoint: string
  endSessionEndpoint: string
}

let discoveryCache: {
  discoveryIssuer: string
  doc: OidcDiscovery
  at: number
} | null = null
const DISCOVERY_TTL_MS = 5 * 60 * 1000

/** Internal origin for server-side OIDC calls (Docker: http://nginx, dev host: http://127.0.0.1). */
function getOidcInternalOrigin(): string | null {
  const raw =
    process.env.OIDC_DISCOVERY_ORIGIN?.trim() ||
    process.env.ULTI_PROXY_ORIGIN?.trim()
  if (!raw) return null
  try {
    return new URL(raw.endsWith("/") ? raw : `${raw}/`).origin
  } catch {
    return null
  }
}

function getOidcPublicOrigin(): string {
  try {
    return new URL(getPublicOidcConfig().issuer).origin
  } catch {
    return "http://localhost"
  }
}

function issuerWithOrigin(issuer: string, origin: string): string {
  try {
    const parsed = new URL(issuer)
    return `${origin}${parsed.pathname}`
  } catch {
    return issuer
  }
}

function rewriteOrigin(url: string, origin: string): string {
  try {
    const parsed = new URL(url)
    const target = new URL(origin.endsWith("/") ? origin : `${origin}/`)
    parsed.protocol = target.protocol
    parsed.hostname = target.hostname
    parsed.port = target.port
    return parsed.toString()
  } catch {
    return url
  }
}

/** Browser redirects must use the public Authentik URL, not Docker-internal hostnames. */
function toPublicEndpoint(
  endpoint: string,
  internalOrigin: string | null,
  publicOrigin: string
): string {
  if (!internalOrigin || internalOrigin === publicOrigin) return endpoint
  return rewriteOrigin(endpoint, publicOrigin)
}

/**
 * When token exchange hits Docker-internal nginx, Authentik must still emit the public
 * issuer (localhost) or ultid's ID token verifier rejects the JWT.
 */
export function oidcServerFetchHeaders(): Record<string, string> {
  const internalOrigin = getOidcInternalOrigin()
  const publicOrigin = getOidcPublicOrigin()
  if (!internalOrigin || internalOrigin === publicOrigin) return {}
  try {
    const publicUrl = new URL(publicOrigin)
    const host = publicUrl.host
    return {
      Host: host,
      "X-Forwarded-Host": host,
      "X-Forwarded-Proto": publicUrl.protocol.replace(":", ""),
    }
  } catch {
    return {}
  }
}

/** Server-side token/logout calls must not use browser-facing localhost from inside Docker. */
function toServerEndpoint(
  endpoint: string,
  internalOrigin: string | null,
  publicOrigin: string
): string {
  if (!internalOrigin || internalOrigin === publicOrigin) return endpoint
  return rewriteOrigin(endpoint, internalOrigin)
}

export function getPublicOidcConfig(): OidcConfig {
  const issuer = trimSlash(
    process.env.NEXT_PUBLIC_OIDC_ISSUER ??
      "http://localhost/auth/application/o/ulti/"
  )
  const clientId =
    process.env.NEXT_PUBLIC_OIDC_CLIENT_ID ?? "ulti-backend"
  const appUrl = getAppOrigin()

  return {
    issuer,
    clientId,
    appUrl,
    redirectUri: `${appUrl}/api/auth/callback`,
    authorizationEndpoint: "",
    tokenEndpoint: "",
    endSessionEndpoint: `${issuer}end-session/`,
  }
}

/** Resolve authorize/token URLs from issuer discovery (Authentik uses shared /o/ endpoints). */
export async function resolveOidcConfig(): Promise<OidcConfig> {
  const base = getPublicOidcConfig()
  const internalOrigin = getOidcInternalOrigin()
  const discoveryIssuer = internalOrigin
    ? issuerWithOrigin(base.issuer, internalOrigin)
    : base.issuer
  const now = Date.now()
  if (
    discoveryCache &&
    discoveryCache.discoveryIssuer === discoveryIssuer &&
    now - discoveryCache.at < DISCOVERY_TTL_MS
  ) {
    return applyDiscovery(base, discoveryCache.doc, internalOrigin)
  }

  const res = await fetch(
    `${discoveryIssuer}.well-known/openid-configuration`,
    { next: { revalidate: 300 } }
  )
  if (!res.ok) {
    throw new Error(
      `OIDC discovery failed (${res.status}) for ${discoveryIssuer}`
    )
  }
  const doc = (await res.json()) as OidcDiscovery
  discoveryCache = { discoveryIssuer, doc, at: now }
  return applyDiscovery(base, doc, internalOrigin)
}

function applyDiscovery(
  base: OidcConfig,
  doc: OidcDiscovery,
  internalOrigin: string | null
): OidcConfig {
  const publicOrigin = getOidcPublicOrigin()
  return {
    ...base,
    authorizationEndpoint: toPublicEndpoint(
      doc.authorization_endpoint,
      internalOrigin,
      publicOrigin
    ),
    tokenEndpoint: toServerEndpoint(
      doc.token_endpoint,
      internalOrigin,
      publicOrigin
    ),
    endSessionEndpoint: toPublicEndpoint(
      doc.end_session_endpoint ?? base.endSessionEndpoint,
      internalOrigin,
      publicOrigin
    ),
  }
}

export async function resolveServerOidcConfig(): Promise<
  OidcConfig & { clientSecret: string }
> {
  const pub = await resolveOidcConfig()
  const clientSecret = process.env.OIDC_CLIENT_SECRET ?? "changeme"
  return { ...pub, clientSecret }
}

/** OIDC login enabled unless explicitly disabled (default: on for local Authentik stack). */
export function isOidcConfigured() {
  return process.env.NEXT_PUBLIC_OIDC_DISABLED !== "true"
}
