var express = require('express');
var bodyParser = require('body-parser');
var expressLogging = require('express-logging');
var queryString = require('querystring');
var path = require('path');
var conf = require('./config');
var jwtDecode = require('jwt-decode');

function handleErr(err, response) {
  response.statusCode = 500;
  response.write('Function invocation failed: ' + err.toString());
  response.end();
  console.log('Error during invocation: ', err);
  return;
}

function handleInvocationTimeout(response, timeout) {
  response.statusCode = 500;
  response.write(`Function invocation took longer than ${timeout} seconds.`);
  response.end();
  console.log(
    `Your lambda function took longer than ${timeout} seconds to finish.
If you need a longer execution time, you can increase the timeout using the -t or --timeout flag.
Please note that default function invocation is 10 seconds, check our documentation for more information (https://www.netlify.com/docs/functions/#custom-deployment-options).
`,
  );
}

function createCallback(response) {
  return function callback(err, lambdaResponse) {
    if (err) {
      return handleErr(err, response);
    }

    response.statusCode = lambdaResponse.statusCode;
    for (const key in lambdaResponse.headers) {
      response.setHeader(key, lambdaResponse.headers[key]);
    }
    for (const key in lambdaResponse.multiValueHeaders) {
      const items = lambdaResponse.multiValueHeaders[key];
      response.setHeader(key, items);
    }

    if (lambdaResponse.body) {
      response.write(
        lambdaResponse.isBase64Encoded
          ? Buffer.from(lambdaResponse.body, 'base64')
          : lambdaResponse.body,
      );
    } else {
      if (
        response.statusCode !== 204 &&
        process.env.CONTEXT !== 'production' &&
        !process.env.SILENCE_EMPTY_LAMBDA_WARNING
      )
        console.log(
          `Your lambda function didn't return a body, which may be a mistake. Check our Usage docs for examples (https://github.com/netlify/netlify-lambda#usage). 
          If this is intentional, you can silence this warning by setting process.env.SILENCE_EMPTY_LAMBDA_WARNING to a truthy value or process.env.CONTEXT to 'production'`,
        );
    }
    response.end();
  };
}

function promiseCallback(promise, callback) {
  if (!promise) return;
  if (typeof promise.then !== 'function') return;
  if (typeof callback !== 'function') return;

  return promise.then(
    function (data) {
      callback(null, data);
    },
    function (err) {
      callback(err, null);
    },
  );
}

function buildClientContext(headers) {
  // inject a client context based on auth header https://github.com/netlify/netlify-lambda/pull/57
  if (!headers['authorization']) return;

  const parts = headers['authorization'].split(' ');
  if (parts.length !== 2 || parts[0] !== 'Bearer') return;

  try {
    return {
      identity: {
        url: 'NETLIFY_LAMBDA_LOCALLY_EMULATED_IDENTITY_URL',
        token: 'NETLIFY_LAMBDA_LOCALLY_EMULATED_IDENTITY_TOKEN',
      },
      user: jwtDecode(parts[1]),
    };
  } catch (e) {
    return; // Ignore errors - bearer token is not a JWT, probably not intended for us
  }
}

function createHandler(dir, isStatic, timeout) {
  return function (request, response) {
    // handle proxies without path re-writes (http-servr)
    var cleanPath = request.path.replace(/^\/.netlify\/functions/, '');

    var func = cleanPath.split('/').filter((e) => !!e)[0];
    if (typeof func === 'undefined') {
      console.error(
        `Something went wrong and the function path derived from ${cleanPath} (raw form: ${request.path}) was undefined. Please doublecheck your function naming and toml configuration.`,
      );
    }
    if (typeof dir === 'undefined') {
      console.error(
        `Something went wrong and the function directory ${dir} was undefined. Please doublecheck your toml configuration.`,
      );
    }
    var module = path.join(process.cwd(), dir, func);
    if (isStatic) {
      delete require.cache[require.resolve(module)];
    }
    var handler;
    try {
      handler = require(module);
    } catch (err) {
      handleErr(err, response);
      return;
    }

    var isBase64 =
      request.body &&
      !(request.headers['content-type'] || '').match(
        /text|application|multipart\/form-data/,
      );
    var lambdaRequest = {
      path: request.path,
      httpMethod: request.method,
      queryStringParameters: queryString.parse(request.url.split(/\?(.+)/)[1]),
      headers: request.headers,
      body: isBase64
        ? Buffer.from(request.body.toString(), 'utf8').toString('base64')
        : request.body,
      isBase64Encoded: isBase64,
    };

    var callback = createCallback(response);

    var promise = handler.handler(
      lambdaRequest,
      { clientContext: buildClientContext(request.headers) || {} },
      callback,
    );

    var invocationTimeoutRef = null;

    Promise.race([
      promiseCallback(promise, callback),
      new Promise(function (resolve) {
        invocationTimeoutRef = setTimeout(function () {
          handleInvocationTimeout(response, timeout);
          resolve();
        }, timeout * 1000);
      }),
    ]).then(
      (result) => {
        clearTimeout(invocationTimeoutRef);
        return result; // not used, but writing this to avoid future footguns
      },
      (err) => {
        clearTimeout(invocationTimeoutRef);
        throw err;
      },
    );
  };
}

exports.listen = async function (port, isStatic, timeout) {
  var config = await conf.load();
  var app = express();
  var dir = config.build.functions || config.build.Functions;
  app.use(bodyParser.raw({ limit: '6mb' }));
  app.use(bodyParser.text({ limit: '6mb', type: '*/*' }));
  app.use(
    expressLogging(console, {
      blacklist: ['/favicon.ico'],
    }),
  );

  app.get('/favicon.ico', function (req, res) {
    res.status(204).end();
  });
  app.get('/', function (req, res) {
    res
      .status(404)
      .send(
        `You have requested the root of http://localhost:${port}. This is likely a mistake. netlify-lambda serves functions at http://localhost:${port}/.netlify/functions/your-function-name; please fix your code.`,
      );
  });
  app.all('*', createHandler(dir, isStatic, timeout));

  const server = app.listen(port, function (err) {
    if (err) {
      console.error('Unable to start lambda server: ', err);
      process.exit(1);
    }

    console.log(`Lambda server is listening on ${port}`);
  });

  return {
    clearCache: function (chunk) {
      var module = path.join(process.cwd(), dir, chunk);
      delete require.cache[require.resolve(module)];
    },
    stopServer: () => server.close(),
  };
};