import { useEffect, useState, type ReactNode } from 'react';
import {
  createClient,
  fetchExchange,
  mapExchange,
  Provider,
  type AnyVariables,
  type Client,
  type Operation,
} from 'urql';
import { authExchange } from '@urql/exchange-auth';
import { requestInteractiveReauth } from '../lib/reauth-coordinator.js';
import { ROUTES } from '../router/routes.js';
import { handleUrqlError } from './urql-error-handler.js';

type TokenCacheMode = 'on' | 'off';

type AccessTokenResolution =
  | {
      status: 'token';
      token: string;
    }
  | {
      status: 'unauthenticated';
    }
  | {
      status: 'error';
      error: unknown;
    };

type AccessTokenProviderResult = string | null | AccessTokenResolution;

type AccessTokenProvider = (options?: {
  cacheMode?: TokenCacheMode;
}) => Promise<AccessTokenProviderResult>;

const BUSINESS_SCOPE_LS_KEY = 'urql:businessScope';

let accessTokenProvider: AccessTokenProvider | null = null;
let bearerToken: string | null = null;
let loginRedirectInProgress = false;
let businessScope: string | null = (() => {
  try {
    return localStorage.getItem(BUSINESS_SCOPE_LS_KEY);
  } catch {
    return null;
  }
})();
let onClientReset: ((client: Client) => void) | null = null;

export function setUrqlAccessTokenProvider(provider: AccessTokenProvider | null): void {
  accessTokenProvider = provider;
  if (!provider) {
    bearerToken = null;
    loginRedirectInProgress = false;
  }
}

export function getBusinessScopeIds(): string[] {
  if (!businessScope) return [];
  return businessScope.split(',');
}

export function setBusinessScope(ids: string[]): void {
  businessScope = ids.length > 0 ? ids.join(',') : null;
  try {
    if (businessScope) {
      localStorage.setItem(BUSINESS_SCOPE_LS_KEY, businessScope);
    } else {
      localStorage.removeItem(BUSINESS_SCOPE_LS_KEY);
    }
  } catch {
    // ignore
  }
  resetUrqlClient();
  onClientReset?.(getUrqlClient());
}

function normalizeAccessTokenResult(result: AccessTokenProviderResult): AccessTokenResolution {
  if (typeof result === 'string') {
    return { status: 'token', token: result };
  }

  if (!result) {
    return { status: 'unauthenticated' };
  }

  if (result.status === 'token') {
    return { status: 'token', token: result.token };
  }

  if (result.status === 'unauthenticated') {
    return { status: 'unauthenticated' };
  }

  return { status: 'error', error: result.error };
}

async function getAccessToken(options?: {
  cacheMode?: TokenCacheMode;
}): Promise<AccessTokenResolution> {
  if (!accessTokenProvider) {
    return { status: 'unauthenticated' };
  }

  try {
    const result = await accessTokenProvider(options);
    return normalizeAccessTokenResult(result);
  } catch (error) {
    return { status: 'error', error };
  }
}

function toError(error: unknown): Error {
  if (error instanceof Error) {
    return error;
  }

  return new Error('Failed to refresh access token');
}

function redirectToLogin(): void {
  if (loginRedirectInProgress) {
    return;
  }

  if (typeof window !== 'undefined' && window.location) {
    if (
      window.location.pathname === ROUTES.LOGIN ||
      window.location.pathname === ROUTES.AUTH_CALLBACK
    ) {
      return;
    }

    try {
      if (window.sessionStorage) {
        const { pathname, search, hash } = window.location;
        const returnTo = `${pathname}${search}${hash}`;
        window.sessionStorage.setItem('auth:returnTo', returnTo);
      }
    } catch {
      // Ignore storage errors and continue with redirect
    }

    loginRedirectInProgress = true;
    window.location.href = `${ROUTES.LOGIN}?reauth=1`;
    return;
  }

  if (typeof globalThis !== 'undefined' && 'location' in globalThis && globalThis.location) {
    if (
      globalThis.location.pathname === ROUTES.LOGIN ||
      globalThis.location.pathname === ROUTES.AUTH_CALLBACK
    ) {
      return;
    }

    loginRedirectInProgress = true;
    globalThis.location.href = `${ROUTES.LOGIN}?reauth=1`;
  }
}

/**
 * Singleton URQL client for use in loaders and server-side operations
 * This is separate from the Provider client to avoid React context dependencies
 */
let globalClient: Client | null = null;

export function getUrqlClient(): Client {
  if (globalClient) {
    return globalClient;
  }

  const isDevAuthEnabled = import.meta.env.VITE_DEV_AUTH === '1';
  const devAuthUserId = import.meta.env.VITE_DEV_AUTH_USER_ID?.trim() ?? '';

  let url: string;
  switch (import.meta.env.MODE) {
    case 'production': {
      url = 'https://accounter.onrender.com/graphql';
      break;
    }
    case 'staging': {
      url = 'https://accounter-staging.onrender.com/graphql';
      break;
    }
    default: {
      url = 'http://localhost:4000/graphql';
      break;
    }
  }

  globalClient = createClient({
    url,
    exchanges: [
      mapExchange({
        onResult(result) {
          handleUrqlError(result);
        },
      }),
      authExchange(async utils => {
        if (!isDevAuthEnabled) {
          const initialToken = await getAccessToken();
          bearerToken = initialToken.status === 'token' ? `Bearer ${initialToken.token}` : null;
        }

        return {
          willAuthError(): boolean {
            // Avoid eager refresh loops; refresh only after explicit UNAUTHENTICATED responses.
            return false;
          },
          addAuthToOperation(operation): Operation<void, AnyVariables> {
            if (isDevAuthEnabled) {
              if (!devAuthUserId) {
                console.warn(
                  'VITE_DEV_AUTH is enabled but VITE_DEV_AUTH_USER_ID is not set. Dev auth header will be omitted.',
                );
                return operation;
              }

              const devHeaders: Record<string, string> = { 'X-Dev-Auth': devAuthUserId };
              if (businessScope) devHeaders['x-business-scope'] = businessScope;
              return utils.appendHeaders(operation, devHeaders);
            }

            if (!bearerToken) {
              return operation;
            }

            const headers: Record<string, string> = { Authorization: bearerToken };
            if (businessScope) headers['x-business-scope'] = businessScope;
            return utils.appendHeaders(operation, headers);
          },
          didAuthError(error): boolean {
            return (
              error.graphQLErrors?.some(e => e.extensions?.code === 'UNAUTHENTICATED') ?? false
            );
          },
          async refreshAuth(): Promise<void> {
            if (isDevAuthEnabled) {
              return;
            }

            const refreshedToken = await getAccessToken({ cacheMode: 'off' });

            if (refreshedToken.status === 'error') {
              // Preserve current bearer token on transient provider failures.
              throw toError(refreshedToken.error);
            }

            if (refreshedToken.status === 'unauthenticated') {
              bearerToken = null;

              // Offer in-place re-authentication (modal + Auth0 popup) so queued operations
              // can resume without losing page state. Falls back to a full-page redirect.
              const outcome = await requestInteractiveReauth();
              if (outcome === 'authenticated') {
                const fresh = await getAccessToken({ cacheMode: 'off' });
                if (fresh.status === 'token') {
                  loginRedirectInProgress = false;
                  bearerToken = `Bearer ${fresh.token}`;
                  return;
                }
              }

              redirectToLogin();
              return;
            }

            loginRedirectInProgress = false;
            bearerToken = `Bearer ${refreshedToken.token}`;
          },
        };
      }),
      fetchExchange,
    ],
  });

  return globalClient;
}

/**
 * Reset the global client (useful for tests or logout)
 */
export function resetUrqlClient(): void {
  globalClient = null;
  bearerToken = null;
  loginRedirectInProgress = false;
}

export function UrqlProvider({ children }: { children?: ReactNode }): ReactNode {
  const [client, setClient] = useState(getUrqlClient);

  useEffect(() => {
    onClientReset = setClient;
    return () => {
      if (onClientReset === setClient) onClientReset = null;
    };
  }, []);

  return <Provider value={client}>{children}</Provider>;
}
