package main

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"net/url"
	"strings"
	"time"

	"github.com/fastly/compute-sdk-go/edgedict"
	"github.com/fastly/compute-sdk-go/fsthttp"
)

// BackendName is the name of our service backend.
const BackendS3 = "origin_0"

// Hash of empty string, used for authentication string generation
// caculated from hex::encode(Hash::hash("".as_bytes()));
const EMPTY_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

// / Google Cloud Storage Configuration for Interoperability API
type S3Config struct {
	access_key_id     string
	secret_access_key string
	region            string
	host              string
}

func (s *S3Config) Set() error {
	dict, err := edgedict.Open("s3_config")
	if err != nil {
		return err
	}
	access_key_id, err := dict.Get("access_key_id")
	if err != nil {
		return fmt.Errorf("access_key_id %s", err)
	}
	s.access_key_id = access_key_id
	secret_access_key, err := dict.Get("secret_access_key")
	if err != nil {
		return fmt.Errorf("secret_access_key %s", err)
	}
	s.secret_access_key = secret_access_key
	region, err := dict.Get("region")
	if err != nil {
		return fmt.Errorf("region %s", err)
	}
	s.region = region
	host, err := dict.Get("host")
	if err != nil {
		return fmt.Errorf("host %s", err)
	}
	s.host = host
	return nil
}

func authorize_s3_request(r *fsthttp.Request, s3 S3Config) *fsthttp.Request {
	canonical_querystring := ""
	x_amz_content_sha256 := EMPTY_HASH
	amz_date := time.Now().Format("20060102T150405Z")
	path_url_decoded, _ := url.QueryUnescape(r.URL.Path)
	path_url_encoded := url.QueryEscape(path_url_decoded)
	canonical_uri := strings.Replace(path_url_encoded, "%2F", "/", -1)
	authorization_value := generate_s3_authorization_header(s3, canonical_uri, canonical_querystring, x_amz_content_sha256, "GET", amz_date, "s3")
	r.Method = "GET"
	r.Header.Set("host", s3.host)
	r.Header.Set("AUTHORIZATION", authorization_value)
	r.Header.Set("x-amz-content-sha256", x_amz_content_sha256)
	r.Header.Set("x-amz-date", amz_date)

	return r
}

func generate_s3_authorization_header(s3 S3Config, canonical_uri string, canonical_querystring string, x_amz_content_sha256 string, method string, amz_date string, service string) string {
	canonical_header := "host:" + s3.host + "\n" + "x-amz-content-sha256:" + x_amz_content_sha256 + "\n" + "x-amz-date:" + amz_date + "\n"
	signed_headers := "host;x-amz-content-sha256;x-amz-date"
	canonical_request := method + "\n" + canonical_uri + "\n" + canonical_querystring + "\n" + canonical_header + "\n" + signed_headers + "\n" + x_amz_content_sha256
	credential_date := amz_date[:8]
	credential_scope := credential_date + "/" + s3.region + "/" + service + "/" + "aws4_request"
	canonical_request_hash := hex.EncodeToString(getSHA256Binary(canonical_request))
	string_to_sign := "AWS4-HMAC-SHA256" + "\n" + amz_date + "\n" + credential_scope + "\n" + canonical_request_hash
	aws4_secret_string := "AWS4" + s3.secret_access_key
	aws4_secert := []byte(aws4_secret_string)
	date_key := sign(aws4_secert, credential_date)
	region_key := sign(date_key, s3.region)
	service_key := sign(region_key, service)
	signing_key := sign(service_key, "aws4_request")
	signature := hex.EncodeToString(sign(signing_key, string_to_sign))
	return "AWS4-HMAC-SHA256 Credential=" + s3.access_key_id + "/" + credential_scope + ",SignedHeaders=" + signed_headers + ",Signature=" + signature
}

func sign(key []byte, message string) []byte {
	h := hmac.New(sha256.New, key)
	h.Write([]byte(message))
	return h.Sum(nil)
}

func main() {
	fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {
		var s3 S3Config
		err := s3.Set()
		if err != nil {
			w.WriteHeader(fsthttp.StatusInternalServerError)
			fmt.Fprintln(w, err.Error())
			return
		}
		position := strings.Index(r.URL.String(), "?")
		new_url := r.URL.String()
		if position > 0 {
			new_url = new_url[0:position]
		}
		r.URL, err = url.Parse(new_url)
		if err != nil {
			w.WriteHeader(fsthttp.StatusInternalServerError)
			fmt.Fprintln(w, err.Error())
		}
		switch r.Method {
		case "GET", "HEAD":
			r = authorize_s3_request(r, s3)
		case "PURGE":
		default:
			w.WriteHeader(fsthttp.StatusNotFound)
			return
		}
		fmt.Printf("Sending: %v, Path: %v", r.URL, r.URL.Path)
		resp, err := r.Send(ctx, BackendS3)
		fmt.Println("Sent")
		if err != nil {
			w.WriteHeader(fsthttp.StatusBadGateway)
			fmt.Fprintln(w, err.Error)
			return
		}

		w.Header().Reset(resp.Header)
		w.WriteHeader(resp.StatusCode)
		io.Copy(w, resp.Body)
	})
}

func getSHA256Binary(s string) []byte {
	r := sha256.Sum256([]byte(s))
	return r[:]
}