// Runtime configuration for the SPA — server URL + auth token. Every fetch
// call goes through fetchWithAuth/apiBase (used by api.ts) so the SPA works
// identically whether it's served same-origin (browser prod, Vite dev proxy)
// or cross-origin (Wails webview, remote access). See
// plans/2026-07-12-wails-desktop-app.md 0.2.

import { isOIDCConfigured, ensureToken, logout as oidcLogout } from './oidc'

export interface OikosConfig {
  apiUrl: string // e.g. "https://oikos.hubris.network", or "" for same-origin
  token?: string // bearer token for auth
  isDesktop?: boolean // true when running inside the Wails desktop app
}

declare global {
  interface Window {
    __OIKOS_CONFIG__?: OikosConfig
  }
}

let cfg: OikosConfig | undefined

export function initConfig(override?: OikosConfig) {
  cfg = override ?? window.__OIKOS_CONFIG__
  if (cfg?.token) {
    localStorage.setItem('oikos_token', cfg.token)
    if (cfg.apiUrl) localStorage.setItem('oikos_api_url', cfg.apiUrl)
  }
}

export function getConfig(): OikosConfig {
  if (!cfg) {
    const token = localStorage.getItem('oikos_token')
    const apiUrl = localStorage.getItem('oikos_api_url')
    if (token || apiUrl) {
      cfg = { apiUrl: apiUrl ?? '', token: token ?? undefined }
    }
  }
  return cfg ?? { apiUrl: '' }
}

export function setConfig(next: OikosConfig) {
  cfg = next
  if (next.token) localStorage.setItem('oikos_token', next.token)
  else localStorage.removeItem('oikos_token')
  if (next.apiUrl) localStorage.setItem('oikos_api_url', next.apiUrl)
  else localStorage.removeItem('oikos_api_url')
}

export function clearConfig() {
  cfg = { apiUrl: '' }
  localStorage.removeItem('oikos_token')
  localStorage.removeItem('oikos_api_url')
}

export function isConfigured(): boolean {
  return !!getConfig().token || isOIDCConfigured()
}

// Relative paths are used in dev (Vite proxy) and when the SPA shares an
// origin with the API server (Caddy reverse proxy). Absolute paths are used
// when the API server is on a different origin (Wails webview, remote access).
export function apiBase(path: string): string {
  const c = getConfig()
  if (!c.apiUrl) return path // relative — relies on same-origin or Vite proxy
  return `${c.apiUrl}${path}`
}

// Resolves the auth token for a request: OIDC takes precedence, then static.
// OIDC is async (may need to refresh an expired access_token); the static
// fallback is synchronous. Returns the header value or null.
async function resolveAuthHeader(): Promise<string | null> {
  // Try OIDC first. ensureToken() refreshes if the cached token is expired or
  // missing; if it returns a token we use it.
  if (isOIDCConfigured()) {
    const tok = await ensureToken()
    if (tok) return `Bearer ${tok}`
    // OIDC session exists but couldn't yield a usable token (e.g. expired
    // access_token with no refresh_token). Fall through to the static token
    // if one was configured — better than a blanket 401.
  }
  const c = getConfig()
  if (c.token) return `Bearer ${c.token}`
  return null
}

// ---- Auth fetch wrapper ----
// Prepends the API base URL (absolute when configured, relative when unset
// for the Vite dev proxy / same-origin prod) and adds the Authorization
// header. Used by every fetch call in api.ts.
//
// OIDC tokens are short-lived; this wrapper awaits ensureToken() so an
// expired access_token is refreshed before the request goes out, rather than
// 401ing on the wire. On a 401 we flush the OIDC session once so the next
// request can fall back to the static token (or re-prompt the operator).
export async function fetchWithAuth(path: string, opts?: RequestInit): Promise<Response> {
  const headers: Record<string, string> = {
    'Content-Type': 'application/json',
    ...((opts?.headers as Record<string, string>) ?? {})
  }
  const authH = await resolveAuthHeader()
  if (authH) {
    headers['Authorization'] = authH
  }

  const res = await fetch(apiBase(path), { ...opts, headers })

  // A 401 on a request we sent an Authorization header for means the token
  // the server just rejected is no longer valid. If OIDC is in use, clear it
  // so resolveAuthHeader() falls back to the static token next time (or the
  // operator gets re-prompted to log in). Don't loop: only one flush, and
  // only when we actually sent an Authorization header.
  if (res.status === 401 && authH && isOIDCConfigured()) {
    oidcLogout()
  }
  return res
}

// SSE path builder — EventSource doesn't take headers, so pass the token as
// a query parameter (the SSE handler's combinedAuth checks it alongside the
// Authorization header, only for this route). Async so the OIDC access token
// can be refreshed before the EventSource is constructed.
export async function sseUrl(path: string): Promise<string> {
  const url = apiBase(path)
  const c = getConfig()
  // Prefer a fresh OIDC token (refreshes if expired); fall back to the static token.
  let token: string | null = null
  if (isOIDCConfigured()) {
    token = await ensureToken()
  }
  if (!token) token = c.token ?? null
  if (!token) return url
  const sep = url.includes('?') ? '&' : '?'
  return `${url}${sep}token=${encodeURIComponent(token)}`
}
