/* The Artificer was built in memory of Babka
 * With love, Ean
 *
 * December 21, 2020
 */
import { log, LogTypes as LT } from '@Log4Deno';

import config from '~config';

import dbClient from 'db/client.ts';

import endpoints from 'endpoints/_index.ts';
import stdResp from 'endpoints/stdResponses.ts';

// start() returns nothing
// start initializes and runs the entire API for the bot
const start = () => {
  log(LT.INFO, `HTTP api running at: http://localhost:${config.api.port}/`);

  // rateLimitTime holds all users with the last time they started a rate limit timer
  const rateLimitTime = new Map<string, number>();
  // rateLimitCnt holds the number of times the user has called the api in the current rate limit timer
  const rateLimitCnt = new Map<string, number>();

  // Catching every request made to the server
  Deno.serve({ port: config.api.port }, async (request) => {
    log(LT.LOG, `Handling request: ${JSON.stringify(request.headers)} | ${JSON.stringify(request.method)} | ${JSON.stringify(request.url)}`);
    // Check if user is authenticated to be using this API
    let authenticated = false;
    let rateLimited = false;
    let updateRateLimitTime = false;
    let apiUserid = 0n;
    let apiUseridStr = '';
    let apiUserEmail = '';
    let apiUserDelCode = '';

    // Check the requests API key
    if (request.headers.has('X-Api-Key')) {
      // Get the userid and flags for the specific key
      const dbApiQuery = await dbClient.query('SELECT userid, email, deleteCode FROM all_keys WHERE apiKey = ? AND active = 1 AND banned = 0', [
        request.headers.get('X-Api-Key'),
      ]);

      // If only one user returned, is not banned, and is currently active, mark as authenticated
      if (dbApiQuery.length === 1) {
        apiUserid = BigInt(dbApiQuery[0].userid);
        apiUserEmail = dbApiQuery[0].email;
        apiUserDelCode = dbApiQuery[0].deleteCode;
        authenticated = true;

        // Rate limiting inits
        apiUseridStr = apiUserid.toString();
        const apiTimeNow = new Date().getTime();

        // Check if user has sent a request recently
        if (rateLimitTime.has(apiUseridStr) && (rateLimitTime.get(apiUseridStr) || 0) + config.api.rateLimitTime > apiTimeNow) {
          // Get current count
          const currentCnt = rateLimitCnt.get(apiUseridStr) || 0;
          if (currentCnt < config.api.rateLimitCnt) {
            // Limit not yet exceeded, update count
            rateLimitCnt.set(apiUseridStr, currentCnt + 1);
          } else {
            // Limit exceeded, prevent API use
            rateLimited = true;
          }
        } else {
          // Update the maps
          updateRateLimitTime = true;
          rateLimitCnt.set(apiUseridStr, 1);
        }
      }
    }

    if (!rateLimited) {
      // Get path and query as a string
      const [urlPath, tempQ] = request.url.split('?');
      const path = urlPath.split('api')[1];

      // Turn the query into a map (if it exists)
      const query = new Map<string, string>();
      if (tempQ !== undefined) {
        tempQ.split('&').forEach((e: string) => {
          log(LT.LOG, `Parsing request query ${JSON.stringify(request)} ${e}`);
          const [option, params] = e.split('=');
          query.set(option.toLowerCase(), params);
        });
      }

      if (path) {
        const lowerCasePath = path.toLowerCase().trim();
        if (authenticated) {
          // Update rate limit details
          if (updateRateLimitTime) {
            const apiTimeNow = new Date().getTime();
            rateLimitTime.set(apiUseridStr, apiTimeNow);
          }

          // Handle the authenticated request
          switch (request.method) {
            case 'GET':
              switch (lowerCasePath) {
                case '/ping':
                case '/ping/':
                  return endpoints.get.apiPing();
                case '/stats':
                case '/stats/':
                  return endpoints.get.apiStats();
                case '/key':
                case '/key/':
                  return endpoints.get.apiKeyAdmin(query, apiUserid);
                case '/channel':
                case '/channel/':
                  return endpoints.get.apiChannel(query, apiUserid);
                case '/roll':
                case '/roll/':
                  return endpoints.get.apiRoll(query, apiUserid);
                default:
                  // Alert API user that they messed up
                  return stdResp.NotFound('Auth Get');
              }
              break;
            case 'POST':
              switch (lowerCasePath) {
                case '/channel/add':
                case '/channel/add/':
                  return endpoints.post.apiChannelAdd(query, apiUserid);
                default:
                  // Alert API user that they messed up
                  return stdResp.NotFound('Auth Post');
              }
              break;
            case 'PUT':
              switch (lowerCasePath) {
                case '/key/ban':
                case '/key/ban/':
                case '/key/unban':
                case '/key/unban/':
                case '/key/activate':
                case '/key/activate/':
                case '/key/deactivate':
                case '/key/deactivate/':
                  return endpoints.put.apiKeyManage(query, apiUserid, path);
                case '/channel/ban':
                case '/channel/ban/':
                case '/channel/unban':
                case '/channel/unban/':
                  return endpoints.put.apiChannelManageBan(query, apiUserid, path);
                case '/channel/activate':
                case '/channel/activate/':
                case '/channel/deactivate':
                case '/channel/deactivate/':
                  return endpoints.put.apiChannelManageActive(query, apiUserid, path);
                default:
                  // Alert API user that they messed up
                  return stdResp.NotFound('Auth Put');
              }
              break;
            case 'DELETE':
              switch (lowerCasePath) {
                case '/key/delete':
                case '/key/delete/':
                  return endpoints.delete.apiKeyDelete(query, apiUserid, apiUserEmail, apiUserDelCode);
                default:
                  // Alert API user that they messed up
                  return stdResp.NotFound('Auth Del');
              }
              break;
            default:
              // Alert API user that they messed up
              return stdResp.MethodNotAllowed('Auth');
          }
        } else {
          // Handle the unauthenticated request
          switch (request.method) {
            case 'GET':
              switch (lowerCasePath) {
                case '/key':
                case '/key/':
                  return endpoints.get.apiKey(query);
                case '/heatmap.png':
                  return endpoints.get.heatmapPng();
                case '/webview':
                  return endpoints.get.generateWebView(query);
                default:
                  // Alert API user that they messed up
                  return stdResp.NotFound('NoAuth Get');
              }
              break;
            default:
              // Alert API user that they messed up
              return stdResp.MethodNotAllowed('NoAuth');
          }
        }
      } else {
        return stdResp.Forbidden('What are you trying to do?');
      }
    } else if (authenticated && rateLimited) {
      // Alert API user that they are doing this too often
      return stdResp.TooManyRequests('Slow down, servers are expensive and this bot is free to use.');
    } else {
      // Alert API user that they shouldn't be doing this
      return stdResp.Forbidden('Why are you here?');
    }
  });
};

export default { start };
