const Parse = require('parse/node').Parse;
const path = require('path');
// These methods handle batch requests.
const batchPath = '/batch';

// Mounts a batch-handler onto a PromiseRouter.
function mountOnto(router) {
  router.route('POST', batchPath, req => {
    return handleBatch(router, req);
  });
}

function parseURL(urlString) {
  try {
    return new URL(urlString);
  } catch (error) {
    return undefined;
  }
}

function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) {
  serverURL = serverURL ? parseURL(serverURL) : undefined;
  publicServerURL = publicServerURL ? parseURL(publicServerURL) : undefined;

  const apiPrefixLength = originalUrl.length - batchPath.length;
  let apiPrefix = originalUrl.slice(0, apiPrefixLength);

  const makeRoutablePath = function (requestPath) {
    // The routablePath is the path minus the api prefix
    if (requestPath.slice(0, apiPrefix.length) != apiPrefix) {
      throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot route batch path ' + requestPath);
    }
    return path.posix.join('/', requestPath.slice(apiPrefix.length));
  };

  if (serverURL && publicServerURL && serverURL.pathname != publicServerURL.pathname) {
    const localPath = serverURL.pathname;
    const publicPath = publicServerURL.pathname;

    // Override the api prefix
    apiPrefix = localPath;
    return function (requestPath) {
      // Figure out which server url was used by figuring out which
      // path more closely matches requestPath
      const startsWithLocal = requestPath.startsWith(localPath);
      const startsWithPublic = requestPath.startsWith(publicPath);
      const pathLengthToUse =
        startsWithLocal && startsWithPublic
          ? Math.max(localPath.length, publicPath.length)
          : startsWithLocal
            ? localPath.length
            : publicPath.length;

      const newPath = path.posix.join('/', localPath, '/', requestPath.slice(pathLengthToUse));

      // Use the method for local routing
      return makeRoutablePath(newPath);
    };
  }

  return makeRoutablePath;
}

// Returns a promise for a {response} object.
// TODO: pass along auth correctly
function handleBatch(router, req) {
  if (!Array.isArray(req.body.requests)) {
    throw new Parse.Error(Parse.Error.INVALID_JSON, 'requests must be an array');
  }

  // The batch paths are all from the root of our domain.
  // That means they include the API prefix, that the API is mounted
  // to. However, our promise router does not route the api prefix. So
  // we need to figure out the API prefix, so that we can strip it
  // from all the subrequests.
  if (!req.originalUrl.endsWith(batchPath)) {
    throw 'internal routing problem - expected url to end with batch';
  }

  const makeRoutablePath = makeBatchRoutingPathFunction(
    req.originalUrl,
    req.config.serverURL,
    req.config.publicServerURL
  );

  const batch = transactionRetries => {
    let initialPromise = Promise.resolve();
    if (req.body.transaction === true) {
      initialPromise = req.config.database.createTransactionalSession();
    }

    return initialPromise.then(() => {
      const promises = req.body.requests.map(restRequest => {
        const routablePath = makeRoutablePath(restRequest.path);

        // Construct a request that we can send to a handler
        const request = {
          body: restRequest.body,
          config: req.config,
          auth: req.auth,
          info: req.info,
        };

        return router.tryRouteRequest(restRequest.method, routablePath, request).then(
          response => {
            return { success: response.response };
          },
          error => {
            return { error: { code: error.code, error: error.message } };
          }
        );
      });

      return Promise.all(promises)
        .then(results => {
          if (req.body.transaction === true) {
            if (results.find(result => typeof result.error === 'object')) {
              return req.config.database.abortTransactionalSession().then(() => {
                return Promise.reject({ response: results });
              });
            } else {
              return req.config.database.commitTransactionalSession().then(() => {
                return { response: results };
              });
            }
          } else {
            return { response: results };
          }
        })
        .catch(error => {
          if (
            error &&
            error.response &&
            error.response.find(
              errorItem => typeof errorItem.error === 'object' && errorItem.error.code === 251
            ) &&
            transactionRetries > 0
          ) {
            return batch(transactionRetries - 1);
          }
          throw error;
        });
    });
  };
  return batch(5);
}

module.exports = {
  mountOnto,
  makeBatchRoutingPathFunction,
};
