import { apiBase, getConfig } from './config'

interface OIDCConfig {
  issuer: string
  client_id: string
  authorization_endpoint: string
}

interface TokenResponse {
  access_token: string
  token_type: string
  expires_in?: number
  refresh_token?: string
  id_token?: string
}

interface OIDCState {
  config: OIDCConfig | null
  accessToken: string | null
  expiresAt: number | null // epoch ms when access_token expires, or null if unknown
  refreshToken: string | null
  user: string | null
  refreshing: Promise<string | null> | null
}

const SESSION_KEY = 'oidc_access_token'
const EXPIRES_KEY = 'oidc_expires_at'
const REFRESH_KEY = 'oidc_refresh_token'
const USER_KEY = 'oidc_user'
const PKCE_KEY = 'oidc_pkce_verifier'
const STATE_KEY = 'oidc_state'

// Skew margin: treat a token as expired this many ms before its real exp,
// so a refresh kicks in before a request races the wire and 401s.
const EXPIRY_SKEW_MS = 30_000

let state: OIDCState = {
  config: null,
  accessToken: sessionStorage.getItem(SESSION_KEY),
  expiresAt: Number(sessionStorage.getItem(EXPIRES_KEY)) || null,
  refreshToken: localStorage.getItem(REFRESH_KEY),
  user: localStorage.getItem(USER_KEY),
  refreshing: null
}

async function fetchConfig(): Promise<OIDCConfig | null> {
  try {
    const resp = await fetch(apiBase('/api/v1/auth/oidc-config'))
    if (!resp.ok) return null
    const cfg: OIDCConfig = await resp.json()
    state.config = cfg
    return cfg
  } catch {
    return null
  }
}

function base64URLEncode(buf: ArrayBuffer): string {
  return btoa(String.fromCharCode(...new Uint8Array(buf)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '')
}

function generateRandom(len: number): string {
  const arr = new Uint8Array(len)
  crypto.getRandomValues(arr)
  return base64URLEncode(arr)
}

async function sha256(plain: string): Promise<ArrayBuffer> {
  return crypto.subtle.digest('SHA-256', new TextEncoder().encode(plain))
}

export async function startLogin(): Promise<void> {
  const cfg = state.config ?? (await fetchConfig())
  if (!cfg) throw new Error('OIDC not configured on server')

  const codeVerifier = generateRandom(64)
  const challengeBuf = await sha256(codeVerifier)
  const codeChallenge = base64URLEncode(challengeBuf)
  const oidcState = generateRandom(32)

  sessionStorage.setItem(PKCE_KEY, codeVerifier)
  sessionStorage.setItem(STATE_KEY, oidcState)

  const isDesktop = new URLSearchParams(location.search).has('desktop')

  let redirectURI: string
  let stateParam: string

  if (isDesktop) {
    const c = getConfig()
    redirectURI = (c.apiUrl || location.origin).replace(/\/$/, '') + '/oidc-callback'
    stateParam = oidcState + '.' + codeVerifier
  } else {
    redirectURI = (location.origin + location.pathname).replace(/\/$/, '')
    stateParam = oidcState
  }

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: cfg.client_id,
    redirect_uri: redirectURI,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    state: stateParam,
    scope: 'openid profile email'
  })

  if (isDesktop) {
    const apiUrl = getConfig().apiUrl || ''
    const ret = encodeURIComponent(
      location.origin +
        location.pathname.replace(/\/$/, '') +
        '?desktop=1&apiUrl=' +
        encodeURIComponent(apiUrl)
    )
    location.href = `http://127.0.0.1:18901/oidc/start?apiUrl=${encodeURIComponent(apiUrl)}&ret=${ret}`
    return
  }

  location.href = `${cfg.authorization_endpoint.replace(/\/$/, '')}/?${params}`
}

export async function handleCallback(code: string, returnedState: string): Promise<boolean> {
  const verifier = sessionStorage.getItem(PKCE_KEY)
  const savedState = sessionStorage.getItem(STATE_KEY)
  sessionStorage.removeItem(PKCE_KEY)
  sessionStorage.removeItem(STATE_KEY)

  if (!verifier || savedState !== returnedState) return false

  const cfg = state.config ?? (await fetchConfig())
  if (!cfg) return false

  const redirectURI = (location.origin + location.pathname).replace(/\/$/, '')

  try {
    const resp = await fetch(apiBase('/api/v1/auth/oidc-token'), {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'authorization_code',
        code,
        code_verifier: verifier,
        redirect_uri: redirectURI
      })
    })

    if (!resp.ok) return false

    const tokens: TokenResponse = await resp.json()
    if (!tokens.access_token) return false

    storeTokens(tokens)

    if (tokens.id_token) {
      const user = parseIDTokenUser(tokens.id_token)
      if (user) {
        state.user = user
        localStorage.setItem(USER_KEY, user)
      }
    }

    return true
  } catch {
    return false
  }
}

function parseIDTokenUser(idToken: string): string | null {
  try {
    const payload = idToken.split('.')[1]
    const claims = JSON.parse(atob(payload))
    return claims.preferred_username || claims.email || claims.sub || null
  } catch {
    return null
  }
}

function storeTokens(tokens: TokenResponse) {
  state.accessToken = tokens.access_token

  // Track expiry so getToken()/ensureToken() can refresh proactively. Default
  // to 5 min if the provider omits expires_in — a safe lower bound that keeps
  // refresh on a sane cadence rather than treating the token as never-expiring.
  const ttl = tokens.expires_in ?? 300
  state.expiresAt = Date.now() + ttl * 1000

  sessionStorage.setItem(SESSION_KEY, tokens.access_token)
  sessionStorage.setItem(EXPIRES_KEY, String(state.expiresAt))

  if (tokens.refresh_token) {
    state.refreshToken = tokens.refresh_token
    localStorage.setItem(REFRESH_KEY, tokens.refresh_token)
  }
}

export function getToken(): string | null {
  // Treat a token whose exp we never recorded (e.g. a pre-fix login) as
  // usable once: if it's still valid server-side it'll pass; if not, the
  // 401 handler in fetchWithAuth will flush it and trigger refresh.
  if (state.expiresAt !== null && Date.now() >= state.expiresAt - EXPIRY_SKEW_MS) {
    return null
  }
  return state.accessToken
}

export function getUser(): string | null {
  return state.user
}

export function isOIDCAvailable(): boolean {
  return !!(state.accessToken || state.refreshToken)
}

export async function ensureToken(): Promise<string | null> {
  // getToken() returns null when the token is missing OR expired-but-present.
  // Both cases should trigger a refresh if we have a refresh_token.
  const tok = getToken()
  if (tok) return tok

  if (state.refreshToken) {
    return refreshAccessToken()
  }

  return null
}

async function refreshAccessToken(): Promise<string | null> {
  if (state.refreshing) return state.refreshing

  const cfg = state.config ?? (await fetchConfig())
  if (!cfg || !state.refreshToken) return null

  state.refreshing = (async () => {
    try {
      const resp = await fetch(apiBase('/api/v1/auth/oidc-token'), {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          grant_type: 'refresh_token',
          refresh_token: state.refreshToken
        })
      })

      if (!resp.ok) {
        clearTokens()
        return null
      }

      const tokens: TokenResponse = await resp.json()
      if (!tokens.access_token) {
        clearTokens()
        return null
      }

      storeTokens(tokens)
      return tokens.access_token
    } catch {
      clearTokens()
      return null
    } finally {
      state.refreshing = null
    }
  })()

  return state.refreshing
}

function clearTokens() {
  state.accessToken = null
  state.expiresAt = null
  state.refreshToken = null
  state.user = null
  sessionStorage.removeItem(SESSION_KEY)
  sessionStorage.removeItem(EXPIRES_KEY)
  localStorage.removeItem(REFRESH_KEY)
  localStorage.removeItem(USER_KEY)
}

export function logout(): void {
  clearTokens()
}

export function isOIDCConfigured(): boolean {
  // A session counts as configured if there's a refresh_token (can recover an
  // expired access_token) or a still-valid access_token. An expired access
  // token with no refresh_token means we'd have to re-login, so don't claim
  // OIDC is configured in that state — let the static token (if any) take over.
  if (state.refreshToken) return true
  return getToken() !== null
}

export async function initOIDC(): Promise<boolean> {
  // Use ensureToken so an expired-but-present access_token (e.g. page reload
  // mid-session) triggers a refresh instead of being returned as-is.
  if (state.refreshToken || state.accessToken) {
    const token = await ensureToken()
    return token !== null
  }

  return false
}

export function hasPendingCallback(): boolean {
  const params = new URLSearchParams(location.search)
  return params.has('code') && params.has('state')
}

export async function processPendingCallback(): Promise<boolean> {
  const params = new URLSearchParams(location.search)
  const code = params.get('code')
  const oidcState = params.get('state')

  if (!code || !oidcState) return false

  const ok = await handleCallback(code, oidcState)

  const url = new URL(location.href)
  url.searchParams.delete('code')
  url.searchParams.delete('state')
  history.replaceState(null, '', url.toString())

  return ok
}
