import * as Hapi from "@hapi/hapi";
import Joi from "joi";
import * as Boom from "@hapi/boom";
import Twilio from "twilio";
import { SavedVoiceProvider } from "@digiresilience/metamigo-db";
import pMemoize from "p-memoize";
import ExpiryMap from "expiry-map";
import ms from "ms";
import * as Helpers from "../../helpers/index.js";
import workerUtils from "../../../../worker-utils.js";

const queueRecording = async (meta) =>
  workerUtils.addJob("twilio-recording", meta, { jobKey: meta.callSid });

const twilioClientFor = (provider: SavedVoiceProvider): Twilio.Twilio => {
  const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
  if (!accountSid || !apiKeySid || !apiKeySecret)
    throw new Error(
      `twilio provider ${provider.name} does not have credentials`
    );

  return Twilio(apiKeySid, apiKeySecret, {
    accountSid,
  });
};

const _getOrCreateTTSTestApplication = async (
  url,
  name,
  client: Twilio.Twilio
) => {
  const application = await client.applications.list({ friendlyName: name });

  if (application[0] && application[0].voiceUrl === url) {
    return application[0];
  }

  return client.applications.create({
    voiceMethod: "POST",
    voiceUrl: url,
    friendlyName: name,
  });
};

const cache = new ExpiryMap(ms("1h"));
const getOrCreateTTSTestApplication = pMemoize(_getOrCreateTTSTestApplication, {
  cache,
});

export const TwilioRoutes = Helpers.noAuth([
  {
    method: "get",
    path: "/api/voice/twilio/prompt/{voiceLineId}",
    options: {
      description: "download the mp3 file to play as a prompt for the user",
      validate: {
        params: {
          voiceLineId: Joi.string().uuid().required(),
        },
      },
      async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
        const { voiceLineId } = request.params;
        const voiceLine = await request
          .db()
          .voiceLines.findById({ id: voiceLineId });

        if (!voiceLine) return Boom.notFound();
        if (!voiceLine.audioPromptEnabled) return Boom.badRequest();

        const mp3 = voiceLine.promptAudio["audio/mpeg"];
        if (!mp3) {
          return Boom.serverUnavailable();
        }

        return h
          .response(Buffer.from(mp3, "base64"))
          .header("Content-Type", "audio/mpeg")
          .header("Content-Disposition", "attachment; filename=prompt.mp3");
      },
    },
  },
  {
    method: "post",
    path: "/api/voice/twilio/record/{voiceLineId}",
    options: {
      description: "webhook for twilio to handle an incoming call",
      validate: {
        params: {
          voiceLineId: Joi.string().uuid().required(),
        },
      },
      async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
        const { voiceLineId } = request.params;
        const { To } = request.payload as { To: string };
        const voiceLine = await request.db().voiceLines.findBy({ number: To });
        if (!voiceLine) return Boom.notFound();
        if (voiceLine.id !== voiceLineId) return Boom.badRequest();

        const frontendUrl = request.server.config().frontend.url;
        const useTextPrompt = !voiceLine.audioPromptEnabled;

        const twiml = new Twilio.twiml.VoiceResponse();
        if (useTextPrompt) {
          let prompt = voiceLine.promptText;
          if (!prompt || prompt.length === 0)
            prompt =
              "The grabadora text prompt is unconfigured. Please set a prompt in the administration screen.";
          twiml.say(
            {
              language: voiceLine.language as any,
              voice: voiceLine.voice as any,
            },
            prompt
          );
        } else {
          const promptUrl = `${frontendUrl}/api/v1/voice/twilio/prompt/${voiceLineId}`;
          twiml.play({ loop: 1 }, promptUrl);
        }

        twiml.record({
          playBeep: true,
          finishOnKey: "1",
          recordingStatusCallback: `${frontendUrl}/api/v1/voice/twilio/recording-ready/${voiceLineId}`,
        });
        return twiml.toString();
      },
    },
  },
  {
    method: "post",
    path: "/api/voice/twilio/recording-ready/{voiceLineId}",
    options: {
      description: "webhook for twilio to handle a recording",
      validate: {
        params: {
          voiceLineId: Joi.string().uuid().required(),
        },
      },
      async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
        const { voiceLineId } = request.params;
        const voiceLine = await request
          .db()
          .voiceLines.findById({ id: voiceLineId });
        if (!voiceLine) return Boom.notFound();

        const { AccountSid, RecordingSid, CallSid } = request.payload as {
          AccountSid: string;
          RecordingSid: string;
          CallSid: string;
        };

        await queueRecording({
          voiceLineId,
          accountSid: AccountSid,
          callSid: CallSid,
          recordingSid: RecordingSid,
        });
        return h.response().code(203);
      },
    },
  },
  {
    method: "post",
    path: "/api/voice/twilio/text-to-speech/{providerId}",
    options: {
      description: "webook for twilio to test the twilio text-to-speech",
      validate: {
        params: {
          providerId: Joi.string().uuid().required(),
        },
      },
      async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
        const { language, voice, prompt } = request.payload as {
          language: any;
          voice: any;
          prompt: string;
        };
        const twiml = new Twilio.twiml.VoiceResponse();
        twiml.say({ language, voice }, prompt);
        return twiml.toString();
      },
    },
  },
  {
    method: "get",
    path: "/api/voice/twilio/text-to-speech-token/{providerId}",
    options: {
      description:
        "generates a one time token to test the twilio text-to-speech",
      validate: {
        params: {
          providerId: Joi.string().uuid().required(),
        },
      },
      async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
        const { providerId } = request.params as { providerId: string };
        const provider: SavedVoiceProvider = await request
          .db()
          .voiceProviders.findById({ id: providerId });
        if (!provider) return Boom.notFound();

        const frontendUrl = request.server.config().frontend.url;
        const url = `${frontendUrl}/api/v1/voice/twilio/text-to-speech/${providerId}`;
        const name = `Grabadora text-to-speech tester: ${providerId}`;
        const app = await getOrCreateTTSTestApplication(
          url,
          name,
          twilioClientFor(provider)
        );

        const { accountSid, apiKeySecret, apiKeySid } = provider.credentials;
        const token = new Twilio.jwt.AccessToken(
          accountSid,
          apiKeySid,
          apiKeySecret,
          { identity: "tts-test" }
        );

        const grant = new Twilio.jwt.AccessToken.VoiceGrant({
          outgoingApplicationSid: app.sid,
          incomingAllow: true,
        });
        token.addGrant(grant);
        return h.response({
          token: token.toJwt(),
        });
      },
    },
  },
]);
