import React, { useState, useEffect, useRef, useContext, useCallback } from 'react'
import { tokenStore } from './auth'
import { generatePKCEPair, generateState } from './auth'
import { readConfig } from './config'

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

export interface AuthState {
  token: string | null
  user: { name?: string } | null
  status: 'unauthenticated' | 'authenticated' | 'auth_error'
  error?: string
  /** True once a token has been set at least once this page-load (supports IV7). */
  hasBeenAuthenticated: boolean
}

export interface AuthContextValue {
  state: AuthState
  signIn(returnTo?: string): void
  signOut(): void
  reportAuthError(error: string): void
}

// ── PKCE pending state shape stored in localStorage ──────────────────────────

interface PendingAuth {
  verifier: string
  state: string
  returnTo: string
  expiresAt: number // ms epoch
}

const PENDING_KEY = 'lethe_auth_pending'

// ── ID token parsing (cosmetic name claim only) ───────────────────────────────

function parseNameFromToken(token: string): string | undefined {
  try {
    const parts = token.split('.')
    if (parts.length < 2) return undefined
    // base64url → base64 → decode
    const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/')
    const json = atob(payload)
    const claims = JSON.parse(json) as Record<string, unknown>
    if (typeof claims['name'] === 'string') {
      return claims['name']
    }
    return undefined
  } catch {
    // Malformed token — ignore; still treat as authenticated (token is what
    // middleware checks; name is purely cosmetic).
    return undefined
  }
}

// ── Context ───────────────────────────────────────────────────────────────────

export const AuthContext = React.createContext<AuthContextValue | null>(null)

/**
 * useAuth — typed accessor for AuthContextValue.
 * Throws if used outside AuthProvider.
 */
export function useAuth(): AuthContextValue {
  const ctx = useContext(AuthContext)
  if (ctx === null) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return ctx
}

// ── AuthProvider ──────────────────────────────────────────────────────────────

export function AuthProvider({ children }: { children: React.ReactNode }): React.JSX.Element {
  // Try reading config once at provider mount; if absent, surface a config-error card.
  const [configError, setConfigError] = React.useState<string | null>(null)
  const configRef = useRef<ReturnType<typeof readConfig> | null>(null)

  if (configRef.current === null && configError === null) {
    try {
      configRef.current = readConfig()
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e)
      // We can't call setState during render directly, so we schedule it.
      // But for the initial render we use a lazy-init approach:
      // configRef stays null and configError is set below.
      // Since this runs synchronously before first paint, use a flag instead.
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      ;(configRef as any)._error = msg
    }
  }

  // Initialise from tokenStore current value so SSR/hydration is consistent.
  const initialToken = tokenStore.get()

  const [state, setState] = useState<AuthState>(() => {
    if (initialToken != null) {
      const name = parseNameFromToken(initialToken)
      return {
        token: initialToken,
        user: { name },
        status: 'authenticated',
        hasBeenAuthenticated: true,
      }
    }
    return {
      token: null,
      user: null,
      status: 'unauthenticated',
      hasBeenAuthenticated: false,
    }
  })

  // hasBeenAuthenticated ref: stays true once first token received (IV7).
  const hasBeenAuthenticatedRef = useRef(state.hasBeenAuthenticated)

  useEffect(() => {
    // Propagate config error detected during render into React state.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const errFromRender = (configRef as any)._error as string | undefined
    if (errFromRender != null) {
      setConfigError(errFromRender)
      return
    }

    const unsubscribe = tokenStore.subscribe((token) => {
      if (token != null) {
        hasBeenAuthenticatedRef.current = true
        const name = parseNameFromToken(token)
        setState({
          token,
          user: { name },
          status: 'authenticated',
          hasBeenAuthenticated: true,
        })
      } else {
        setState({
          token: null,
          user: null,
          status: 'unauthenticated',
          hasBeenAuthenticated: hasBeenAuthenticatedRef.current,
        })
      }
    })

    return unsubscribe
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []) // mount-only

  const signIn = useCallback((returnTo?: string): void => {
    const cfg = configRef.current
    if (cfg === null) {
      // Config missing — cannot sign in; surface error.
      setState((prev) => ({
        ...prev,
        status: 'auth_error',
        error: 'lethe config missing',
      }))
      return
    }

    void (async () => {
      const { verifier, challenge } = await generatePKCEPair()
      const stateParam = generateState()
      const rt = returnTo ?? '/'

      const pending: PendingAuth = {
        verifier,
        state: stateParam,
        returnTo: rt,
        expiresAt: Date.now() + 5 * 60 * 1000, // 5 min TTL
      }
      localStorage.setItem(PENDING_KEY, JSON.stringify(pending))

      const origin = window.location.origin
      const params = new URLSearchParams({
        response_type:         'code',
        client_id:             cfg.clientId,
        redirect_uri:          `${origin}/auth/callback`,
        state:                 stateParam,
        code_challenge:        challenge,
        code_challenge_method: 'S256',
      })
      window.location.assign(`${cfg.issuer}/authorize?${params.toString()}`)
    })()
  }, [])

  const signOut = useCallback((): void => {
    tokenStore.set(null)
    // localStorage PKCE pending is left alone (harmless; will expire naturally).
    // IV4: no tokens in localStorage — nothing to clear there.
  }, [])

  const reportAuthError = useCallback((error: string): void => {
    setState((prev) => ({
      ...prev,
      status: 'auth_error',
      error,
      hasBeenAuthenticated: hasBeenAuthenticatedRef.current,
    }))
  }, [])

  if (configError != 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-config missing</div>
          <div className="muted">
            window.__LETHE_CONFIG__ was not injected. Reload or contact your administrator.
          </div>
        </div>
      </div>
    )
  }

  const value: AuthContextValue = { state, signIn, signOut, reportAuthError }

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  )
}
