import {JwtPayload, Session} from '@shopify/shopify-api';

import type {BasicParams} from '../../types';
import {AppDistribution} from '../../types';
import type {AppConfigArg} from '../../config-types';
import {
  getSessionTokenHeader,
  ensureCORSHeadersFactory,
  getSessionTokenFromUrlParam,
  respondToBotRequest,
  respondToOptionsRequest,
  validateSessionToken,
  getShopFromRequest,
} from '../helpers';

import {
  cancelBillingFactory,
  requestBillingFactory,
  requireBillingFactory,
  checkBillingFactory,
  createUsageRecordFactory,
  updateUsageCappedAmountFactory,
} from './billing';
import type {
  AdminContext,
  AuthenticateAdmin,
  EmbeddedAdminContext,
  NonEmbeddedAdminContext,
} from './types';
import {
  createAdminApiContext,
  ensureAppIsEmbeddedIfRequired,
  ensureSessionTokenSearchParamIfRequired,
  redirectFactory,
  renderAppBridge,
  validateShopAndHostParams,
} from './helpers';
import {AuthorizationStrategy} from './strategies/types';
import {scopesApiFactory} from './scope/factory';

export interface SessionTokenContext {
  shop: string;
  sessionId?: string;
  sessionToken?: string;
  payload?: JwtPayload;
}

interface AuthStrategyParams extends BasicParams {
  strategy: AuthorizationStrategy;
}

export function authStrategyFactory<ConfigArg extends AppConfigArg>({
  strategy,
  ...params
}: AuthStrategyParams): AuthenticateAdmin<ConfigArg> {
  const {api, logger, config} = params;

  async function respondToBouncePageRequest(request: Request) {
    const url = new URL(request.url);

    if (url.pathname.endsWith(config.auth.patchSessionTokenPath)) {
      logger.debug('Rendering bounce page', {
        shop: getShopFromRequest(request),
      });
      throw renderAppBridge({config, logger, api}, request);
    }
  }

  async function respondToExitIframeRequest(request: Request) {
    const url = new URL(request.url);

    if (url.pathname.endsWith(config.auth.exitIframePath)) {
      const destination = url.searchParams.get('exitIframe')!;

      logger.debug('Rendering exit iframe page', {
        shop: getShopFromRequest(request),
        destination,
      });
      throw renderAppBridge({config, logger, api}, request, {url: destination});
    }
  }

  type AdminContextBase =
    | EmbeddedAdminContext<ConfigArg>
    | NonEmbeddedAdminContext<ConfigArg>;

  function createContext(
    request: Request,
    session: Session,
    authStrategy: AuthorizationStrategy,
    sessionToken?: JwtPayload,
  ): AdminContext<ConfigArg> {
    let context: AdminContextBase = {
      admin: createAdminApiContext(
        session,
        params,
        authStrategy.handleClientError(request),
      ),
      billing: {
        require: requireBillingFactory(params, request, session),
        check: checkBillingFactory(params, request, session),
        request: requestBillingFactory(params, request, session),
        cancel: cancelBillingFactory(params, request, session),
        createUsageRecord: createUsageRecordFactory(params, request, session),
        updateUsageCappedAmount: updateUsageCappedAmountFactory(
          params,
          request,
          session,
        ),
      },

      session,
      cors: ensureCORSHeadersFactory(params, request),
    };

    context = addEmbeddedFeatures(context, request, session, sessionToken);
    context = addScopesFeatures(context);

    return context as AdminContext<ConfigArg>;
  }

  function addEmbeddedFeatures(
    context: AdminContextBase,
    request: Request,
    session: Session,
    sessionToken?: JwtPayload,
  ) {
    if (config.distribution === AppDistribution.ShopifyAdmin) {
      return context;
    }
    return {
      ...context,
      sessionToken,
      redirect: redirectFactory(params, request, session.shop),
    };
  }

  function addScopesFeatures(context: AdminContextBase) {
    return {
      ...context,
      scopes: scopesApiFactory(params, context.session, context.admin),
    };
  }

  return async function authenticateAdmin(request: Request) {
    try {
      respondToBotRequest(params, request);
      respondToOptionsRequest(params, request);
      await respondToBouncePageRequest(request);
      await respondToExitIframeRequest(request);

      // If this is a valid request, but it doesn't have a session token header, this is a document request. We need to
      // ensure we're embedded if needed and we have the information needed to load the session.
      if (!getSessionTokenHeader(request)) {
        validateShopAndHostParams(params, request);
        await ensureAppIsEmbeddedIfRequired(params, request);
        await ensureSessionTokenSearchParamIfRequired(params, request);
      }

      logger.info('Authenticating admin request', {
        shop: getShopFromRequest(request),
      });

      const {payload, shop, sessionId, sessionToken} =
        await getSessionTokenContext(params, request);

      logger.debug('Loading session from storage', {shop, sessionId});
      const existingSession = sessionId
        ? await config.sessionStorage!.loadSession(sessionId)
        : undefined;

      const session = await strategy.authenticate(request, {
        session: existingSession,
        sessionToken,
        shop,
      });

      return createContext(request, session, strategy, payload);
    } catch (errorOrResponse) {
      if (errorOrResponse instanceof Response) {
        logger.debug('Authenticate returned a response', {
          shop: getShopFromRequest(request),
        });
        ensureCORSHeadersFactory(params, request)(errorOrResponse);
      }

      throw errorOrResponse;
    }
  };
}

async function getSessionTokenContext(
  params: BasicParams,
  request: Request,
): Promise<SessionTokenContext> {
  const {api, config, logger} = params;

  const headerSessionToken = getSessionTokenHeader(request);
  const searchParamSessionToken = getSessionTokenFromUrlParam(request);
  const sessionToken = (headerSessionToken || searchParamSessionToken)!;

  logger.debug('Attempting to authenticate session token', {
    shop: getShopFromRequest(request),
    sessionToken: JSON.stringify({
      header: headerSessionToken,
      search: searchParamSessionToken,
    }),
  });

  if (config.distribution !== AppDistribution.ShopifyAdmin) {
    const payload = await validateSessionToken(params, request, sessionToken);
    const dest = new URL(payload.dest);
    const shop = dest.hostname;

    logger.debug('Session token is valid - authenticated', {shop, payload});
    const sessionId = config.useOnlineTokens
      ? api.session.getJwtSessionId(shop, payload.sub)
      : api.session.getOfflineId(shop);

    return {shop, payload, sessionId, sessionToken};
  }

  const url = new URL(request.url);
  const shop = url.searchParams.get('shop')!;

  const sessionId = await api.session.getCurrentId({
    isOnline: config.useOnlineTokens,
    rawRequest: request,
  });

  return {shop, sessionId, payload: undefined, sessionToken};
}
