/// <reference types="@fastly/js-compute" />
import * as crypto from 'crypto-js';
import { formatISO } from 'date-fns';
import { ConfigStore } from "fastly:config-store";

addEventListener('fetch', event => event.respondWith(handleRequest(event)));

async function handleRequest(event) {
  // Ignore the query string from client
  let req = removeQuery(event.request);
  
  // Only generate authorize header for GET and HEAD request
  // Pass PURGE to backend as it is
  // Block other kinds of method
  if (req.method === 'GET' || req.method === 'HEAD') {
    authorizeRequest(req);

    let res = await fetch(req, {
      backend: 'gcs_backend',
    });

    // Remove some headers returned from GCS
    res.headers.delete('x-guploader-uploadid');
    res.headers.delete('x-goog-hash');
    res.headers.delete('x-goog-storage-class');
    res.headers.delete('server');
    res.headers.delete('alt-svc');

    return res;
  } else if (req.method === 'PURGE') {
    // When doing the purge, we need to make sure the cache key matches
    // Cache key is cacualted from URL and HOST header of request sent to backend
    // URL of backend request has no query string
    req.headers.set('host', getConfig().HOST);

    return await fetch(req, {
      backend: 'gcs_backend',
    });
  } else {
    return new Response("This method is not allowed", { status: 405 });
  }
}

// Call the tasks and build the request with the authorization header
function authorizeRequest(req) {
  const cfg = getConfig();
  const timeStampISO8601Format = formatISO(new Date(), { format: 'basic' });
  const YYYYMMDD = timeStampISO8601Format.slice(0, 8);

  const canonicalRequest = generateCanonicalRequest(req, timeStampISO8601Format);
  console.log(`canonicalRequest = ${canonicalRequest}`);

  const stringToSign = generateStringToSign(timeStampISO8601Format, YYYYMMDD, canonicalRequest);
  console.log(`stringToSign = ${stringToSign}`);

  const signature = generateSignature(YYYYMMDD, stringToSign);
  console.log(`signature = ${signature}`);

  const authorizationValue = `GOOG4-HMAC-SHA256 Credential=${[
    cfg.ACCESS_ID,
    YYYYMMDD,
    cfg.GCS_REGION,
    cfg.GCS_SERVICE,
    'goog4_request'
  ].join('/')},SignedHeaders=${cfg.SIGNED_HEADERS},Signature=${signature}`;
  console.log(`authorizationValue = ${authorizationValue}`);

  req.headers.set('host', cfg.HOST);
  req.headers.set('authorization', authorizationValue);
  req.headers.set('x-goog-content-sha256',cfg.EMPTY_HASH);
  req.headers.set('x-goog-date', timeStampISO8601Format);
}

// Task 1: Create a Canonical Request
// https://cloud.google.com/storage/docs/authentication/canonical-requests
function generateCanonicalRequest(req, timeStampISO8601Format) {
  const httpMethod = req.method;

  // Do url decode and re-encode in case some client are not do url encoding.
  // https://cloud.google.com/storage/docs/authentication/canonical-requests#about-resource-path
  const url = new URL(req.url);
  const decoded = decodeURIComponent(url.pathname);
  const encoded = encodeURIComponent(decoded);
  const canonicalUri = encoded.replaceAll('%2F', '/');

  const cfg = getConfig();
  const canonicalQuery = '';
  const canonicalHeaders = `host:${cfg.HOST}\nx-goog-content-sha256:${cfg.EMPTY_HASH}\nx-goog-date:${timeStampISO8601Format}\n`;

  return `${httpMethod}\n${canonicalUri}\n${canonicalQuery}\n${canonicalHeaders}\n${cfg.SIGNED_HEADERS}\n${cfg.EMPTY_HASH}`;
}

// Task 2: Create a String to Sign
// https://cloud.google.com/storage/docs/authentication/signatures#string-to-sign
function generateStringToSign(timeStampISO8601Format, YYYYMMDD, canonicalRequest) {
  const cfg = getConfig();
  const scope = `${YYYYMMDD}/${cfg.GCS_REGION}/${cfg.GCS_SERVICE}/goog4_request`;
  const hashedCanonicalRequest = crypto.SHA256(canonicalRequest);

  return `GOOG4-HMAC-SHA256\n${timeStampISO8601Format}\n${scope}\n${hashedCanonicalRequest}`;
}

// Task 3: Calculate Signature
// https://cloud.google.com/storage/docs/authentication/signatures#signing-process
function generateSignature(YYYYMMDD, stringToSign) {
  const cfg = getConfig();
  const round1 = hmacSha256('GOOG4' + cfg.SECRET, YYYYMMDD);
  const round2 = hmacSha256(round1, cfg.GCS_REGION);
  const round3 = hmacSha256(round2, cfg.GCS_SERVICE);
  const round4 = hmacSha256(round3, 'goog4_request');

  return hmacSha256(round4, stringToSign);
}

function removeQuery(req) {
  let url = new URL(req.url);
  url.search = new URLSearchParams();

  return new Request(url.toJSON(), req);
}

function hmacSha256(signingKey, stringToSign) {
    return crypto.HmacSHA256(stringToSign, signingKey, { asBytes: true });
}

function getConfig() {
  const config = new ConfigStore('gcs_config');
  return {
    // Hash of empty string, used for authentication string generation
    // caculated from crypto.SHA256('');
    EMPTY_HASH: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
    // The GCS access id that is linked to a specific service or user account.
    ACCESS_ID: config.get('access_key_id'),
    // A 40-character Base-64 encoded string that is linked to the access ID above.
    SECRET: config.get('secret_access_key'),
    GCS_REGION: config.get('region'),
    GCS_SERVICE: 'storage',
    GCS_BUCKET_NAME: config.get('bucket'),
    HOST: `${config.get('bucket')}.storage.googleapis.com`,
    SIGNED_HEADERS: 'host;x-goog-content-sha256;x-goog-date',
  };
}