/**
 * Native OIDC Authorization Code + PKCE flow for the Tauri shells.
 *
 * Replaces the server-side `/api/auth/*` routes (httpOnly cookies) on mobile:
 * - opens the system browser (ASWebAuthenticationSession on iOS / Custom Tabs
 *   on Android) to the Authentik authorize endpoint,
 * - receives the `ulti<app>://oauth/callback` redirect via the deep-link plugin
 *   (forwarded as the `ulti://deep-link` window event by the Rust shell),
 * - exchanges the code for tokens against a **public** PKCE client (no secret),
 * - persists the session in the shared OS secure store (cross-app SSO).
 */
import { createPkcePair, randomString } from "@/lib/auth/pkce"
import { platformUserFromToken } from "@/lib/auth/jwt-claims"
import { getRuntimeConfig } from "@/lib/runtime-config"
import { isTauriRuntime } from "@/lib/platform"
import { listen } from "@/lib/native/bridge"
import {
  clearSession,
  readSession,
  writeSession,
  type NativeSession,
} from "@/lib/native/secure-store"

const PENDING_KEY = "ulti-native-oauth-pending"
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000

type PendingAuth = {
  verifier: string
  state: string
  returnTo: string
}

type TokenResponse = {
  access_token?: string
  refresh_token?: string
  expires_in?: number
  id_token?: string
}

function savePending(p: PendingAuth) {
  sessionStorage.setItem(PENDING_KEY, JSON.stringify(p))
}

function takePending(): PendingAuth | null {
  const raw = sessionStorage.getItem(PENDING_KEY)
  if (!raw) return null
  sessionStorage.removeItem(PENDING_KEY)
  try {
    return JSON.parse(raw) as PendingAuth
  } catch {
    return null
  }
}

async function openExternal(url: string) {
  if (isTauriRuntime()) {
    try {
      const opener = await import("@tauri-apps/plugin-opener")
      await opener.openUrl(url)
      return
    } catch {
      /* fall through to window.open */
    }
  }
  window.open(url, "_blank", "noopener")
}

function buildSession(tokens: TokenResponse): NativeSession {
  const bearer = tokens.id_token ?? tokens.access_token
  if (!bearer) throw new Error("no_id_token")
  const expiresAt = Date.now() + (tokens.expires_in ?? 3600) * 1000
  return {
    accessToken: bearer,
    refreshToken: tokens.refresh_token ?? null,
    expiresAt,
    user: platformUserFromToken(bearer),
  }
}

async function exchangeCode(code: string, verifier: string): Promise<NativeSession> {
  const cfg = getRuntimeConfig()
  if (!cfg) throw new Error("no_runtime_config")
  const body = new URLSearchParams({
    grant_type: "authorization_code",
    client_id: cfg.oidc.clientId,
    code,
    redirect_uri: cfg.oidc.redirectUri,
    code_verifier: verifier,
  })
  const res = await fetch(cfg.oidc.tokenEndpoint, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body,
  })
  if (!res.ok) {
    throw new Error(`token_exchange_failed:${res.status}`)
  }
  const session = buildSession((await res.json()) as TokenResponse)
  await writeSession(session)
  return session
}

/** Parse `code`/`state` out of a returned `ulti<app>://oauth/callback?...` URL. */
function parseCallback(url: string): { code?: string; state?: string; error?: string } {
  try {
    const u = new URL(url)
    return {
      code: u.searchParams.get("code") ?? undefined,
      state: u.searchParams.get("state") ?? undefined,
      error: u.searchParams.get("error") ?? undefined,
    }
  } catch {
    return {}
  }
}

/**
 * Run the full native login. Resolves with the new session once the deep-link
 * callback is received and the code is exchanged.
 */
export async function nativeStartLogin(options?: {
  returnTo?: string
  prompt?: string
}): Promise<NativeSession> {
  const cfg = getRuntimeConfig()
  if (!cfg) throw new Error("no_runtime_config")

  const { verifier, challenge } = await createPkcePair()
  const state = randomString(16)
  const returnTo = options?.returnTo ?? "/mail/inbox"
  savePending({ verifier, state, returnTo })

  const params = new URLSearchParams({
    client_id: cfg.oidc.clientId,
    redirect_uri: cfg.oidc.redirectUri,
    response_type: "code",
    scope: "openid profile email offline_access",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
    prompt: options?.prompt ?? "select_account",
  })

  const authorizeUrl = `${cfg.oidc.authorizationEndpoint}?${params.toString()}`

  return new Promise<NativeSession>((resolve, reject) => {
    let settled = false
    let unlisten: (() => void) | null = null

    const cleanup = () => {
      settled = true
      if (unlisten) unlisten()
      clearTimeout(timer)
    }

    const timer = setTimeout(() => {
      if (settled) return
      cleanup()
      reject(new Error("login_timeout"))
    }, LOGIN_TIMEOUT_MS)

    const handleUrl = async (rawUrls: unknown) => {
      if (settled) return
      const urls = Array.isArray(rawUrls) ? rawUrls : [rawUrls]
      for (const raw of urls) {
        if (typeof raw !== "string") continue
        if (!raw.includes("oauth/callback")) continue
        const { code, state: returnedState, error } = parseCallback(raw)
        const pending = takePending()
        if (error) {
          cleanup()
          reject(new Error(error))
          return
        }
        if (!code || !pending || returnedState !== pending.state) {
          cleanup()
          reject(new Error("invalid_state"))
          return
        }
        try {
          const session = await exchangeCode(code, pending.verifier)
          cleanup()
          resolve(session)
        } catch (err) {
          cleanup()
          reject(err instanceof Error ? err : new Error("exchange_failed"))
        }
        return
      }
    }

    void (async () => {
      unlisten = await listen("ulti://deep-link", (payload) => {
        void handleUrl(payload)
      })
      await openExternal(authorizeUrl)
    })().catch((err) => {
      if (settled) return
      cleanup()
      reject(err instanceof Error ? err : new Error("open_failed"))
    })
  })
}

/** Refresh the session using the stored refresh token (public client). */
export async function nativeRefresh(): Promise<NativeSession | null> {
  const cfg = getRuntimeConfig()
  const session = await readSession()
  if (!cfg || !session?.refreshToken) return null
  const body = new URLSearchParams({
    grant_type: "refresh_token",
    client_id: cfg.oidc.clientId,
    refresh_token: session.refreshToken,
  })
  try {
    const res = await fetch(cfg.oidc.tokenEndpoint, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body,
    })
    if (!res.ok) return null
    const tokens = (await res.json()) as TokenResponse
    const next = buildSession({
      ...tokens,
      refresh_token: tokens.refresh_token ?? session.refreshToken,
    })
    await writeSession(next)
    return next
  } catch {
    return null
  }
}

/** Clear the native session (and end the Authentik session in the browser). */
export async function nativeLogout(): Promise<void> {
  const cfg = getRuntimeConfig()
  await clearSession()
  if (cfg?.oidc.endSessionEndpoint) {
    try {
      await openExternal(cfg.oidc.endSessionEndpoint)
    } catch {
      /* best effort */
    }
  }
}
