import axios from "axios";
import qs from "qs";

import { getLocation } from "@calcom/lib/CalEventParser";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type {
  Calendar,
  CalendarEvent,
  EventBusyDate,
  IntegrationCalendar,
  NewCalendarEventType,
  Person,
} from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";

import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";

export type ZohoToken = {
  scope: string;
  api_domain: string;
  expires_in: number;
  expiryDate: number;
  token_type: string;
  access_token: string;
  accountServer: string;
  refresh_token: string;
};

export type ZohoContact = {
  Email: string;
};

/**
 * Converts to the date Format as required by zoho: 2020-08-02T15:30:00+05:30
 * https://www.zoho.com/crm/developer/docs/api/v2/events-response.html
 */
const toISO8601String = (date: Date) => {
  const tzo = -date.getTimezoneOffset(),
    dif = tzo >= 0 ? "+" : "-",
    pad = function (num: number) {
      return (num < 10 ? "0" : "") + num;
    };

  return (
    date.getFullYear() +
    "-" +
    pad(date.getMonth() + 1) +
    "-" +
    pad(date.getDate()) +
    "T" +
    pad(date.getHours()) +
    ":" +
    pad(date.getMinutes()) +
    ":" +
    pad(date.getSeconds()) +
    dif +
    pad(Math.floor(Math.abs(tzo) / 60)) +
    ":" +
    pad(Math.abs(tzo) % 60)
  );
};
export default class ZohoCrmCalendarService implements Calendar {
  private integrationName = "";
  private auth: Promise<{ getToken: () => Promise<void> }>;
  private log: typeof logger;
  private client_id = "";
  private client_secret = "";
  private accessToken = "";

  constructor(credential: CredentialPayload) {
    this.integrationName = "zohocrm_other_calendar";
    this.auth = this.zohoCrmAuth(credential).then((r) => r);
    this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
  }

  private createContacts = async (attendees: Person[]) => {
    const contacts = attendees.map((attendee) => {
      const [firstname, lastname] = !!attendee.name ? attendee.name.split(" ") : [attendee.email, "-"];
      return {
        First_Name: firstname,
        Last_Name: lastname || "-",
        Email: attendee.email,
      };
    });
    return axios({
      method: "post",
      url: `https://www.zohoapis.com/crm/v3/Contacts`,
      headers: {
        "content-type": "application/json",
        authorization: `Zoho-oauthtoken ${this.accessToken}`,
      },
      data: JSON.stringify({ data: contacts }),
    });
  };

  private contactSearch = async (event: CalendarEvent) => {
    const searchCriteria =
      "(" + event.attendees.map((attendee) => `(Email:equals:${encodeURI(attendee.email)})`).join("or") + ")";

    return await axios({
      method: "get",
      url: `https://www.zohoapis.com/crm/v3/Contacts/search?criteria=${searchCriteria}`,
      headers: {
        authorization: `Zoho-oauthtoken ${this.accessToken}`,
      },
    })
      .then((data) => data.data)
      .catch((e) => this.log.error(e, e.response?.data));
  };

  private getMeetingBody = (event: CalendarEvent): string => {
    return `<b>${event.organizer.language.translate("invitee_timezone")}:</b> ${
      event.attendees[0].timeZone
    }<br><br><b>${event.organizer.language.translate("share_additional_notes")}</b><br>${
      event.additionalNotes || "-"
    }`;
  };

  private createZohoEvent = async (event: CalendarEvent) => {
    const zohoEvent = {
      Event_Title: event.title,
      Start_DateTime: toISO8601String(new Date(event.startTime)),
      End_DateTime: toISO8601String(new Date(event.endTime)),
      Description: this.getMeetingBody(event),
      Venue: getLocation(event),
    };

    return axios({
      method: "post",
      url: `https://www.zohoapis.com/crm/v3/Events`,
      headers: {
        "content-type": "application/json",
        authorization: `Zoho-oauthtoken ${this.accessToken}`,
      },
      data: JSON.stringify({ data: [zohoEvent] }),
    })
      .then((data) => data.data)
      .catch((e) => this.log.error(e, e.response?.data));
  };

  private updateMeeting = async (uid: string, event: CalendarEvent) => {
    const zohoEvent = {
      id: uid,
      Event_Title: event.title,
      Start_DateTime: toISO8601String(new Date(event.startTime)),
      End_DateTime: toISO8601String(new Date(event.endTime)),
      Description: this.getMeetingBody(event),
      Venue: getLocation(event),
    };
    return axios({
      method: "put",
      url: `https://www.zohoapis.com/crm/v3/Events`,
      headers: {
        "content-type": "application/json",
        authorization: `Zoho-oauthtoken ${this.accessToken}`,
      },
      data: JSON.stringify({ data: [zohoEvent] }),
    })
      .then((data) => data.data)
      .catch((e) => this.log.error(e, e.response?.data));
  };

  private deleteMeeting = async (uid: string) => {
    return axios({
      method: "delete",
      url: `https://www.zohoapis.com/crm/v3/Events?ids=${uid}`,
      headers: {
        "content-type": "application/json",
        authorization: `Zoho-oauthtoken ${this.accessToken}`,
      },
    })
      .then((data) => data.data)
      .catch((e) => this.log.error(e, e.response?.data));
  };

  private zohoCrmAuth = async (credential: CredentialPayload) => {
    const appKeys = await getAppKeysFromSlug("zohocrm");
    if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id;
    if (typeof appKeys.client_secret === "string") this.client_secret = appKeys.client_secret;
    if (!this.client_id) throw new HttpError({ statusCode: 400, message: "Zoho CRM client_id missing." });
    if (!this.client_secret)
      throw new HttpError({ statusCode: 400, message: "Zoho CRM client_secret missing." });
    const credentialKey = credential.key as unknown as ZohoToken;
    const isTokenValid = (token: ZohoToken) => {
      const isValid = token && token.access_token && token.expiryDate && token.expiryDate < Date.now();
      if (isValid) {
        this.accessToken = token.access_token;
      }
      return isValid;
    };

    const refreshAccessToken = async (credentialKey: ZohoToken) => {
      try {
        const url = `${credentialKey.accountServer}/oauth/v2/token`;
        const formData = {
          grant_type: "refresh_token",
          client_id: this.client_id,
          client_secret: this.client_secret,
          refresh_token: credentialKey.refresh_token,
        };
        const zohoCrmTokenInfo = await axios({
          method: "post",
          url: url,
          data: qs.stringify(formData),
          headers: {
            "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
          },
        });
        if (!zohoCrmTokenInfo.data.error) {
          // set expiry date as offset from current time.
          zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);

          await prisma.credential.update({
            where: {
              id: credential.id,
            },
            data: {
              key: {
                ...(zohoCrmTokenInfo.data as ZohoToken),
                refresh_token: credentialKey.refresh_token,
                accountServer: credentialKey.accountServer,
              },
            },
          });
          this.accessToken = zohoCrmTokenInfo.data.access_token;
          this.log.debug("Fetched token", this.accessToken);
        } else {
          this.log.error(zohoCrmTokenInfo.data);
        }
      } catch (e: unknown) {
        this.log.error(e);
      }
    };

    return {
      getToken: () => (isTokenValid(credentialKey) ? Promise.resolve() : refreshAccessToken(credentialKey)),
    };
  };

  async handleEventCreation(event: CalendarEvent, contacts: CalendarEvent["attendees"]) {
    const meetingEvent = await this.createZohoEvent(event);
    if (meetingEvent.data && meetingEvent.data.length && meetingEvent.data[0].status === "success") {
      this.log.debug("event:creation:ok", { meetingEvent });
      return Promise.resolve({
        uid: meetingEvent.data[0].details.id,
        id: meetingEvent.data[0].details.id,
        type: this.integrationName,
        password: "",
        url: "",
        additionalInfo: { contacts, meetingEvent },
      });
    }
    this.log.debug("meeting:creation:notOk", { meetingEvent, event, contacts });
    return Promise.reject("Something went wrong when creating a meeting in ZohoCRM");
  }

  async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
    const auth = await this.auth;
    await auth.getToken();
    const contacts = await this.contactSearch(event);
    if (contacts.data && contacts.data.length) {
      if (contacts.data.length === event.attendees.length) {
        // all contacts are in Zoho CRM already.
        this.log.debug("contact:search:all", { event, contacts: contacts.data });
        return await this.handleEventCreation(event, event.attendees);
      } else {
        // Some attendees don't exist in ZohoCRM
        // Get the existing contacts' email to filter out
        this.log.debug("contact:search:notAll", { event, contacts });
        const existingContacts = contacts.data.map((contact: ZohoContact) => contact.Email);
        this.log.debug("contact:filter:existing", { existingContacts });
        // Get non existing contacts filtering out existing from attendees
        const nonExistingContacts: Person[] = event.attendees.filter(
          (attendee) => !existingContacts.includes(attendee.email)
        );
        this.log.debug("contact:filter:nonExisting", { nonExistingContacts });
        // Only create contacts in ZohoCRM that were not present in the previous contact search
        const createContacts = await this.createContacts(nonExistingContacts);
        this.log.debug("contact:created", { createContacts });
        // Continue with event creation and association only when all contacts are present in Zoho
        if (createContacts.data?.data[0].status === "success") {
          this.log.debug("contact:creation:ok");
          return await this.handleEventCreation(event, nonExistingContacts.concat(contacts));
        }
        return Promise.reject({
          calError: "Something went wrong when creating non-existing attendees in ZohoCRM",
        });
      }
    } else {
      this.log.debug("contact:search:none", { event, contacts });
      const createContacts = await this.createContacts(event.attendees);
      this.log.debug("contact:created", { createContacts });
      if (createContacts.data?.data[0].status === "success") {
        this.log.debug("contact:creation:ok");
        return await this.handleEventCreation(event, event.attendees);
      }
    }

    return Promise.reject({
      calError: "Something went wrong when searching/creating the attendees in ZohoCRM",
    });
  }

  async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
    const auth = await this.auth;
    await auth.getToken();
    return await this.updateMeeting(uid, event);
  }

  async deleteEvent(uid: string): Promise<void> {
    const auth = await this.auth;
    await auth.getToken();
    return await this.deleteMeeting(uid);
  }

  async getAvailability(
    _dateFrom: string,
    _dateTo: string,
    _selectedCalendars: IntegrationCalendar[]
  ): Promise<EventBusyDate[]> {
    return Promise.resolve([]);
  }

  async listCalendars(_event?: CalendarEvent): Promise<IntegrationCalendar[]> {
    return Promise.resolve([]);
  }
}
