import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react'
import { api, endpoints, getApiBase, setToken } from '../api'
import type { AuthUser } from '../types'

interface AuthState {
  user: AuthUser | null
  loading: boolean
  login: (email: string, password: string) => Promise<void>
  logout: () => Promise<void>
  hasPermission: (perm: string) => boolean
}

const AuthContext = createContext<AuthState>(null!)

export function useAuth() {
  return useContext(AuthContext)
}

// React 18+ StrictMode double-invokes effects in dev (mount → cleanup → mount),
// and the backend rotates the refresh cookie on every successful POST. Two
// concurrent bootstraps would race the rotation and leave the second one with
// a stale cookie. The module-scoped in-flight promise lets the second mount
// await the first's network round-trip instead of duplicating it. Risk 4 in
// AZ-510 spec.
let bootstrapInflight: Promise<AuthUser | null> | null = null

export function __resetBootstrapInflightForTests(): void {
  bootstrapInflight = null
}

// Dev-only escape hatch: `VITE_DEV_AUTH_BYPASS=true` skips the backend round
// trip and injects a fake admin user so the SPA renders authenticated. Lives
// in this file so the bypass is gated by the same effect that owns auth state;
// the import.meta.env check is also tree-shaken out of production builds when
// the flag is unset at build time.
const DEV_BYPASS_USER: AuthUser = {
  id: 'dev-bypass',
  email: 'dev@azaion.local',
  name: 'Dev Bypass',
  role: 'admin',
  // Permission codes are short identifiers checked via hasPermission(code) —
  // currently used by the Header to gate the nav tabs (FL, ANN, DATASET, ADM).
  permissions: ['FL', 'ANN', 'DATASET', 'ADM'],
}

async function runBootstrap(): Promise<AuthUser | null> {
  // Gated on import.meta.env.DEV so a leaked VITE_DEV_AUTH_BYPASS=true in a
  // production build cannot grant admin access. Vite tree-shakes the entire
  // branch when DEV is false at build time.
  if (import.meta.env.DEV && import.meta.env.VITE_DEV_AUTH_BYPASS === 'true') {
    setToken('dev-bypass-token')
    return DEV_BYPASS_USER
  }
  // POST refresh with credentials — the whole point of the consolidation. Goes
  // through fetch() directly (not api.post) because api.post does not thread
  // credentials:'include'; widening api.post would change CORS posture for
  // every authenticated callsite. Same pattern lives in api/client.ts:88 for
  // the 401-retry refresh path.
  const refreshRes = await fetch(getApiBase() + endpoints.admin.authRefresh(), {
    method: 'POST',
    credentials: 'include',
  })
  if (!refreshRes.ok) return null
  const refreshData = (await refreshRes.json()) as { token: string }
  setToken(refreshData.token)
  try {
    return await api.get<AuthUser>(endpoints.admin.usersMe())
  } catch (err) {
    // Refresh succeeded but /users/me failed — clear the bearer so an in-flight
    // re-render does not see (user: null) alongside an active accessToken
    // (Constraint #4 in spec).
    console.error('[AuthContext] Refresh succeeded but /users/me failed:', err)
    setToken(null)
    return null
  }
}

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<AuthUser | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    let cancelled = false
    const inflight =
      bootstrapInflight ??
      (bootstrapInflight = runBootstrap().finally(() => {
        bootstrapInflight = null
      }))
    inflight
      .then(result => {
        if (cancelled) return
        setUser(result)
        setLoading(false)
      })
      .catch(err => {
        // Network error on the POST refresh itself (the /users/me failure path
        // is handled inside runBootstrap and resolves to null). Reliability NFR
        // requires loading to flip to false on every failure path.
        console.error('[AuthContext] Bootstrap failed:', err)
        if (cancelled) return
        setToken(null)
        setUser(null)
        setLoading(false)
      })
    return () => {
      cancelled = true
    }
  }, [])

  const login = useCallback(async (email: string, password: string) => {
    const data = await api.post<{ token: string; user: AuthUser }>(endpoints.admin.authLogin(), { email, password })
    setToken(data.token)
    setUser(data.user)
  }, [])

  const logout = useCallback(async () => {
    try { await api.post(endpoints.admin.authLogout()) } catch {}
    setToken(null)
    setUser(null)
  }, [])

  const hasPermission = useCallback((perm: string) => {
    // `permissions` is required by the AuthUser type but the runtime payload
    // from `/users/me` may omit it (older backend builds, or test fixtures
    // returning the bare User shape). Treat missing as "no permissions" rather
    // than crashing the React tree.
    return user?.permissions?.includes(perm) ?? false
  }, [user])

  return (
    <AuthContext.Provider value={{ user, loading, login, logout, hasPermission }}>
      {children}
    </AuthContext.Provider>
  )
}
