import { defu } from 'defu'
import { createAuthClient } from 'better-auth/client'
import { adminClient, magicLinkClient, organizationClient } from 'better-auth/client/plugins'
import { passkeyClient } from '@better-auth/passkey/client'
import type { AuthClient, AuthSession, AuthUser } from '../types/auth'
import { useCapacitor } from './useCapacitor'
import { useApiClient } from './useApiClient'

interface RuntimeAuthConfig {
  redirectUserTo: string
  redirectGuestTo: string
}

// Lazily create a single client instance on the browser only
// Note: Plugins are conditionally loaded at runtime, so TypeScript can't know
// which plugins are available. We use AuthClient type which includes all possible plugins.
let clientSingleton: AuthClient | null = null

// Track the current session fetch promise at module level so concurrent calls
// (e.g., from login page and middleware) can await the same promise
let sessionFetchPromise: Promise<{ session: AuthSession | null, user: AuthUser | null }> | null = null

function getClient() {
  if (!import.meta.client) return null
  if (clientSingleton) return clientSingleton

  const config = useRuntimeConfig()
  const { isCapacitor, baseURL, getAuthToken } = useCapacitor()

  // Determine base URL for Better Auth
  const authBaseURL = baseURL.value
    ? baseURL.value // Capacitor: use the configured backend URL
    : (typeof window !== 'undefined'
        ? window.location.origin // Web: use current origin
        : (config.public.betterAuthUrl || 'http://localhost:3000')) // SSR fallback

  // Build plugins array conditionally based on saas config
  const plugins: Array<
    | ReturnType<typeof adminClient>
    | ReturnType<typeof passkeyClient>
    | ReturnType<typeof magicLinkClient>
    | ReturnType<typeof organizationClient>
  > = [
    adminClient(),
    passkeyClient()
  ]

  // Add magicLink plugin if enabled
  if (config.public.saas?.enableMagicLink) {
    plugins.push(magicLinkClient())
  }

  // Add organization plugin if enabled
  if (config.public.saas?.enableOrganizations) {
    plugins.push(organizationClient())
  }

  clientSingleton = createAuthClient({
    baseURL: authBaseURL,
    plugins,
    // Configure authentication based on environment:
    // - Capacitor apps: Use Bearer tokens (cookies don't work cross-origin)
    // - Web browsers: Use cookies (default Better Auth behavior)
    fetchOptions: {
      credentials: 'include',
      // Only use Bearer auth for Capacitor apps
      ...(isCapacitor.value
        ? {
            auth: {
              type: 'Bearer',
              token: () => getAuthToken() || ''
            }
          }
        : {})
    }
  }) as AuthClient

  return clientSingleton
}

// Lightweight wrappers that call the client at call-time (client-only)
async function signInEmail(
  payload: Parameters<AuthClient['signIn']['email']>[0],
  options?: Parameters<AuthClient['signIn']['email']>[1]
) {
  const c = getClient()
  if (!c) throw new Error('Auth client is only available on the client')

  const { isCapacitor, setAuthToken, extractTokenFromResponse, getAuthToken } = useCapacitor()

  // Only extract and store Bearer tokens for Capacitor apps
  // Web browsers will use cookies instead
  const onSuccessWrapper = isCapacitor.value
    ? (ctx: { response?: { headers?: Headers } }) => {
        // Extract bearer token from response headers (Better Auth bearer plugin)
        const token = ctx.response?.headers?.get('set-auth-token')
        if (token) {
          setAuthToken(token)
          // Reset the client singleton so it picks up the new token
          clientSingleton = null
        }
      }
    : undefined

  const result = await c.signIn.email(payload, {
    ...(onSuccessWrapper ? { onSuccess: onSuccessWrapper } : {}),
    ...(options || {})
  } as Parameters<AuthClient['signIn']['email']>[1])

  // Fallback: try to extract token from response data if not found in headers
  // Only for Capacitor apps
  if (isCapacitor.value) {
    const storedToken = getAuthToken()
    if (!storedToken) {
      const token = extractTokenFromResponse(result)
      if (token) {
        setAuthToken(token)
        // Reset the client singleton so it picks up the new token
        clientSingleton = null
      }
    }
  }

  return result
}

async function signInSocial(
  payload: Parameters<AuthClient['signIn']['social']>[0],
  options?: Parameters<AuthClient['signIn']['social']>[1]
) {
  const c = getClient()
  if (!c) throw new Error('Auth client is only available on the client')
  return c.signIn.social(payload, options)
}

async function signUpEmail(
  payload: Parameters<AuthClient['signUp']['email']>[0],
  options?: Parameters<AuthClient['signUp']['email']>[1]
) {
  const c = getClient()
  if (!c) throw new Error('Auth client is only available on the client')
  return c.signUp.email(payload, options)
}

async function signInMagicLink(
  payload: { email: string, callbackURL?: string },
  options?: Record<string, unknown>
) {
  const c = getClient()
  if (!c) throw new Error('Auth client is only available on the client')
  // magicLink plugin is conditionally loaded, so we need to check if it exists
  // Type assertion needed because TypeScript can't know at compile time if plugin is loaded
  const signInWithMagicLink = c.signIn as AuthClient['signIn'] & {
    magicLink?: (p: typeof payload, o?: typeof options) => Promise<unknown>
  }
  if (signInWithMagicLink.magicLink) {
    return signInWithMagicLink.magicLink(payload, options)
  }
  throw new Error('Magic link authentication is not enabled')
}

async function requestPasswordReset(payload: { email: string, redirectTo?: string }) {
  const c = getClient()
  if (!c) throw new Error('Auth client is only available on the client')
  // Type assertion needed because requestPasswordReset may not be in the type definition
  // but exists in the runtime Better Auth client
  return (c as unknown as { requestPasswordReset: (p: typeof payload) => Promise<{ error?: { message: string } }> }).requestPasswordReset(payload)
}

async function resetPassword(payload: Parameters<AuthClient['resetPassword']>[0]) {
  const c = getClient()
  if (!c) throw new Error('Auth client is only available on the client')
  return c.resetPassword(payload)
}

async function changePassword(payload: Parameters<AuthClient['changePassword']>[0]) {
  const c = getClient()
  if (!c) throw new Error('Auth client is only available on the client')
  return c.changePassword(payload)
}

async function listUserPasskeys() {
  const c = getClient()
  if (!c) throw new Error('Auth client is only available on the client')
  return c.passkey.listUserPasskeys()
}

async function addPasskey(payload: { name: string }) {
  const c = getClient()
  if (!c) throw new Error('Auth client is only available on the client')
  return c.passkey.addPasskey(payload)
}

async function deletePasskey(payload: { id: string }) {
  const c = getClient()
  if (!c) throw new Error('Auth client is only available on the client')
  return c.passkey.deletePasskey(payload)
}

export function useAuth() {
  const options = defu(useRuntimeConfig().public.auth as Partial<RuntimeAuthConfig>, {
    redirectUserTo: '/dashboard',
    redirectGuestTo: '/auth'
  })
  const session = useState<AuthSession | null>('auth:session', () => null)
  // Internal ref for mutations (used by plugins and internal functions)
  const userState = useState<AuthUser | null>('auth:user', () => null)
  // Computed wrapper for easier template usage (auto-unwraps in templates)
  const user = computed(() => userState.value)
  const sessionFetching = import.meta.server ? ref(false) : useState('auth:sessionFetching', () => false)

  async function fetchSession() {
    // If a fetch is already in progress, wait for it to complete instead of returning early
    // This prevents race conditions where concurrent fetchSession calls (e.g., from login page
    // and middleware) would miss the session update
    if (sessionFetchPromise) {
      return sessionFetchPromise
    }

    // Create and track the promise at module level BEFORE setting the flag
    // This prevents race conditions where concurrent calls could slip through
    // between setting the flag and assigning the promise
    const promise = (async () => {
      sessionFetching.value = true
      try {
        if (import.meta.client) {
          // On client, fetch via our custom session endpoint instead of Better Auth's native getSession
          // because we need to support:
          // 1. Impersonation - Super-admins can impersonate users via cookie-based session override
          // 2. Ban enforcement - Server-side checking if a user is banned (including time-based bans)
          //
          // The /api/user/session endpoint calls Better Auth's getSession() and then enhances
          // it with our custom logic via enhanceSession() helper
          try {
            const { apiCall, extractData } = useApiClient()
            const response = await apiCall<AuthSession | { session: null, user: null }>('/api/user/session')

            // Extract data from ApiResponse wrapper
            const sessionData = extractData(response)

            // Handle the response data - check if it's a valid session with user
            if (sessionData && typeof sessionData === 'object' && 'user' in sessionData && sessionData.user) {
              // Valid session with user - TypeScript now knows this is AuthSession
              session.value = sessionData as unknown as AuthSession
              userState.value = sessionData.user
            } else {
              // No session (anonymous user)
              session.value = null
              userState.value = null
            }

            // No direct Better Auth fallback; server /api/user/session handles Bearer token mapping
          } catch (error) {
            // Handle errors gracefully:
            // - Anonymous users: endpoint returns { session: null, user: null } (no error)
            // - Banned users: endpoint throws 403
            // - Other errors: rethrow
            const errorData = error as { data?: { statusCode?: number }, statusCode?: number }
            const status = errorData?.data?.statusCode || errorData?.statusCode
            if (status === 403) {
              // User is banned, clear session
              session.value = null
              userState.value = null
            } else {
              // Unexpected error, rethrow
              throw error
            }
          }
        } else {
          // On server, session is already populated by the server plugin.
          // No action required.
        }
        return { session: session.value, user: userState.value }
      } finally {
        sessionFetching.value = false
      }
    })()

    // Assign the promise to the module-level variable
    sessionFetchPromise = promise

    // Clean up the promise reference AFTER it settles (success or failure)
    // This ensures concurrent calls during the fetch will reuse the same promise
    // and only after it completes will new calls create a fresh promise
    promise.finally(() => {
      // Only clear if this is still the current promise (handles rapid successive calls)
      if (sessionFetchPromise === promise) {
        sessionFetchPromise = null
      }
    }).catch(() => {
      // Catch any unhandled rejections to prevent test errors
      // The actual error handling is done in the try-catch above
      // This is just to prevent "Unhandled Promise Rejection" warnings
    })

    return promise
  }

  async function signOut({ redirectTo }: { redirectTo?: string } = {}) {
    const c = getClient()
    if (!c) throw new Error('Auth client is only available on the client')
    const res = await c.signOut()
    session.value = null
    userState.value = null

    // Clear the stored token for Capacitor apps only
    const { clearAuthToken } = useCapacitor()
    clearAuthToken()

    // Reset the client singleton
    clientSingleton = null
    if (redirectTo) {
      await navigateTo(redirectTo)
    }
    return res
  }

  // Admin helpers
  async function impersonateUser(payload: { userId: string }) {
    const c = getClient()
    if (!c) throw new Error('Auth client is only available on the client')
    // Always use our server route so our cookie-based impersonation is set
    const { apiCall, extractData } = useApiClient()
    const response = await apiCall('/api/super-admin/users/impersonate', {
      method: 'POST',
      body: { userId: payload.userId }
    })
    return extractData(response)
  }

  async function stopImpersonation() {
    const c = getClient()
    if (!c) throw new Error('Auth client is only available on the client')
    // Always use our server route so our cookie is cleared
    const { apiCall, extractData } = useApiClient()
    const response = await apiCall('/api/super-admin/stop-impersonation', { method: 'POST' })
    return extractData(response)
  }

  async function liftBanUser(payload: { userId: string }) {
    const c = getClient()
    if (!c) throw new Error('Auth client is only available on the client')

    // No Better Auth built-in for app-specific banning; use server API
    const { apiCall, extractData } = useApiClient()
    const response = await apiCall('/api/super-admin/users/ban', {
      method: 'POST',
      body: { userId: payload.userId, banned: false }
    })
    return extractData(response)
  }

  if (import.meta.client) {
    getClient()!.$store.listen('$sessionSignal', async (signal: unknown) => {
      if (!signal) return
      await fetchSession()
    })
  }

  return {
    session,
    user,
    loggedIn: computed(() => !!session.value),
    signInEmail,
    signInSocial,
    signUpEmail,
    signInMagicLink,
    requestPasswordReset,
    resetPassword,
    changePassword,
    impersonateUser,
    stopImpersonation,
    liftBanUser,
    signOut,
    fetchSession,
    listUserPasskeys,
    addPasskey,
    deletePasskey,
    options
  }
}
