// Import libraries
import uuid from 'uuid';
import oauth from 'oauth-signature';
import CryptoJS from 'crypto-js';

// Import caccl modules
import CACCLError from 'caccl-error';

// Import local modules
import ErrorCode from './shared/types/ErrorCode';

/*------------------------------------------------------------------------*/
/*                                 Helpers                                */
/*------------------------------------------------------------------------*/

/**
 * Encodes headers for sending
 * @author Gabe Abrams
 * @param str the text of the header to encode
 * @returns encoded text
 */
const encode = (str: string): string => {
  return (
    encodeURIComponent(str)
      .replace(/[!'()]/g, escape)
      .replace(/\*/g, '%2A')
  );
};

/**
 * Post-processes the text response and preps it for XML
 * @author Gabe Abrams
 * @param text the text of the response
 * @returns the post-processed text
 */
const postProcessText = (text: string): string => {
  // Use CDATA and encode newlines for Canvas
  return `<![CDATA[${text.replace(/\n/g, '<br />')}]]>`;
};

/*------------------------------------------------------------------------*/
/*                                  Main                                  */
/*------------------------------------------------------------------------*/

/**
 * Submits a grade passback request to Canvas
 * @author Gabe Abrams
 * @param opts object containing all arguments
 * @param opts.request an object containing all the information for the
 *   passback request
 * @param [opts.request.text] the text of the submission. If this is
 *   included, url cannot be included
 * @param [opts.request.url] a url to send as the student's submission.
 *   If this is included, text cannot be included
 * @param [opts.request.score] the student's score on this assignment
 * @param [opts.request.percent] the student's score as a percent (0-100)
 *   on the assignment
 * @param [opts.request.submittedAt=now] a timestamp for when the
 *   student submitted the grade. The type must either be a Date object or an
 *   ISO 8601 formatted string
 * @param opts.info an object containing all LTI info required for the
 *   grade passback process
 * @param opts.info.sourcedId the LTI sourcedid
 * @param opts.info.url the LTI outcome service url
 * @param opts.credentials an object containing the app's credentials
 * @param opts.credentials.consumerKey the app's consumer key
 * @param opts.credentials.consumerSecret the app's consumer secret
 * @returns true if the request was successful
 */
const handlePassback = async (
  opts: {
    request: {
      text?: string,
      url?: string,
      score?: number,
      percent?: number,
      submittedAt?: (Date | string),
    },
    info: {
      sourcedId: string,
      url: string,
    },
    credentials: {
      consumerKey: string,
      consumerSecret: string,
    },
  },
): Promise<boolean> => {
  const {
    request,
    info,
    credentials,
  } = opts;

  /* --------------- Pre-processing and Verification -------------- */
  // Enforce constraints
  if (request.text && request.url) {
    throw new CACCLError({
      message: 'We could not send a grade passback to Canvas because both a text and url submission were included (only one is allowed).',
      code: ErrorCode.TooManySubmissionValues,
    });
  }
  if (request.score && request.percent) {
    throw new CACCLError({
      message: 'We could not send a grade passback to Canvas because both a score and grade percent were included (only one is allowed).',
      code: ErrorCode.TooManyScores,
    });
  }

  // Determine the submission type and extract the submission
  let submissionType;
  if (request.text) {
    submissionType = 'text';
  } else if (request.url) {
    submissionType = 'url';
  }
  const submission = (request.text || request.url);
  // > Format the submittedAt timestamp
  let submittedAt;
  if (request.submittedAt) {
    submittedAt = (
      typeof submittedAt === 'string'
        ? submittedAt
        : (submittedAt as any).toISOString()
    );
  }

  // Extract score
  const { score, percent } = request;

  // Deconstruct info
  const { sourcedId } = info;
  const outcomeURL = info.url;

  // Deconstruct credentials
  const { consumerKey, consumerSecret } = credentials;

  /* ------------------------ XML Building ------------------------ */
  // Score
  let scoreBlock = '';
  if (score) {
    scoreBlock = (
      `
         <resultTotalScore>
           <language>en</language>
           <textString>${score}</textString>
         </resultTotalScore>`
    );
  }

  // Percent of grade
  let percentBlock = '';
  if (percent) {
    percentBlock = (
      `
          <resultScore>
            <language>en</language>
            <textString>${percent / 100}</textString>
          </resultScore>`
    );
  }

  // Submission
  let subBlock = '';
  if (submissionType) {
    subBlock = (
      submissionType === 'url'
        ? (
          `
          <resultData>
            <url>${submission}</url>
          </resultData>`
        )
        : (
          `
          <resultData>
            <text>${postProcessText(submission)}</text>
          </resultData>`
        )
    );
  }

  // Timestamp
  const timestampBlock = (
    submittedAt
      ? (
        `
        <submissionDetails>
          <submittedAt>
            ${submittedAt}
          </submittedAt>
        </submissionDetails>`
      )
      : ''
  );

  // Put all the pieces together into one XML
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
  <imsx_POXHeader>
    <imsx_POXRequestHeaderInfo>
      <imsx_version>V1.0</imsx_version>
      <imsx_messageIdentifier>${uuid.v1()}</imsx_messageIdentifier>
    </imsx_POXRequestHeaderInfo>
  </imsx_POXHeader>
  <imsx_POXBody>
    <replaceResultRequest>${timestampBlock}
      <resultRecord>
        <sourcedGUID>
          <sourcedId>${sourcedId}</sourcedId>
        </sourcedGUID>
        <result>${percentBlock}${scoreBlock}${subBlock}
        </result>
      </resultRecord>
    </replaceResultRequest>
  </imsx_POXBody>
</imsx_POXEnvelopeRequest>`;

  /* ---------------------- Sign the Request ---------------------- */

  // Build the oauth headers
  const oauthNonce = uuid.v4();
  const oauthTimestamp = Math.round(Date.now() / 1000);
  const bodyHash = CryptoJS.SHA1(xml).toString(CryptoJS.enc.Base64);

  // Put oauth headers into one object
  const oauthHeaders = {
    oauth_version: '1.0',
    oauth_nonce: oauthNonce,
    oauth_timestamp: oauthTimestamp,
    oauth_consumer_key: consumerKey,
    oauth_body_hash: bodyHash,
    oauth_signature_method: 'HMAC-SHA1',
  };

  // Sign the oauth headers
  const oauthSignature = (
    oauth.generate(
      'POST',
      outcomeURL,
      oauthHeaders,
      consumerSecret
    )
  );

  // Create request headers
  const headers = {
    Authorization: `OAuth realm="",oauth_version="1.0",oauth_nonce="${encode(oauthNonce)}",oauth_timestamp="${encode(String(oauthTimestamp))}",oauth_consumer_key="${encode(consumerKey)}",oauth_body_hash="${encode(bodyHash)}",oauth_signature_method="HMAC-SHA1",oauth_signature="${oauthSignature}"`,
    'Content-Type': 'application/xml',
    'Content-Length': String(xml.length),
  };

  // Send the request to Canvas
  try {
    await fetch(
      outcomeURL,
      {
        method: 'POST',
        headers,
        body: xml,
      },
    );

    // Success! Resolve with true
    return true;
  } catch (err) {
    // Failure! Throw an error
    throw new CACCLError({
      message: `We could not pass submission data back to Canvas because we encountered a ${err.response.status} error (${err.response.statusText}).`,
      code: ErrorCode.PassbackRequestError,
    });
  }
};

export default handlePassback;
