import { ofetch } from 'ofetch'

// ---------------------------------------------------------------------------
// Bundled-app API layer (Capacitor build ONLY).
//
// The existing /mobile pages call the API with relative `/api/...` URLs via both `$fetch`/
// `useFetch` AND raw `window.fetch` (e.g. tasks.vue transcribe, Strapi uploads). In the bundled
// SPA there is no origin server, so this plugin transparently:
//   1. rebases relative `/api/...` → `${apiBase}/api/...`
//   2. injects `Authorization: Bearer` + `X-Logship-*` context headers (token auth)
//   3. rewrites the login call to the dedicated `/api/app-auth/login` endpoint and captures the
//      response (and the credentials) into the session store
//   4. keeps the session alive: two-tier recovery (refresh-token → silent credential re-login),
//      proactively on boot / resume / interval, and reactively on a 401.
//
// Why proactive matters here: the server CANNOT refresh for the app (it never receives the refresh
// token or the credentials — only the Bearer token + context headers), and several routes return
// their auth failure as a 200 body (errorHandlingHelper) rather than a clean 401. So reacting to
// 401s alone is not enough; if we let the access token lapse, pages just receive error payloads and
// "show nothing". Keeping the token fresh ahead of use is what makes the app feel "always logged in".
//
// It is gated on `config.public.apiBase`, which only exists in the app build → on the web build
// this plugin is also dropped by the `app:resolve` hook, so it is a guaranteed no-op there.
// ZERO edits to the mobile pages are required.
// ---------------------------------------------------------------------------

export default defineNuxtPlugin(async (nuxtApp) => {
  const config = useRuntimeConfig()
  const apiBase = config.public.apiBase as string | undefined
  if (!apiBase) return // web build / no app target → leave $fetch and window.fetch untouched

  const session = useAuthSession()
  await session.load()

  const origFetch: typeof fetch = globalThis.fetch.bind(globalThis)

  const isRelApi = (u: string) => u.startsWith('/api/')
  const isOurApi = (u: string) => isRelApi(u) || u.startsWith(`${apiBase}/api/`)
  const isAuthEndpoint = (u: string) => u.includes('/api/app-auth/')
  // signin.vue posts to /api/idempiere-auth/login; the app uses the dedicated token endpoint.
  const rewrite = (u: string) =>
    u.replace('/api/idempiere-auth/login', '/api/app-auth/login')
      .replace('/api/idempiere-auth/logout', '/api/app-auth/logout')
  const toAbsolute = (u: string) => (isRelApi(u) ? apiBase + u : u)

  const appHeaders = (): Record<string, string> => {
    const h: Record<string, string> = { ...session.contextHeaders() }
    const tk = session.token.value
    if (tk) h['Authorization'] = `Bearer ${tk}`
    return h
  }

  // --- session-dead handling: send the user to /signin instead of leaving them on a page that
  //     only throws errors. NEVER triggered on a network error (offline ≠ logged out). ----------
  let deadHandled = false
  const handleSessionDead = async () => {
    if (deadHandled) return
    deadHandled = true
    await session.clear()
    try {
      const router: any = (nuxtApp as any).$router
      const cur: string | undefined = router?.currentRoute?.value?.path
      if (router && cur && !cur.startsWith('/signin')) await router.push('/signin')
    } catch { /* ignore */ }
  }

  // --- single-flight, two-tier token recovery (uses the ORIGINAL fetch so it never re-enters the
  //     wrappers). `lastWasNetwork` distinguishes "server refused us" (→ sign out) from "couldn't
  //     reach the server" (→ keep the session; just offline). ------------------------------------
  let refreshing: Promise<boolean> | null = null
  let lastWasNetwork = false
  const doRefresh = (): Promise<boolean> => {
    if (!refreshing) {
      refreshing = (async () => {
        lastWasNetwork = false
        const ctx = session.context.value

        // tier 1 — exchange the refresh token for a fresh access token
        const rt = session.refreshToken.value
        if (rt) {
          try {
            const r = await origFetch(`${apiBase}/api/app-auth/token-refresh`, {
              method: 'POST',
              headers: { 'Content-Type': 'application/json', 'X-Logship-App': '1' },
              body: JSON.stringify({ refresh_token: rt, clientId: ctx.clientId, userId: ctx.userId }),
            })
            if (r.ok) {
              const d = await r.json()
              if (d?.token) { session.setTokens(d.token, d.refresh_token); deadHandled = false; return true }
            }
            // reachable but rejected → fall through to a credential re-login
          } catch { lastWasNetwork = true }
        }

        // tier 2 — silent re-login with the stored credentials (mirrors the web's auth/tokens
        // fallback in refreshTokenHelper). The X-Logship-* context headers make the server land
        // straight back on the same client/role/org/warehouse without a /select-role round-trip.
        const creds = session.getCredentials()
        if (creds) {
          try {
            const res = await origFetch(`${apiBase}/api/app-auth/login`, {
              method: 'POST',
              headers: { 'Content-Type': 'application/json', ...session.contextHeaders() },
              body: JSON.stringify({ userName: creds.userName, password: creds.password }),
            })
            if (res.ok) {
              const d = await res.json()
              if (d?.token) { session.setFromLogin(d); deadHandled = false; lastWasNetwork = false; return true }
              lastWasNetwork = false // server answered, credentials rejected
            } else {
              lastWasNetwork = false
            }
          } catch { lastWasNetwork = true }
        }
        return false
      })().finally(() => { refreshing = null })
    }
    return refreshing
  }

  // --- proactive keep-alive --------------------------------------------------------------------
  const canRecover = () => !!(session.refreshToken.value || session.getCredentials())

  // Decode the JWT `exp` (best-effort) so we only refresh when actually near expiry. If the token
  // is missing or its exp can't be read, we err on the side of refreshing (return true).
  const tokenExpiresSoon = (): boolean => {
    const tk = session.token.value
    if (!tk) return true
    try {
      const part = tk.split('.')[1]
      if (!part) return true
      const pad = part.length % 4
      const b64 = part.replace(/-/g, '+').replace(/_/g, '/') + (pad ? '='.repeat(4 - pad) : '')
      const payload = JSON.parse(atob(b64))
      if (!payload?.exp) return true
      return payload.exp * 1000 - Date.now() < 10 * 60 * 1000 // refresh when <10 min remain
    } catch { return true }
  }

  let lastRefreshAt = 0
  const ensureFreshToken = async (force = false) => {
    if (!canRecover()) return // nothing to refresh with → never touch the session
    if (!force && !tokenExpiresSoon()) return
    const now = Date.now()
    if (!force && now - lastRefreshAt < 60_000) return // small burst guard
    lastRefreshAt = now
    const ok = await doRefresh()
    if (!ok && !lastWasNetwork) await handleSessionDead()
  }

  // Refresh once at boot (if a session exists and its token is stale) and make the FIRST API calls
  // wait for it, so pages that fetch on mount use a fresh token instead of an expired one.
  let bootRefresh: Promise<any> = Promise.resolve()
  if (canRecover()) bootRefresh = ensureFreshToken().catch(() => {})

  // ---------- 1) ofetch instance backing $fetch / useFetch ----------
  const baseApi = ofetch.create({
    baseURL: apiBase,
    onRequest(ctx: any) {
      const u = String(ctx.request)
      if (isOurApi(u)) {
        ctx.options.headers = { ...(ctx.options.headers || {}), ...appHeaders() }
      }
    },
  })

  const $api = (async (request: any, opts: any = {}) => {
    let req = request
    if (typeof req === 'string') req = rewrite(req)
    // Gate the first data calls behind the boot refresh (auth endpoints are never gated).
    if (typeof req === 'string' && isOurApi(req) && !isAuthEndpoint(req)) {
      try { await bootRefresh } catch { /* ignore */ }
    }
    try {
      const data = await baseApi(req, opts)
      if (typeof req === 'string' && req.includes('/api/app-auth/login')) {
        session.setFromLogin(data)
        const b = opts?.body
        // Only persist credentials on a genuinely successful login (the endpoint returns 200 with
        // { status: 401 } and no token on wrong credentials — don't store those).
        if (data?.token && b && b.userName && b.password) session.setCredentials(b.userName, b.password)
        if (data?.token) deadHandled = false
      }
      if (typeof req === 'string' && req.includes('/api/app-auth/logout')) await session.clear()
      return data
    } catch (e: any) {
      if (e?.response?.status === 401 && typeof req === 'string' && isOurApi(req) && !isAuthEndpoint(req) && !opts._retry) {
        if (await doRefresh()) return await ($api as any)(req, { ...opts, _retry: true })
        if (!lastWasNetwork) await handleSessionDead()
      }
      throw e
    }
  }) as any
  // Preserve the ofetch surface used internally by Nuxt (useFetch may touch .raw/.create).
  $api.raw = (baseApi as any).raw
  $api.create = (baseApi as any).create
  globalThis.$fetch = $api
  ;(nuxtApp as any).$fetch = $api

  // ---------- 2) wrap window.fetch for raw fetch() calls ----------
  // Only relative `/api/...` requests are rebased + authed + refreshed. Absolute URLs (Strapi
  // media uploads) and `data:`/blob URLs pass straight through to the native WebView fetch, which
  // handles multipart `FormData`/`Blob` uploads natively.
  globalThis.fetch = (async (input: any, init: any = {}) => {
    const urlStr = typeof input === 'string' ? input : ''
    if (urlStr && isRelApi(urlStr)) {
      const url = toAbsolute(rewrite(urlStr))
      if (isOurApi(url) && !isAuthEndpoint(url)) { try { await bootRefresh } catch { /* ignore */ } }
      const headers = new Headers(init.headers || undefined)
      const ah = appHeaders()
      for (const k in ah) headers.set(k, ah[k])
      let res = await origFetch(url, { ...init, headers })
      if (res.status === 401 && !isAuthEndpoint(url) && !init._retry) {
        if (await doRefresh()) {
          const tk = session.token.value
          if (tk) headers.set('Authorization', `Bearer ${tk}`)
          res = await origFetch(url, { ...init, headers })
        } else if (!lastWasNetwork) {
          await handleSessionDead()
        }
      }
      if (url.includes('/api/app-auth/login') && res.ok) {
        try {
          const d = await res.clone().json()
          session.setFromLogin(d)
          // Only persist credentials on a genuinely successful login (200 + token).
          if (d?.token && init.body) {
            try { const b = JSON.parse(init.body); if (b?.userName && b?.password) session.setCredentials(b.userName, b.password) } catch { /* ignore */ }
          }
          if (d?.token) deadHandled = false
        } catch { /* ignore */ }
      }
      return res
    }
    return origFetch(input, init)
  }) as any

  // ---------- 3) keep-alive triggers (web events — no @capacitor/app native dependency) ----------
  // Fire when the WebView regains focus / becomes visible (typical "user reopens the app" moment)
  // and on a slow interval as a backstop. ensureFreshToken() is debounced + expiry-gated, so these
  // are cheap and only hit the network when the token is actually near expiry.
  if (typeof document !== 'undefined') {
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') ensureFreshToken()
    })
  }
  if (typeof window !== 'undefined') {
    window.addEventListener('focus', () => ensureFreshToken())
    setInterval(() => ensureFreshToken(), 10 * 60 * 1000)
  }
})
