/**
 *  Doorman entrypoint
 */
import { randomUUID } from 'crypto';
import fetch from "node-fetch";
import '@twilio-labs/serverless-runtime-types';
import {
  ServerlessFunctionSignature,
} from '@twilio-labs/serverless-runtime-types/types';
import { BuzzerDialEvent } from '../types/BuzzerDialEvent';
import { getConfig, notifyAllDiscord, notifyDiscord, tryLogCallerForOnboarding } from '../utils/DoormanUtils';
import { dialFallbackTwiml, doorOnboardTwiml, doorOpenTwiml } from '../utils/TwimlUtils';
import { TwilioContext } from '../types/TwilioContext';
import VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
import { DoorStatus } from '../../../doorman-api/src/types/DoorStatus';
import { StatusResponse } from '../../../doorman-api/src/functions/api/door/status';
import { InfoResponse } from '../../../doorman-api/src/functions/api/door/info';
import { getMetricFromRegistry, withMetrics } from '../../../doorman-api/src/common/DoormanHandler';
import { Counter, Summary } from 'prom-client';
import { BuzzerActivatedMetrics, registerMetrics } from '../metrics/BuzzerActivatedMetrics';

const XML_FAIL_FAST_CALLBACK_COMMENT = "This response is from a fail fast callback";

export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent> = withMetrics('buzzer-activated', async function (context, event, callback, metricsRegistry, logger, failFastCallback) {
  // metrics
  registerMetrics(metricsRegistry);

  let invokeId = `[${randomUUID()}]`;
  let configString = event.config;
  let config: InfoResponse | undefined;
  console.log(invokeId + " starting execution");

  // get by api or parse it out from query
  if (!configString) {
    config = await getConfig(context, event.From);
  } else {
    try {
      config = JSON.parse(configString);
    } catch (e) {
      config = await getConfig(context, event.From);
    }
  }

  // if this is not configured
  if (!config || !config.door) {
    // check if onboarding is enabled
    const callLog = await tryLogCallerForOnboarding(context, event.From);

    // onboarding is enabled, we got OTP to speak to user
    if (callLog.otp) {
      console.log(invokeId + " going to speak OTP to caller");
      getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.ONBOARDING_ATTEMPT)
        .inc({ From: event.From }, 1);

      const response = doorOnboardTwiml(callLog.otp);

      callback(null, response);
      return;
    } else {
      // no onboarding, and this is an unknown caller. Just reject
      let twiml = new Twilio.twiml.VoiceResponse();
      twiml.reject();

      getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.CALL_REJECTED)
        .inc({ From: event.From }, 1);
      callback(null, twiml);
      return;
    }
  }

  // let users know someone is currently buzzing, and allow unlock by discord user
  let msg = `🔔 Someone is dialing now @ Door "${config.door}"\n\n`;

  msg += `Will dial fallback numbers if door remains locked\n[Click to unlock](${context.DOORMAN_URL}/api/door/auth?door=${config.door}&key=`;
  let msgs = config.discordUsers.map((u: string) =>
    msg + u + ')'
  );

  await notifyDiscord(context, msgs, config.discordUsers, config.discordUsers.map(() => ""), metricsRegistry);

  // set me to true to simulate a timeout
  let responseLock = false;
  let intervals: Timer[] = [];
  let timeouts: Timer[] = [];

  let pollLock = false;

  const unlockPromise = new Promise<VoiceResponse>((resolve, reject) => {
    intervals.push(setInterval(() => {
      // prevent multiple polling at once
      if (pollLock) {
        return;
      }

      getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.POLL_ATTEMPTS)
        .inc({ door: config.door }, 1);

      const recordPollLatency = getMetricFromRegistry<Summary>(metricsRegistry, BuzzerActivatedMetrics.POLL_LATENCY)
        .startTimer({ door: config.door });

      pollLock = true;
      fetch(context.DOORMAN_URL + `/api/door/status?door=${config.door}`)
        .then(res => res.json())
        .then(async (rawBody) => {
          let body = rawBody as StatusResponse;
          recordPollLatency();
          pollLock = false;

          if (body?.status === DoorStatus.OPEN) {
            clearInterval(intervals[0]);
            const twiml = doorOpenTwiml(config);

            if (!responseLock) {
              responseLock = true;
              console.log(
                invokeId + " UnlockPromise: I was the fastest, so I will attempt to notify discord users before resolving with unlock"
              );
              getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.API_UNLOCK)
                .inc({ door: config.door }, 1);
              await notifyAllDiscord(context, config, `🔓 Doorman buzzed someone up @ Door "${config.door}"`, metricsRegistry, JSON.stringify(body.fingerprint));

              resolve(twiml);
            } else {
              console.log(
                invokeId + " UnlockPromise: dropping out of the race, graceful fallback is already notifiying discord users"
              );
            }
          }
        }).catch(err => console.log(invokeId + err));
    }, 400));
  });

  const gracefulFallbackPromise = new Promise<VoiceResponse>((resolve, reject) => {
    timeouts.push(setTimeout(async () => {
      const twiml = dialFallbackTwiml(config);

      if (!responseLock) {
        responseLock = true;
        getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.DIAL_THROUGH)
          .inc({ door: config.door }, 1);

        console.log(
          invokeId + " GracefulFallbackPromise: I was the fastest, so I will attempt to notify discord users before resolving with a call"
        );
        resolve(twiml);
      } else {
        console.log(
          invokeId + " GracefulFallbackPromise: dropping out of the race, another response is already underway"
        );
      }
    }, 7_250));
  });

  // provide a method to the decorator to use as a fast fallback that shouldn't have any await
  failFastCallback(() => {
    // prevent other responses
    responseLock = true;

    getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.RESULT_NOTIFICATION_FATE_UNKNOWN)
      .inc({ door: config.door }, 1);

    getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.DIAL_THROUGH)
      .inc({ door: config.door }, 1);

    const twiml = dialFallbackTwiml(config);
    console.log(
      invokeId + " UngracefulFallback: Cutting it too close to timeout! Skipping notifying users and responding fallback"
    );
    twiml.comment(XML_FAIL_FAST_CALLBACK_COMMENT);
    return [null, twiml];
  });

  const twiml = await Promise.race([unlockPromise, gracefulFallbackPromise]);
  console.log(invokeId + " Race ended, clearing residual timers");
  timeouts.forEach(timeout => clearTimeout(timeout));
  intervals.forEach(interval => clearInterval(interval));
  callback(null, twiml);
});
