// auth.callback.tsx — handles the OIDC authorization code callback.
//
// NOTE: This file uses raw `fetch` to POST to the OP token endpoint.
// This is the ONE allowed exception to the invariant "no raw fetch outside
// client.ts": the OP /token endpoint is a cross-origin request to the issuer
// host and is not part of the /api/v1/* surface that apiFetch serves.

import React, { useEffect, useRef } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { tokenStore, countCallbackFailures } from '../lib/auth'
import { useAuth } from '../lib/authContext'
import { readConfig } from '../lib/config'

// ── Types ─────────────────────────────────────────────────────────────────────

interface PendingAuth {
  verifier: string
  state: string
  returnTo: string
  expiresAt: number
}

const PENDING_KEY   = 'lethe_auth_pending'
const FAILURES_KEY  = 'lethe_auth_failures'

// ── Route ─────────────────────────────────────────────────────────────────────

export const Route = createFileRoute('/auth/callback')({
  validateSearch: (s: Record<string, unknown>) => ({
    code:              typeof s['code']              === 'string' ? s['code']              : undefined,
    state:             typeof s['state']             === 'string' ? s['state']             : undefined,
    error:             typeof s['error']             === 'string' ? s['error']             : undefined,
    error_description: typeof s['error_description'] === 'string' ? s['error_description'] : undefined,
  }),
  component: CallbackRoute,
})

// ── Component ─────────────────────────────────────────────────────────────────

function CallbackRoute(): React.JSX.Element {
  const navigate = useNavigate()
  const { reportAuthError } = useAuth()
  const search = Route.useSearch()

  // Prevent double-execution in React StrictMode double-invoke.
  const ranRef = useRef(false)

  // blocked state: once set, render "please reload" card with no retry.
  const [blocked, setBlocked] = React.useState(false)
  const [authError, setAuthError] = React.useState<string | null>(null)

  useEffect(() => {
    if (ranRef.current) return
    ranRef.current = true

    void (async () => {
      // ── Helper: record a failure and check the anti-loop guard ──────────────
      function recordFailureAndCheck(): boolean {
        const raw = localStorage.getItem(FAILURES_KEY)
        let log: number[] = []
        try {
          if (raw != null) {
            log = JSON.parse(raw) as number[]
            if (!Array.isArray(log)) log = []
          }
        } catch {
          log = []
        }
        log.push(Date.now())
        localStorage.setItem(FAILURES_KEY, JSON.stringify(log))

        const { blocked: isBlocked } = countCallbackFailures(Date.now(), log)
        return isBlocked
      }

      // ── Helper: fail with a message ─────────────────────────────────────────
      function fail(msg: string): void {
        const isBlocked = recordFailureAndCheck()
        reportAuthError(msg)
        setAuthError(msg)
        if (isBlocked) {
          setBlocked(true)
        }
      }

      // ── 0. Check for OP-reported error in the callback URL ──────────────────
      if (search.error != null) {
        const desc = search.error_description ?? search.error
        fail(`Authorization error: ${desc}`)
        return
      }

      // ── 1. Validate that code and state are present ─────────────────────────
      if (search.code == null || search.state == null) {
        fail('Missing code or state in callback')
        return
      }

      // ── 2. Read and validate the pending auth entry from localStorage ────────
      const raw = localStorage.getItem(PENDING_KEY)
      if (raw == null) {
        fail('No pending authorization found (state may have expired)')
        return
      }

      let pending: PendingAuth
      try {
        pending = JSON.parse(raw) as PendingAuth
      } catch {
        fail('Corrupted pending authorization state')
        return
      }

      // State parameter must match.
      if (pending.state !== search.state) {
        fail('State mismatch — possible CSRF')
        return
      }

      // TTL check.
      if (Date.now() > pending.expiresAt) {
        fail('Authorization request expired — please try again')
        return
      }

      // Consume the pending entry immediately (single-use).
      localStorage.removeItem(PENDING_KEY)

      // ── 3. Read config ──────────────────────────────────────────────────────
      let cfg: ReturnType<typeof readConfig>
      try {
        cfg = readConfig()
      } catch (e) {
        fail('Auth config missing during token exchange')
        return
      }

      // ── 4. Exchange code for tokens via POST to OP /token ───────────────────
      const origin = window.location.origin
      const body = new URLSearchParams({
        grant_type:    'authorization_code',
        code:          search.code,
        code_verifier: pending.verifier,
        redirect_uri:  `${origin}/auth/callback`,
        client_id:     cfg.clientId,
      })

      let resp: Response
      try {
        resp = await fetch(`${cfg.issuer}/token`, {
          method:  'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body:    body.toString(),
        })
      } catch (e) {
        const msg = e instanceof Error ? e.message : String(e)
        fail(`Token endpoint unreachable: ${msg}`)
        return
      }

      if (!resp.ok) {
        let detail = resp.statusText
        try {
          const errBody = await resp.json() as Record<string, unknown>
          if (typeof errBody['error_description'] === 'string') {
            detail = errBody['error_description']
          } else if (typeof errBody['error'] === 'string') {
            detail = errBody['error']
          }
        } catch {
          // ignore JSON parse failure; use status text
        }
        fail(`Token exchange failed (${resp.status}): ${detail}`)
        return
      }

      // ── 5. Parse token response ─────────────────────────────────────────────
      let tokenResp: Record<string, unknown>
      try {
        tokenResp = await resp.json() as Record<string, unknown>
      } catch {
        fail('Token endpoint returned invalid JSON')
        return
      }

      const accessToken = tokenResp['access_token']
      if (typeof accessToken !== 'string' || accessToken === '') {
        fail('Token endpoint did not return an access_token')
        return
      }

      // ── 6. Store token and navigate to returnTo ─────────────────────────────
      // Clear anti-loop failures log on success.
      localStorage.removeItem(FAILURES_KEY)

      tokenStore.set(accessToken)
      await navigate({ to: pending.returnTo })
    })()
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []) // mount-only

  // ── Render ──────────────────────────────────────────────────────────────────

  if (blocked) {
    return (
      <div
        className="body body-pad"
        style={{ display: 'flex', justifyContent: 'center', paddingTop: 60 }}
      >
        <div className="card" style={{ padding: '24px 32px', textAlign: 'center' }}>
          <div className="uppercase-mono" style={{ marginBottom: 8 }}>sign-in blocked</div>
          <div className="muted" style={{ marginBottom: 16 }}>
            Too many failed sign-in attempts. Please reload the page to try again.
          </div>
        </div>
      </div>
    )
  }

  if (authError != null) {
    return (
      <div
        className="body body-pad"
        style={{ display: 'flex', justifyContent: 'center', paddingTop: 60 }}
      >
        <div className="card" style={{ padding: '24px 32px', textAlign: 'center' }}>
          <div className="uppercase-mono" style={{ marginBottom: 8 }}>auth error</div>
          <div className="muted">{authError}</div>
        </div>
      </div>
    )
  }

  // Default: processing the callback.
  return (
    <div
      className="body body-pad"
      style={{ display: 'flex', justifyContent: 'center', paddingTop: 60 }}
    >
      <div className="card" style={{ padding: '24px 32px', textAlign: 'center' }}>
        <div className="uppercase-mono" style={{ marginBottom: 8 }}>completing sign-in…</div>
        <div className="muted">Please wait.</div>
      </div>
    </div>
  )
}
