/* eslint-disable @typescript-eslint/no-var-requires */

/**
 * This webpack config establishes a proxy server to enable a local build of circulation-admin to
 * connect to a remote Circulation Manager back-end. Requests for webpack assets are served from
 * memory; other requests are proxied to the specified Circulation Manager.
 *
 * Usage:
 *   npm run dev-server -- --env=backend=https://gorgon.tpp-qa.lyrasistechnology.org
 * or
 *   npx webpack serve --config webpack.dev-server.config --env=backend=https://gorgon.tpp-qa.lyrasistechnology.org
 *
 * This makes the circulation-admin webapp available at http://localhost:8080/admin.
 */

const {
  createProxyMiddleware,
  responseInterceptor,
} = require("http-proxy-middleware");

const { merge } = require("webpack-merge");
const { URL } = require("url");
const dev = require("./webpack.dev.config.js");

/**
 * The public path to local webpack assets. This is chosen to have low chance of collision with any
 * path on the back-end (e.g., a library ID, or "/admin"). This should start and end with slashes.
 */
const devAssetsPublicPath = "/webpack-dev-assets/";

/**
 * Build the webpack configuration.
 * See https://webpack.js.org/configuration/configuration-types/#exporting-a-function
 *
 * @param {object} env The environment.
 * @returns The webpack configuration.
 */
module.exports = (env) => {
  const { backend } = env;

  if (!backend) {
    console.error("Please specify the URL of a Circulation Manager back-end.");

    console.error(
      "Example: npm run dev-server -- --env=backend=https://gorgon.tpp-qa.lyrasistechnology.org"
    );

    throw "No back-end URL was specified.";
  }

  console.info(`Using Circulation Manager back-end: ${backend}`);

  const backendUrl = new URL(backend);

  /**
   * Rewrite a location header (as received in a 3xx response). This changes back-end URLs to
   * point to the local server instead. The CM may redirect to a URL that contains a "redirect"
   * parameter that contains another URL. The "redirect" parameter is also rewritten.
   *
   * @param res The response.
   * @param req The request.
   */
  const rewriteLocationHeader = (res, req) => {
    const location = res.getHeader("location");

    if (!location) {
      return;
    }

    const locationUrl = new URL(location, backendUrl);

    if (locationUrl.host !== backendUrl.host) {
      return;
    }

    const requestHost = req.headers.host;

    if (!requestHost) {
      return;
    }

    locationUrl.protocol = "http";
    locationUrl.host = requestHost;

    const redirectParam = locationUrl.searchParams.get("redirect");

    if (redirectParam) {
      const redirectUrl = new URL(redirectParam);

      if (redirectUrl.host == backendUrl.host) {
        redirectUrl.protocol = "http";
        redirectUrl.host = requestHost;

        locationUrl.searchParams.set("redirect", redirectUrl.href);
      }
    }

    res.setHeader("location", locationUrl.href);
  };

  /**
   * Rewrites an OpenSearch description response. This changes back-end URLs in the description to
   * point to the local server instead. This is a simple find-and-replace.
   *
   * @param responseBuffer A buffer containing the response body.
   * @param req The request.
   * @returns
   */
  const rewriteOpenSearch = (responseBuffer, req) => {
    const requestHost = req.headers.host;

    if (!requestHost) {
      return responseBuffer;
    }

    const osd = responseBuffer.toString("utf8");

    return osd.replace(
      new RegExp(backendUrl.origin, "g"),
      `http://${requestHost}`
    );
  };

  /**
   * Rewrites an OPDS response. This changes back-end URLs in the feed to point to the local server
   * instead. This is a simple find-and-replace.
   *
   * @param responseBuffer A buffer containing the response body.
   * @param req The request.
   * @returns
   */
  const rewriteOPDS = (responseBuffer, req) => {
    const requestHost = req.headers.host;

    if (!requestHost) {
      return responseBuffer;
    }

    const feed = responseBuffer.toString("utf8");

    return feed.replace(
      new RegExp(backendUrl.origin, "g"),
      `http://${requestHost}`
    );
  };

  /**
   * Rewrites an HTML response. This changes jsdelivr CDN URLs in the page to point to the webpack
   * assets on the local server instead. This is a simple find-and-replace.
   *
   * @param responseBuffer A buffer containing the response body.
   * @param req The request.
   * @returns
   */
  const rewriteHTML = (responseBuffer, req) => {
    const requestHost = req.headers.host;

    if (!requestHost) {
      return responseBuffer;
    }

    const page = responseBuffer.toString("utf8");
    const packageName = process.env.npm_package_name;
    const cdnUrlPattern = `"https://cdn.jsdelivr.net/npm/${packageName}(@.*?)?/dist/(.*?)"`;

    return page.replace(
      new RegExp(cdnUrlPattern, "g"),
      `"http://${requestHost}${devAssetsPublicPath}$2"`
    );
  };

  const proxyMiddleware = createProxyMiddleware({
    changeOrigin: true,
    onProxyRes: responseInterceptor(
      async (responseBuffer, proxyRes, req, res) => {
        rewriteLocationHeader(res, req);

        const contentType = res.getHeader("content-type");

        if (contentType.startsWith("application/atom+xml")) {
          return rewriteOPDS(responseBuffer, req);
        }

        if (contentType.startsWith("text/html")) {
          return rewriteHTML(responseBuffer, req);
        }

        if (contentType.startsWith("application/opensearchdescription+xml")) {
          return rewriteOpenSearch(responseBuffer, req);
        }

        return responseBuffer;
      }
    ),
    proxyTimeout: 120000,
    secure: false,
    selfHandleResponse: true,
    target: backend,
    timeout: 120000,
  });

  const config = merge(dev, {
    devServer: {
      onAfterSetupMiddleware: (devServer) => {
        devServer.app.use("/", proxyMiddleware);
      },
    },
    output: {
      publicPath: devAssetsPublicPath,
    },
  });

  return config;
};