import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { UserAgentHeader } from "../../../utils/blockUserAgent";
import { getMetricFromRegistry, withMetrics } from "../../../common/DoormanHandler";
import { TwilioContext } from "../../../types/TwilioContext";
import { OnboardMetrics, registerMetrics } from "../../../metrics/OnboardMetrics";
import { createOnboardDoorConfig, DOOR_CONFIG_SK, DoorConfigSchema, getDoorConfigID, getOnboardDoorId, OnboardDoorReq, OnboardDoorReqSchema } from "../../../schema/DoorConfig";
import { createDynaBridgeClient } from "../../../utils/ddb";
import DiscordOauth2 from "discord-oauth2";
import { DoorAliasSchema, getDoorAliasID } from "../../../schema/DoorAlias";
import { Counter } from "prom-client";
import { z } from "zod";
import zu from "zod_utilz";
import { setResponseJson } from "../../../utils/responseUtils";

export const OnboardRequestSchema = z.object({
  newConfig: zu.stringToJSON().optional(),
  code: z.string().optional(),
  state: zu.stringToJSON().optional(),
}).partial()
.refine(data => data.newConfig || (data.code && data.state), 'newConfig or (code and state) must be specified');

export type OnboardRequest = z.infer<typeof OnboardRequestSchema>;

export interface OnboardRequestTwilio extends ServerlessEventObject<OnboardRequest, UserAgentHeader> { };

export const DiscordOnboardingStateSchema = z.object({
  name: z.string(),
  id: z.string(),
  apiRedirect: z.string(),
});

export type DiscordOnboardingState = z.infer<typeof DiscordOnboardingStateSchema>;

const ONBOARDING_SCOPE = ['identify', 'email', 'guilds.join'];

export const handler: ServerlessFunctionSignature<TwilioContext, OnboardRequestTwilio> = withMetrics('onboard', async (context, event, callback, metricsRegistry) => {
  const response = new Twilio.Response();
  registerMetrics(metricsRegistry);

  const req = OnboardRequestSchema.parse(event);

  const db = createDynaBridgeClient(context);

  // return oauth link
  const oauth = new DiscordOauth2({
    clientId: context.DISCORD_CLIENT_ID,
    clientSecret: context.DISCORD_CLIENT_SECRET,
    redirectUri: context.DOORMAN_URL,
  });

  // create door config route
  if (req.code && req.state) {
    const code = req.code;
    let discordState: DiscordOnboardingState = DiscordOnboardingStateSchema.parse(req.state);

    const config = await db.entities.onboardDoorConfig.findById(getOnboardDoorId(discordState.name));
    if (!config || config.nonce !== discordState.id) {
      setResponseJson(response, 404, { msg: "approval not found" });
      return callback(null, response);
    }

    console.log("getting discord token");

    let token: DiscordOauth2.TokenRequestResult;

    try {
      token = await oauth.tokenRequest({
        code,
        scope: ONBOARDING_SCOPE,
        grantType: "authorization_code",
      });
    } catch (err) {
      console.log("something went wrong with discord authorization");
      setResponseJson(response, 401, { err });
      return callback(null, response);
    }

    const user = await oauth.getUser(token.access_token);
    const profileId = user.id;

    console.log(profileId);

    if (!profileId) {
      setResponseJson(response, 404, { msg: "profile not found" });
      return callback(null, response);
    }

    console.log('trying to join Doorman server');

    // join doorman discord server
    await fetch(
      `https://discord.com/api/v8/guilds/${context.DISCORD_GUILD_ID}/members/${profileId}`,
      {
        method: 'PUT',
        headers: {
          'Authorization': 'Bot ' + context.DISCORD_BOT_TOKEN,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            'access_token': token.access_token,
        })
      }).catch(err => console.error(err));

    type ActionType = Parameters<typeof db.transaction>[0][0];
    
    // door alias
    const doorAlias = DoorAliasSchema.parse({
      name: config.name,
      PK: config.buzzer,
    });

    const createDoorAlias: ActionType = {
      action: "Put",
      type: "doorAlias",
      entity: doorAlias,
    };

    // door config
    const doorConfig = DoorConfigSchema.parse({
      ...config,
      discordUsers: [profileId],
      PK: `door-${config.name}`,
      SK: DOOR_CONFIG_SK,
    });

    const createDoorConfig: ActionType = {
      action: "Put",
      type: "doorConfig",
      entity: doorConfig,
    }

    // delete onboarding config
    const deleteOnboardingConfig: ActionType = {
      action: "Delete",
      type: "onboardDoorConfig",
      entity: config,
    };

    // and all must succeed or none succeed
    try {
      await db.transaction([createDoorAlias, createDoorConfig, deleteOnboardingConfig]);
      setResponseJson(response, 200, { redirect: context.DOORMAN_URL + `?door=${config.name}` });
    } catch (e) {
      getMetricFromRegistry<Counter>(metricsRegistry, OnboardMetrics.TRANSACTION_CONFLICT).inc(1);
      console.error(e);

      setResponseJson(response, 409, { msg: "something went wrong during onboarding" });
    }

    return callback(null, response);
  }
  
  let newConfig = req.newConfig;

  if (!newConfig) {
    setResponseJson(response, 400, { msg: "missing newConfig" });
    return callback(null, response);
  }

  console.log("parsing config");
  let newConfigParsed: OnboardDoorReq = OnboardDoorReqSchema.parse(newConfig);
  const newConfigObj = createOnboardDoorConfig(newConfigParsed);

  // check if this name or buzzer is already registered?
  const existingAlias = await db.entities.doorAlias.findById(getDoorAliasID(newConfigObj.buzzer));

  if (existingAlias) {
    getMetricFromRegistry<Counter>(metricsRegistry, OnboardMetrics.BUZZER_CONFLICT).inc({ buzzer: newConfigObj.buzzer }, 1);
    setResponseJson(response, 409, { msg: `A buzzer is already registered with the number ${newConfigObj.buzzer}` });
    return callback(null, response);
  }

  const existingConfig = await db.entities.doorConfig.findById(getDoorConfigID(newConfigObj.name));

  if (existingConfig) {
    getMetricFromRegistry<Counter>(metricsRegistry, OnboardMetrics.NAME_CONFLICT).inc({ name: newConfigObj.name }, 1);
    setResponseJson(response, 409, { msg: `A buzzer is already registered with the name ${newConfigObj.name}` });
    return callback(null, response);
  }

  // TODO: check here for any race condition on onboarding. Techncially 2 ppl could conflict on name or number 
  // and would fail after the discord auth step resulting in TRANSACTION_CONFLICT

  // save to DB, this is a middle step until discord auth comes in
  await db.entities.onboardDoorConfig.save(newConfigObj);

  const discordState: DiscordOnboardingState = DiscordOnboardingStateSchema.parse({
    name: newConfigObj.name,
    id: newConfigObj.nonce,
    apiRedirect: '/api/door/onboard',
  });

  const redirect = oauth.generateAuthUrl({
    scope: ONBOARDING_SCOPE,
    responseType: 'code',
    state: JSON.stringify(discordState),
  });

  setResponseJson(response, 200, { redirect });
  return callback(null, response);
});
