import { ClientResponse } from '@sendgrid/client/src/response';
import { ResponseError } from '@sendgrid/helpers/classes';
import { NextApiRequest, NextApiResponse } from 'next';
import { User, UserWithRoles, Appt, ApptJSON } from '@tutorbook/model';
import { ApptEmail } from '@tutorbook/emails';

import to from 'await-to-js';
import mail from '@sendgrid/mail';
import error from './helpers/error';

import {
  firestore,
  DocumentSnapshot,
  DocumentReference,
  CollectionReference,
} from './helpers/firebase';

mail.setApiKey(process.env.SENDGRID_API_KEY as string);

/**
 * Sends out invite emails to all of the new appointment's attendees with a link
 * to their Bramble room and instructions on how to make the best out of their
 * virtual tutoring session.
 */
async function sendApptEmails(
  appt: Appt,
  attendees: ReadonlyArray<User>
): Promise<void> {
  await Promise.all(
    attendees.map(async (attendee: User) => {
      /* eslint-disable-next-line @typescript-eslint/ban-types */
      const [err] = await to<[ClientResponse, {}], Error | ResponseError>(
        mail.send(new ApptEmail(attendee, appt, attendees))
      );
      const emailStr = `the appt (${appt.id as string}) email`;
      const attendeeStr = `${attendee.name} <${attendee.email}>`;
      if (err) {
        const msg = `[ERROR] ${err.name} sending ${attendeeStr} ${emailStr}:`;
        console.error(msg, err);
      } else {
        console.log(`[DEBUG] Sent ${attendeeStr} ${emailStr}.`);
      }
    })
  );
}

export type CreateApptRes = ApptJSON;

/**
 * Takes an `ApptJSON` object, an authentication token, and:
 * 1. Verifies the correct request body was sent (e.g. all parameters are there
 *    and are all of the correct types).
 * 2. Fetches the given pending request's data from our Firestore database.
 * 3. Performs the following verifications (some of which are also included in
 *    the original `/api/request` endpoint):
 *    - Verifies that the requested `Timeslot` is within all of the `attendee`'s
 *      availability (by reading each `attendee`'s Firestore profile document).
 *      Note that we **do not** throw an error if the sender (i.e. the tutee) is
 *      unavailable.
 *    - Verifies that the requested `subjects` are included in each of the
 *      tutors' Firestore profile documents (where a tutor is defined as an
 *      `attendee` whose `roles` include `tutor`).
 *    - Verifies that the parent (the owner of the given `id`) is actually the
 *      pupil's parent (i.e. the `attendee`s who have the `pupil` role all
 *      include the given `id` in their profile's `parents` field).
 * 4. Deletes the old `request` documents.
 * 5. Creates a new `appt` document containing the request body in each of the
 *    `attendee`'s Firestore `appts` subcollection.
 * 6. Updates each `attendee`'s availability (in their Firestore profile
 *    document) to reflect this appointment (i.e. remove the appointment's
 *    `time` from their availability).
 * 7. Sends each of the `appt`'s `attendee`'s an email containing instructions
 *    for how to access their Bramble virtual-tutoring room.
 *
 * @param {string} request - The path of the pending tutoring lesson's Firestore
 * document to approve (e.g. `partitions/default/users/MKroB319GCfMdVZ2QQFBle8GtCZ2/requests/CEt4uGqTtRg17rZamCLC`).
 * @param {string} id - The user ID of the parent approving the lesson request
 * (e.g. `MKroB319GCfMdVZ2QQFBle8GtCZ2`).
 *
 * @todo Is it really required that we have the parent's user ID? Right now, we
 * only allow pupils to add the contact information of one parent. And we don't
 * really care **which** parent approves the lesson request anyways.
 *
 * @todo Require and check authentication headers for parent's JWT.
 */
export default async function createAppt(
  req: NextApiRequest,
  res: NextApiResponse<CreateApptRes>
): Promise<void> {
  /* eslint-disable @typescript-eslint/no-unsafe-member-access */
  // 1. Verify that the request body is valid.
  if (!req.body) {
    error(res, 'You must provide a request body.');
  } else if (typeof req.body.request !== 'string') {
    error(res, 'Your request body must contain a request field.');
  } else if (typeof req.body.id !== 'string') {
    error(res, 'Your request body must contain a id field.');
  } else {
    // 2. Fetch the lesson request data.
    let ref: DocumentReference | null = null;
    let db: DocumentReference | null = null;
    try {
      ref = firestore.doc(req.body.request);
      // Partition is 4th parent (e.g. `/test/users/PUPIL-DOC/requests/DOC`).
      db = (((ref.parent as CollectionReference).parent as DocumentReference)
        .parent as CollectionReference).parent;
      if (!db) throw new Error('Database partition did not exist.');
    } catch (err) {
      error(res, 'You must provide a valid request document path.', 400, err);
    }
    if (!db || !ref) {
      // Don't do anything b/c we already sent an error code to the client.
    } else {
      const doc: DocumentSnapshot = await ref.get();
      if (!doc.exists) {
        error(
          res,
          'This pending lesson request no longer exists (it was probably ' +
            'already approved).'
        );
      } else {
        // 3. Perform verifications.
        const appt: Appt = Appt.fromFirestore(doc);
        // The Firestore path in `req.body.request` is the path of the parent's
        // child's document (i.e. the request document nested under that child's
        // profile document).
        const pupilUID: string = ((doc.ref.parent as CollectionReference)
          .parent as DocumentReference).id;
        let attendeesIncludePupil = false;
        let pupilIsParentsChild = false;
        let errored = false;
        const attendees: UserWithRoles[] = [];
        await Promise.all(
          appt.attendees.map(async (attendee) => {
            // 3. Verify that the attendees have uIDs.
            if (!attendee.id) {
              error(res, 'All attendees must have valid uIDs.');
              errored = false;
              return;
            }
            const attendeeRef: DocumentReference = (db as DocumentReference)
              .collection('users')
              .doc(attendee.id);
            const attendeeDoc: DocumentSnapshot = await attendeeRef.get();
            // 3. Verify that the attendees exist.
            if (!attendeeDoc.exists) {
              error(res, `Attendee (${attendee.id}) does not exist.`);
              errored = false;
              return;
            }
            const user: User = User.fromFirestore(attendeeDoc);
            if (user.id === pupilUID) {
              // 3. Verify that the pupil is among the appointment's attendees.
              attendeesIncludePupil = true;
              // 3. Verify that the pupil is the parent's child.
              if (user.parents.indexOf(req.body.id) < 0) {
                error(
                  res,
                  `${user.toString()} is not (${
                    req.body.id as string
                  })'s child.`
                );
              } else {
                pupilIsParentsChild = true;
              }
            }
            // 3. Verify the tutors can teach the requested subjects.
            const isTutor: boolean = attendee.roles.indexOf('tutor') >= 0;
            const isMentor: boolean = attendee.roles.indexOf('mentor') >= 0;
            const canTutorSubject: (s: string) => boolean = (
              subject: string
            ) => {
              return user.tutoring.subjects.includes(subject);
            };
            const canMentorSubject: (s: string) => boolean = (
              subject: string
            ) => {
              return user.mentoring.subjects.includes(subject);
            };
            if (isTutor && !appt.subjects.every(canTutorSubject)) {
              error(res, `${user.toString()}) cannot tutor these subjects.`);
              errored = false;
              return;
            }
            if (isMentor && !appt.subjects.every(canMentorSubject)) {
              error(res, `${user.toString()}) cannot mentor these subjects.`);
              errored = false;
              return;
            }
            // 3. Verify that the tutor and mentor attendees are available.
            if (
              appt.time &&
              (isTutor || isMentor) &&
              !user.availability.contains(appt.time)
            ) {
              error(
                res,
                `${user.toString()} isn't available on ${appt.time.toString()}.`
              );
              errored = false;
              return;
            }
            (user as UserWithRoles).roles = attendee.roles;
            attendees.push(user as UserWithRoles);
          })
        );
        if (errored) {
          // Don't do anything b/c we already sent an error code to the client.
        } else if (!pupilIsParentsChild) {
          // Don't do anything b/c we already sent an error code to the client.
        } else if (!attendeesIncludePupil) {
          error(res, `Parent's pupil (${pupilUID}) must attend the appt.`);
        } else {
          console.log(`[DEBUG] Creating appt (${appt.id as string})...`);
          await Promise.all(
            attendees.map(async (attendee: UserWithRoles) => {
              if (
                attendee.roles.indexOf('tutee') >= 0 ||
                attendee.roles.indexOf('mentee') >= 0
              ) {
                // 4. Delete the old request documents.
                await (attendee.ref as DocumentReference)
                  .collection('requests')
                  .doc(appt.id as string)
                  .delete();
              }
              // 5. Create the appointment Firestore document.
              await (attendee.ref as DocumentReference)
                .collection('appts')
                .doc(appt.id as string)
                .set(appt.toFirestore());
              // 6. Update the attendees availability.
              if (appt.time) attendee.availability.remove(appt.time);
              await (attendee.ref as DocumentReference).update(
                attendee.toFirestore()
              );
            })
          );
          // 7. Send out the invitation email to the attendees.
          await sendApptEmails(appt, attendees);
          res.status(201).json(appt.toJSON());
          console.log(`[DEBUG] Created appt (${appt.id as string}).`);
        }
      }
    }
  }
  /* eslint-enable @typescript-eslint/no-unsafe-member-access */
}
