import {adminClientFactory, storefrontClientFactory} from '../../../clients';
import {ensureValidOfflineSession} from '../../../helpers';
import {BasicParams} from '../../../types';

import {
  AppProxyContext,
  AppProxyContextWithSession,
  AuthenticateAppProxy,
  LiquidResponseFunction,
} from './types';

export function authenticateAppProxyFactory(
  params: BasicParams,
): AuthenticateAppProxy {
  const {logger} = params;

  return async function authenticate(
    request: Request,
  ): Promise<AppProxyContext | AppProxyContextWithSession> {
    const url = new URL(request.url);
    const shop = url.searchParams.get('shop')!;
    logger.info('Authenticating app proxy request', {shop});

    if (!(await validateAppProxyHmac(params, url))) {
      logger.info('App proxy request has invalid signature', {shop});
      throw new Response(undefined, {
        status: 400,
        statusText: 'Bad Request',
      });
    }

    const session = await ensureValidOfflineSession(params, shop);

    if (!session) {
      logger.debug('Could not find offline session, returning empty context', {
        shop,
        ...Object.fromEntries(url.searchParams.entries()),
      });

      const context: AppProxyContext = {
        liquid,
        session: undefined,
        admin: undefined,
        storefront: undefined,
      };

      return context;
    }

    const context: AppProxyContextWithSession = {
      liquid,
      session,
      admin: adminClientFactory({params, session}),
      storefront: storefrontClientFactory({params, session}),
    };

    return context;
  };
}

const liquid: LiquidResponseFunction = (body, initAndOptions) => {
  const processedBody = processLiquidBody(body);

  if (typeof initAndOptions !== 'object') {
    return new Response(processedBody, {
      status: initAndOptions || 200,
      headers: {
        'Content-Type': 'application/liquid',
      },
    });
  }

  const {layout, ...responseInit} = initAndOptions || {};
  const responseBody =
    layout === false ? `{% layout none %} ${processedBody}` : processedBody;

  const headers = new Headers(responseInit.headers);
  headers.set('Content-Type', 'application/liquid');

  return new Response(responseBody, {
    ...responseInit,
    headers,
  });
};

async function validateAppProxyHmac(
  params: BasicParams,
  url: URL,
): Promise<boolean> {
  const {api, logger} = params;

  try {
    let searchParams = new URLSearchParams(url.search);
    if (!searchParams.get('index')) {
      searchParams.delete('index');
    }

    let isValid = await api.utils.validateHmac(
      Object.fromEntries(searchParams.entries()),
      {signator: 'appProxy'},
    );

    if (!isValid) {
      const cleanPath = url.pathname
        .replace(/^\//, '')
        .replace(/\/$/, '')
        .replaceAll('/', '.');
      const data = `routes%2F${cleanPath}`;

      searchParams = new URLSearchParams(
        `?_data=${data}&${searchParams.toString().replace(/^\?/, '')}`,
      );

      isValid = await api.utils.validateHmac(
        Object.fromEntries(searchParams.entries()),
        {signator: 'appProxy'},
      );

      if (!isValid) {
        const searchParams = new URLSearchParams(
          `?_data=${data}._index&${url.search.replace(/^\?/, '')}`,
        );

        isValid = await api.utils.validateHmac(
          Object.fromEntries(searchParams.entries()),
          {signator: 'appProxy'},
        );
      }
    }

    return isValid;
  } catch (error) {
    const shop = url.searchParams.get('shop')!;
    logger.info(error.message, {shop});
    throw new Response(undefined, {status: 400, statusText: 'Bad Request'});
  }
}

function processLiquidBody(body: string) {
  return (
    body
      // Add trailing slashes to relative form action URLs
      .replaceAll(
        /<(form[^>]+)action="(\/[^"?]+)(\?[^"]+)?">/g,
        '<$1action="$2/$3">',
      )
      // Add trailing slashes to relative link href URLs
      .replaceAll(/<(a[^>]+)href="(\/[^"?]+)(\?[^"]+)?">/g, '<$1href="$2/$3">')
  );
}
