/**
 *  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 } from '../utils/DoormanUtils';
import { dialFallbackTwiml, 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 { InfoResponseClient } 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';

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: InfoResponseClient | 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);
    }
  }

  // reject the call if this is not configured
  if (!config || !config.door) {
    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 right now @ Door "${config.door}" [Click to unlock](${context.DOORMAN_URL}/api/door/auth?door=${config.door}&key=`;
  let msgs = config.discordUsers.map((u) => 
    msg + u + ')'
  );

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

  let responseLock = false;
  let intervals: Timer[] = [];
  let timeouts: Timer[] = [];

  const unlockPromise = new Promise<VoiceResponse>((resolve, reject) => {
    intervals.push(setInterval(() => {
      getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.POLL_ATTEMPTS)
        .inc({ door: config.door }, 1);

      const recordPollLatency = getMetricFromRegistry<Summary>(metricsRegistry, BuzzerActivatedMetrics.POLL_LATENCY)
        .startTimer({ door: config.door });
      
      fetch(context.DOORMAN_URL + `/api/door/status?door=${config.door}`)
        .then(res => res.json())
        .then(async (rawBody) => {
          let body = rawBody as StatusResponse;
          recordPollLatency();
          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));
    }, 1250));
  });

  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"
        );
        await notifyAllDiscord(
          context, 
          config, 
          `📞 Somebody buzzed the door and it dialed through to fallback phone numbers @ Door "${config.door}"`,
          metricsRegistry,
        );
        resolve(twiml);
      } else {
        console.log(
          invokeId + " GracefulFallbackPromise: dropping out of the race, another response is already underway"
        );
      }
    }, 7500));
  });

  // 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"
      );
      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);
});
