import {
  AxAuthIdpConfig,
  concatUrl,
  ConfigStatusResponse,
  ConfigStatusResponseCode,
  IdentityProvider,
  IdentityServiceConfig,
  LogoutResponse,
  LogoutResponseCode,
  TokenResponse,
} from '@axinom/mosaic-id-utils';
import {
  CompleteManagementUserPasswordResetDocument,
  CompleteManagementUserPasswordResetMutationVariables,
  CompleteManagementUserSignUpDocument,
  CompleteManagementUserSignUpMutationVariables,
  InitiateManagementUserPasswordResetDocument,
  InitiateManagementUserPasswordResetMutationVariables,
  InitiateManagementUserSignUpDocument,
  InitiateManagementUserSignUpMutationVariables,
} from '../generated/graphql/axauth';
import { IdentityProviderInfo } from '../types';
import { stringifyGqlQuery } from '../utils';

export type IdServiceConfiguration = (
  | {
      status: ConfigStatusResponseCode.SUCCESS;
      providers: IdentityProviderInfo[];
    }
  | {
      status:
        | ConfigStatusResponseCode.TENANT_NOT_ACTIVE
        | ConfigStatusResponseCode.ENVIRONMENT_NOT_ACTIVE
        | ConfigStatusResponseCode.IDP_MISCONFIGURATION
        | ConfigStatusResponseCode.ERROR;
    }
) & {
  tenantId: string;
  environmentId: string;
};

export interface WellKnownUrls {
  axAuthManagementGraphQlEndpoint: string;
}

export interface InitiateSignUpRequest {
  firstName: string;
  lastName: string;
  email: string;
  password: string;
}

export interface IdentityServiceClient {
  getConfiguration: () => Promise<IdServiceConfiguration>;
  getWellKnownUrls: () => Promise<WellKnownUrls>;
  getToken: () => Promise<TokenResponse>;
  logoutUser: () => Promise<boolean>;
  getIdpAuthUrl: (idpId: IdentityProvider, originUrl: string) => string;
  addTokenChangedHandler: (callback: TokenChangedCallback) => void;
  removeTokenChangedHandler: (callback: TokenChangedCallback) => void;
  initiateSignUp: (
    initiateSignUpRequest: InitiateSignUpRequest,
    axAuthManagementBaseUrl: string,
  ) => Promise<{ isSuccess: boolean; errorMessage?: string }>;
  completeUserSignUp: (
    signUpOtp: string,
    axAuthManagementBaseUrl: string,
  ) => Promise<{ isSuccess: boolean; errorMessage?: string }>;
  initiatePasswordReset: (
    email: string,
    axAuthManagementBaseUrl: string,
  ) => Promise<{ isSuccess: boolean; errorMessage?: string }>;
  completePasswordReset: (
    resetOtp: string,
    newPassword: string,
    axAuthManagementBaseUrl: string,
  ) => Promise<{ isSuccess: boolean; errorMessage?: string }>;
}

export type TokenChangedCallback = (token: TokenResponse | null) => void;

/**
 * Creates a configuration service client. For performance reasons there should only be a single client instance created by an application
 * @param config The configuration values for the identity service that should be used.
 */
export const createIdentityServiceClient = (
  config: IdentityServiceConfig,
): IdentityServiceClient => {
  const token = (() => {
    let _response: TokenResponse | null = null;

    return {
      set response(value: TokenResponse | null) {
        _response = value;
        if (value !== null) {
          // In case the token is just nulled (e.g. by the setTimeout that clears the token once it's about to expire)
          // we won't emit the token changed event. So the user will not be considered logged out.
          emitTokenChanged(value);
        }
      },
      get response() {
        return _response;
      },
    };
  })();

  let tokenChangedHandlers: TokenChangedCallback[] = [];

  async function invokeIdentityServiceMethod<T>(method: string): Promise<T> {
    const idServiceTokenUrl = concatUrl(
      config.idServiceAuthBaseUrl,
      config.tenantId,
      config.environmentId,
      method,
    ).toString();

    return (
      await fetch(idServiceTokenUrl.toString(), {
        cache: 'no-cache',
        credentials: 'include',
        redirect: 'follow',
        referrerPolicy: 'origin',
      })
    ).json() as Promise<T>;
  }

  const emitTokenChanged = (token: TokenResponse | null): void => {
    tokenChangedHandlers.forEach((h) => h(token));
  };

  const loadTokenFromService = async (): Promise<TokenResponse> => {
    token.response = await invokeIdentityServiceMethod<TokenResponse>('token');

    if (token.response.user) {
      setTimeout(async () => {
        // Clear tokenResponse if the token expires, so the next call will retrieve a new one.
        token.response = null;
      }, (token.response.user.token.expiresInSeconds - 60) * 1000);
    }
    return token.response;
  };
  let tokenLoadingPromise: Promise<TokenResponse> | null = null;
  let axAuthClientId: string | undefined;

  return {
    /**
     * Adds an event handler that will be raised whenever a new token response is loaded from the backend.
     *
     * Please note, that it is not recommended to remember the token for later use. When in need of a token,
     * please use the `getToken` method, which will make sure that a valid token is returned.
     */
    addTokenChangedHandler(callback: TokenChangedCallback) {
      tokenChangedHandlers.push(callback);
    },

    /**
     * Removes an event handler for the TokenChanged event.
     */
    removeTokenChangedHandler(callback: TokenChangedCallback) {
      tokenChangedHandlers = tokenChangedHandlers.filter((c) => c !== callback);
    },

    /**
     * Loads the configuration data from the service.
     */
    async getConfiguration(): Promise<IdServiceConfiguration> {
      const configStatusResponse =
        await invokeIdentityServiceMethod<ConfigStatusResponse>(
          'id-config-status',
        );

      if (configStatusResponse.code === ConfigStatusResponseCode.SUCCESS) {
        const providers = configStatusResponse.enabledIdentityProviders.map(
          (idpConfig) => {
            if (idpConfig.idProvider === IdentityProvider.AX_AUTH) {
              axAuthClientId = (idpConfig as AxAuthIdpConfig).clientId;
            }
            return {
              idpId: idpConfig.idProvider,
              displayName: idpConfig.customBrandingConfig.displayName ?? '',
              idpImageUrl: idpConfig.customBrandingConfig.idpImageUrl ?? '',
              enabled: idpConfig.enabled,
            };
          },
        );

        return {
          status: configStatusResponse.code,
          providers,
          tenantId: configStatusResponse.tenantId,
          environmentId: configStatusResponse.environmentId,
        };
      } else {
        return {
          status: configStatusResponse.code,
          tenantId: configStatusResponse.tenantId,
          environmentId: configStatusResponse.environmentId,
        };
      }
    },

    async getWellKnownUrls(): Promise<WellKnownUrls> {
      const idServiceTokenUrl = concatUrl(
        config.idServiceAuthBaseUrl,
        '.well-known',
      ).toString();

      const response = await (
        await fetch(idServiceTokenUrl.toString(), {
          cache: 'no-cache',
          credentials: 'include',
          redirect: 'follow',
          referrerPolicy: 'origin',
        })
      ).json();
      return {
        axAuthManagementGraphQlEndpoint:
          response['axAuthManagementGraphQlEndpoint'],
      };
    },

    /**
     * Returns a token.
     */
    async getToken() {
      if (token.response) {
        // we already have a (still) valid token
        return Promise.resolve(token.response);
      }

      if (tokenLoadingPromise === null) {
        // if no request for a new token is currently running, we create a new one
        tokenLoadingPromise = loadTokenFromService().then((response) => {
          tokenLoadingPromise = null;
          return response;
        });
      }
      // returning the loading promise
      return tokenLoadingPromise;
    },

    /**
     * Logs out the user.
     */
    async logoutUser() {
      // TODO: Handle autoNavigation on login redirect and clear on logout
      // sessionStorage.removeItem('lastProtectedRoute');
      // sessionStorage.removeItem('autoNavigateOnRedirect');

      token.response = null;
      // We want the user to be considered logged out, so we throw emit the TokenChanged here using 'null'.
      emitTokenChanged(null);

      const logoutUrl = concatUrl(
        config.idServiceAuthBaseUrl,
        config.tenantId,
        config.environmentId,
        'logout',
      );
      logoutUrl.searchParams.set(
        'post_logout_redirect_uri',
        window.location.origin,
      );

      const logoutResult = (await (
        await fetch(logoutUrl.toString(), {
          cache: 'no-cache',
          credentials: 'include',
          redirect: 'follow',
          referrerPolicy: 'origin',
        })
      ).json()) as LogoutResponse;

      if (logoutResult && logoutResult.code === LogoutResponseCode.SUCCESS) {
        if (logoutResult.idpLogoutUrl) {
          window.location.assign(logoutResult.idpLogoutUrl);
        }
        return true;
      } else {
        return false;
      }
    },

    /**
     * Returns the authentication url of the given identity provider. To start the sign in, the application should open/forward to that url.
     */
    // Function to be used with onClick event for idp-buttons
    getIdpAuthUrl(idpId: IdentityProvider, originUrl: string) {
      const { idServiceAuthBaseUrl, tenantId, environmentId } = config;

      const idpName = IdentityProvider[idpId];
      let authApi = 'auth';

      if (idpId === IdentityProvider.AX_AUTH) {
        authApi = 'signInWithCredentials';
      }

      const url = concatUrl(
        idServiceAuthBaseUrl,
        tenantId,
        environmentId,
        authApi,
      );

      url.search = `providerId=${idpName}&originUrl=${originUrl}&idServiceClientUrl=${idServiceAuthBaseUrl}`;

      return url.toString();
    },

    async initiateSignUp(
      initiateSignUpRequest: InitiateSignUpRequest,
      axAuthManagementBaseUrl: string,
    ) {
      if (!axAuthClientId) {
        throw new Error('Invalid configuration. AxAuthClientId is missing.');
      }
      const variables: InitiateManagementUserSignUpMutationVariables = {
        input: {
          firstName: initiateSignUpRequest.firstName,
          lastName: initiateSignUpRequest.lastName,
          email: initiateSignUpRequest.email,
          password: initiateSignUpRequest.password,
          oAuthClientId: axAuthClientId,
        },
      };

      try {
        const userSignUpResponse = await fetch(`${axAuthManagementBaseUrl}`, {
          method: 'POST',
          cache: 'no-cache',
          redirect: 'follow',
          referrerPolicy: 'origin',
          headers: {
            'content-type': 'application/json',
          },
          body: stringifyGqlQuery(
            InitiateManagementUserSignUpDocument,
            variables,
          ),
        });

        if (userSignUpResponse.ok) {
          const responseBody = await userSignUpResponse.json();
          if (responseBody.errors) {
            return {
              isSuccess: false,
              errorMessage: responseBody.errors[0].message,
            };
          }
          return { isSuccess: true };
        }
        const responseBody = await userSignUpResponse.text();
        return {
          isSuccess: false,
          errorMessage: `Unknown error occurred while initiating user sign up process. Original error: ${responseBody}`,
        };
      } catch (e) {
        const error = e as Error;
        return {
          isSuccess: false,
          errorMessage: `Unknown error occurred while initiating user sign up process. Original error: ${error.message}`,
        };
      }
    },

    async initiatePasswordReset(
      email: string,
      axAuthManagementBaseUrl: string,
    ) {
      if (!axAuthClientId) {
        throw new Error('Invalid configuration. AxAuthClientId is missing.');
      }
      const variables: InitiateManagementUserPasswordResetMutationVariables = {
        input: {
          email,
          oAuthClientId: axAuthClientId,
        },
      };

      try {
        const passwordResetResponse = await fetch(
          `${axAuthManagementBaseUrl}`,
          {
            method: 'POST',
            cache: 'no-cache',
            redirect: 'follow',
            referrerPolicy: 'origin',
            headers: {
              'content-type': 'application/json',
            },
            body: stringifyGqlQuery(
              InitiateManagementUserPasswordResetDocument,
              variables,
            ),
          },
        );

        if (passwordResetResponse.ok) {
          const responseBody = await passwordResetResponse.json();
          if (responseBody.errors) {
            return {
              isSuccess: false,
              errorMessage: responseBody.errors[0].message,
            };
          }
          return { isSuccess: true };
        }
        const responseBody = await passwordResetResponse.text();
        return {
          isSuccess: false,
          errorMessage: `Unknown error occurred while initiating password reset process. Original error: ${responseBody}`,
        };
      } catch (e) {
        const error = e as Error;
        return {
          isSuccess: false,
          errorMessage: `Unknown error occurred while initiating password reset process. Original error: ${error.message}`,
        };
      }
    },

    async completePasswordReset(
      resetOtp: string,
      newPassword: string,
      axAuthManagementBaseUrl: string,
    ) {
      const variables: CompleteManagementUserPasswordResetMutationVariables = {
        input: {
          resetOtp,
          newPassword,
        },
      };

      try {
        const completePasswordResetResponse = await fetch(
          `${axAuthManagementBaseUrl}`,
          {
            method: 'POST',
            cache: 'no-cache',
            redirect: 'follow',
            referrerPolicy: 'origin',
            headers: {
              'content-type': 'application/json',
            },
            body: stringifyGqlQuery(
              CompleteManagementUserPasswordResetDocument,
              variables,
            ),
          },
        );

        if (completePasswordResetResponse.ok) {
          const responseBody = await completePasswordResetResponse.json();
          if (responseBody.errors) {
            return {
              isSuccess: false,
              errorMessage: responseBody.errors[0].message,
            };
          }
          return { isSuccess: true };
        }
        const responseBody = await completePasswordResetResponse.text();
        return {
          isSuccess: false,
          errorMessage: `Unknown error occurred while completing password reset process. Original error: ${responseBody}`,
        };
      } catch (e) {
        const error = e as Error;
        return {
          isSuccess: false,
          errorMessage: `Unknown error occurred while completing password reset process. Original error: ${error.message}`,
        };
      }
    },

    async completeUserSignUp(
      signUpOtp: string,
      axAuthManagementBaseUrl: string,
    ) {
      const variables: CompleteManagementUserSignUpMutationVariables = {
        input: {
          signUpOtp,
        },
      };

      try {
        const completeUserSignUpResponse = await fetch(
          `${axAuthManagementBaseUrl}`,
          {
            method: 'POST',
            cache: 'no-cache',
            redirect: 'follow',
            referrerPolicy: 'origin',
            headers: {
              'content-type': 'application/json',
            },
            body: stringifyGqlQuery(
              CompleteManagementUserSignUpDocument,
              variables,
            ),
          },
        );

        if (completeUserSignUpResponse.ok) {
          const responseBody = await completeUserSignUpResponse.json();
          if (responseBody.errors) {
            return {
              isSuccess: false,
              errorMessage: responseBody.errors[0].message,
            };
          }
          return { isSuccess: true };
        }
        const responseBody = await completeUserSignUpResponse.text();
        return {
          isSuccess: false,
          errorMessage: `Unknown error occurred while completing sign up process. Original error: ${responseBody}`,
        };
      } catch (e) {
        const error = e as Error;
        return {
          isSuccess: false,
          errorMessage: `Unknown error occurred while completing sign up process. Original error: ${error.message}`,
        };
      }
    },
  };
};
