import 'server-only';

import getBaseWebpackConfig from 'next/dist/build/webpack-config';

import {
  AuthError,
  type EmailOtpType,
  SupabaseClient,
  User,
} from '@supabase/supabase-js';

/**
 * @name createAuthCallbackService
 * @description Creates an instance of the AuthCallbackService
 * @param client
 */
export function createAuthCallbackService(client: SupabaseClient) {
  return new AuthCallbackService(client);
}

/**
 * @name AuthCallbackService
 * @description Service for handling auth callbacks in Supabase
 */
class AuthCallbackService {
  constructor(private readonly client: SupabaseClient) {}

  /**
   * @name verifyTokenHash
   * @description Verifies the token hash and type and redirects the user to the next page
   * This should be used when using a token hash to verify the user's email
   * @param request
   * @param params
   */
  async verifyTokenHash(
    request: Request,
    params: {
      joinTeamPath: string;
      redirectPath: string;
      errorPath?: string;
    },
  ): Promise<URL> {
    const url = new URL(request.url);
    const searchParams = url.searchParams;

    const host = request.headers.get('host');

    // set the host to the request host since outside of Vercel it gets set as "localhost" or "0.0.0.0"
    this.adjustUrlHostForLocalDevelopment(url, host);

    url.pathname = params.redirectPath;

    const token_hash = searchParams.get('token_hash');
    const type = searchParams.get('type') as EmailOtpType | null;
    const callbackParam =
      searchParams.get('next') ?? searchParams.get('callback');

    let nextPath: string | null = null;
    const callbackUrl = callbackParam ? new URL(callbackParam) : null;

    // if we have a callback url, we check if it has a next path
    if (callbackUrl) {
      // if we have a callback url, we check if it has a next path
      const callbackNextPath = callbackUrl.searchParams.get('next');

      // if we have a next path in the callback url, we use that
      if (callbackNextPath) {
        nextPath = callbackNextPath;
      } else {
        nextPath = callbackUrl.pathname;
      }
    }

    const inviteToken = callbackUrl?.searchParams.get('invite_token');
    const errorPath = params.errorPath ?? '/auth/callback/error';

    // remove the query params from the url
    searchParams.delete('token_hash');
    searchParams.delete('type');
    searchParams.delete('next');

    // if we have a next path, we redirect to that path
    if (nextPath) {
      url.pathname = nextPath;
    }

    // if we have an invite token, we append it to the redirect url
    if (inviteToken) {
      // if we have an invite token, we redirect to the join team page
      // instead of the default next url. This is because the user is trying
      // to join a team and we want to make sure they are redirected to the
      // correct page.
      url.pathname = params.joinTeamPath;
      searchParams.set('invite_token', inviteToken);

      const emailParam = callbackUrl?.searchParams.get('email');

      if (emailParam) {
        searchParams.set('email', emailParam);
      }
    }

    if (token_hash && type) {
      const { error } = await this.client.auth.verifyOtp({
        type,
        token_hash,
      });

      if (!error) {
        return url;
      }

      if (error.code) {
        url.searchParams.set('code', error.code);
      }

      const errorMessage = getAuthErrorMessage({
        error: error.message,
        code: error.code,
      });

      url.searchParams.set('error', errorMessage);
    }

    // return the user to an error page with some instructions
    url.pathname = errorPath;

    return url;
  }

  /**
   * @name exchangeCodeForSession
   * @description Exchanges the auth code for a session and redirects the user to the next page or an error page
   * @param authCode
   */
  async exchangeCodeForSession(authCode: string): Promise<
    | {
        isSuccess: boolean;
        user: User;
      }
    | ErrorURLParameters
  > {
    let user: User;
    try {
      const { data, error } =
        await this.client.auth.exchangeCodeForSession(authCode);

      // if we have an error, we redirect to the error page
      if (error) {
        return getErrorURLParameters({
          code: error.code,
          error: error.message,
        });
      }

      // Handle Keycloak users - set up Medusa integration
      if (data?.user && this.isKeycloakUser(data.user)) {
        await this.setupMedusaUserForKeycloak(data.user);
      }

      user = data.user;
    } catch (error) {
      console.error(
        {
          error,
          name: `auth.callback`,
        },
        `An error occurred while exchanging code for session`,
      );

      const message = error instanceof Error ? error.message : error;

      return getErrorURLParameters({
        code: (error as AuthError)?.code,
        error: message as string,
      });
    }

    return {
      isSuccess: true,
      user,
    };
  }

  /**
   * Check if user is from Keycloak provider
   */
  private isKeycloakUser(user: any): boolean {
    return (
      user?.app_metadata?.provider === 'keycloak' ||
      user?.app_metadata?.providers?.includes('keycloak')
    );
  }

  private async setupMedusaUserForKeycloak(user: any): Promise<void> {
    if (!user.email) {
      console.warn('Keycloak user has no email, skipping Medusa setup');
      return;
    }

    try {
      // Check if user already has medusa_account_id
      const { data: accountData, error: fetchError } = await this.client
        .schema('medreport')
        .from('accounts')
        .select('medusa_account_id, name, last_name')
        .eq('primary_owner_user_id', user.id)
        .eq('is_personal_account', true)
        .single();

      if (fetchError && fetchError.code !== 'PGRST116') {
        console.error(
          'Error fetching account data for Keycloak user:',
          fetchError,
        );
        return;
      }

      const { medusaLoginOrRegister } = await import(
        '../../features/medusa-storefront/src/lib/data/customer'
      );
      const medusaAccountId = await medusaLoginOrRegister({
        email: user.email,
        supabaseUserId: user.id,
        name: accountData?.name ?? '-',
        lastName: accountData?.last_name ?? '-',
      });

      const currentMedusaAccountId = accountData?.medusa_account_id;
      if (
        !currentMedusaAccountId ||
        currentMedusaAccountId !== medusaAccountId
      ) {
        const { error: updateError } = await this.client
          .schema('medreport')
          .from('accounts')
          .update({ medusa_account_id: medusaAccountId })
          .eq('primary_owner_user_id', user.id)
          .eq('is_personal_account', true);

        if (updateError) {
          console.error('Error updating account with Medusa ID:', updateError);
          return;
        }

        console.log(
          'Successfully set up Medusa account for Keycloak user:',
          medusaAccountId,
        );
      } else {
        console.log(
          'Keycloak user already has Medusa account:',
          accountData.medusa_account_id,
        );
      }
    } catch (error) {
      console.error(
        'Error setting up Medusa account for Keycloak user:',
        error,
      );
    }
  }

  private adjustUrlHostForLocalDevelopment(url: URL, host: string | null) {
    if (this.isLocalhost(url.host) && !this.isLocalhost(host)) {
      url.host = host as string;
      url.port = '';
    }
  }

  private isLocalhost(host: string | null) {
    if (!host) {
      return false;
    }

    return (
      host.includes('localhost:') ||
      host.includes('0.0.0.0:') ||
      host.includes('127.0.0.1:')
    );
  }
}

interface ErrorURLParameters {
  error: string;
  code?: string;
  searchParams: string;
}

export function getErrorURLParameters({
  error,
  code,
}: {
  error: string;
  code?: string;
}): ErrorURLParameters {
  const errorMessage = getAuthErrorMessage({ error, code });

  console.error(
    {
      error,
      name: `auth.callback`,
    },
    `An error occurred while signing user in`,
  );

  const searchParams = new URLSearchParams({
    error: errorMessage,
    code: code ?? '',
  });

  return {
    error: errorMessage,
    code: code ?? '',
    searchParams: searchParams.toString(),
  };
}

/**
 * Checks if the given error message indicates a verifier error.
 * We check for this specific error because it's highly likely that the
 * user is trying to sign in using a different browser than the one they
 * used to request the sign in link. This is a common mistake, so we
 * want to provide a helpful error message.
 */
function isVerifierError(error: string) {
  return error.includes('both auth code and code verifier should be non-empty');
}

function getAuthErrorMessage(params: { error: string; code?: string }) {
  // this error arises when the user tries to sign in with an expired email link
  if (params.code) {
    if (params.code === 'otp_expired') {
      return 'auth:errors.otp_expired';
    }
  }

  // this error arises when the user is trying to sign in with a different
  // browser than the one they used to request the sign in link
  if (isVerifierError(params.error)) {
    return 'auth:errors.codeVerifierMismatch';
  }

  // fallback to the default error message
  return `auth:authenticationErrorAlertBody`;
}
