// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

// [START functions_slack_setup]
const functions = require('@google-cloud/functions-framework');
const google = require('@googleapis/kgsearch');
const crypto = require('crypto');

// Get a reference to the Knowledge Graph Search component
const kgsearch = google.kgsearch('v1');
// [END functions_slack_setup]

// [START functions_slack_format]
/**
 * Format the Knowledge Graph API response into a richly formatted Slack message.
 *
 * @param {string} query The user's search query.
 * @param {object} response The response from the Knowledge Graph API.
 * @returns {object} The formatted message.
 */
const formatSlackMessage = (query, response) => {
  let entity;

  // Extract the first entity from the result list, if any
  if (
    response &&
    response.data &&
    response.data.itemListElement &&
    response.data.itemListElement.length > 0
  ) {
    entity = response.data.itemListElement[0].result;
  }

  // Prepare a rich Slack message
  // See https://api.slack.com/docs/message-formatting
  const slackMessage = {
    response_type: 'in_channel',
    text: `Query: ${query}`,
    attachments: [],
  };

  if (entity) {
    const attachment = {
      color: '#3367d6',
    };
    if (entity.name) {
      attachment.title = entity.name;
      if (entity.description) {
        attachment.title = `${attachment.title}: ${entity.description}`;
      }
    }
    if (entity.detailedDescription) {
      if (entity.detailedDescription.url) {
        attachment.title_link = entity.detailedDescription.url;
      }
      if (entity.detailedDescription.articleBody) {
        attachment.text = entity.detailedDescription.articleBody;
      }
    }
    if (entity.image && entity.image.contentUrl) {
      attachment.image_url = entity.image.contentUrl;
    }
    slackMessage.attachments.push(attachment);
  } else {
    slackMessage.attachments.push({
      text: 'No results match your query...',
    });
  }

  return slackMessage;
};
// [END functions_slack_format]

// [START functions_verify_webhook]
/**
 * Verify that the webhook request came from Slack.
 *
 * @param {object} req Cloud Function request object.
 * @param {string} req.headers Headers Slack SDK uses to authenticate request.
 * @param {string} req.rawBody Raw body of webhook request to check signature against.
 */
const verifyWebhook = req => {
  const signingSecret = process.env.SLACK_SECRET;
  const requestSignature = req.headers['x-slack-signature'];
  const requestTimestamp = req.headers['x-slack-request-timestamp'];
  const requestBody = req.rawBody;

  if (!requestSignature || !requestTimestamp) {
    const err = new Error('Missing Slack validation headers.');
    err.code = 400;
    throw err;
  }

  if (!signingSecret) {
    const err = new Error(
      'Server configuration error: SLACK_SECRET is missing.'
    );
    err.code = 500;
    throw err;
  }

  // Prevent replay attacks by verifying the timestamp is recent
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(requestTimestamp)) > 60 * 5) {
    const err = new Error('Slack request timestamp is too old.');
    err.code = 401;
    throw err;
  }

  const hmac = crypto.createHmac('sha256', signingSecret);
  hmac.update('v0:' + requestTimestamp + ':', 'utf8');
  hmac.update(requestBody || '');
  const expectedSignature = 'v0=' + hmac.digest('hex');

  const sigBuffer = Buffer.from(requestSignature, 'utf8');
  const expBuffer = Buffer.from(expectedSignature, 'utf8');

  if (
    sigBuffer.length !== expBuffer.length ||
    !crypto.timingSafeEqual(sigBuffer, expBuffer)
  ) {
    const err = new Error('Invalid Slack signature.');
    err.code = 401;
    throw err;
  }
};
// [END functions_verify_webhook]

// [START functions_slack_request]
/**
 * Send the user's search query to the Knowledge Graph API.
 *
 * @param {string} query The user's search query.
 */
const makeSearchRequest = async query => {
  const response = await kgsearch.entities.search({
    auth: process.env.KG_API_KEY,
    query,
    limit: 1,
  });
  return formatSlackMessage(query, response);
};
// [END functions_slack_request]

// [START functions_slack_search]
/**
 * Receive a Slash Command request from Slack.
 *
 * Trigger this function by creating a Slack slash command with the HTTP Trigger URL.
 * You can find the HTTP URL in the Cloud Console or using `gcloud functions describe`
 *
 * @param {object} req Cloud Function request object.
 * @param {object} req.body The request payload.
 * @param {string} req.rawBody Raw request payload used to validate Slack's message signature.
 * @param {string} req.body.text The user's search query.
 * @param {object} res Cloud Function response object.
 */
functions.http('kgSearch', async (req, res) => {
  try {
    if (req.method !== 'POST') {
      const error = new Error('Only POST requests are accepted');
      error.code = 405;
      throw error;
    }

    if (!req.body.text) {
      const error = new Error('No text found in body.');
      error.code = 400;
      throw error;
    }

    // Verify that this request came from Slack
    verifyWebhook(req);

    // Make the request to the Knowledge Graph Search API
    const response = await makeSearchRequest(req.body.text);

    // Send the formatted message back to Slack
    res.json(response);
  } catch (err) {
    console.error(err);
    res.status(err.code || 500).send(err.message || 'Internal Server Error');
  }
});
// [END functions_slack_search]
