package api

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net/http"
	"sort"
	"strings"
	"time"
)

// SecurityProviderExoscale represents an Exoscale public API security
// provider.
type SecurityProviderExoscale struct {
	// ReqExpire represents the request expiration duration.
	ReqExpire time.Duration

	apiKey    string
	apiSecret string
}

// NewSecurityProvider returns a new Exoscale public API security
// provider to sign API requests using the specified API key/secret.
func NewSecurityProvider(apiKey, apiSecret string) (*SecurityProviderExoscale, error) {
	if apiKey == "" {
		return nil, errors.New("missing API key")
	}

	if apiSecret == "" {
		return nil, errors.New("missing API secret")
	}

	return &SecurityProviderExoscale{
		ReqExpire: 10 * time.Minute,
		apiKey:    apiKey,
		apiSecret: apiSecret,
	}, nil
}

// Intercept is an HTTP middleware that intercepts and signs client requests
// before sending them to the API endpoint.
func (s *SecurityProviderExoscale) Intercept(_ context.Context, req *http.Request) error {
	return s.signRequest(req, time.Now().UTC().Add(s.ReqExpire))
}

func (s *SecurityProviderExoscale) signRequest(req *http.Request, expiration time.Time) error {
	var (
		sigParts    []string
		headerParts []string
	)

	// Request method/URL path
	sigParts = append(sigParts, fmt.Sprintf("%s %s", req.Method, req.URL.EscapedPath()))
	headerParts = append(headerParts, "EXO2-HMAC-SHA256 credential="+s.apiKey)

	// Request body if present
	body := ""
	if req.Body != nil {
		data, err := io.ReadAll(req.Body)
		if err != nil {
			return err
		}
		err = req.Body.Close()
		if err != nil {
			return err
		}
		body = string(data)
		req.Body = io.NopCloser(bytes.NewReader(data))
	}
	sigParts = append(sigParts, body)

	// Request query string parameters
	// Important: this is order-sensitive, we have to have to sort parameters alphabetically to ensure signed
	// values match the names listed in the "signed-query-args=" signature pragma.
	signedParams, paramsValues := extractRequestParameters(req)
	sigParts = append(sigParts, paramsValues)
	if len(signedParams) > 0 {
		headerParts = append(headerParts, "signed-query-args="+strings.Join(signedParams, ";"))
	}

	// Request headers -- none at the moment
	// Note: the same order-sensitive caution for query string parameters applies to headers.
	sigParts = append(sigParts, "")

	// Request expiration date (UNIX timestamp, no line return)
	sigParts = append(sigParts, fmt.Sprint(expiration.Unix()))
	headerParts = append(headerParts, "expires="+fmt.Sprint(expiration.Unix()))

	h := hmac.New(sha256.New, []byte(s.apiSecret))
	if _, err := h.Write([]byte(strings.Join(sigParts, "\n"))); err != nil {
		return err
	}
	headerParts = append(headerParts, "signature="+base64.StdEncoding.EncodeToString(h.Sum(nil)))

	req.Header.Set("Authorization", strings.Join(headerParts, ","))

	return nil
}

// extractRequestParameters returns the list of request URL parameters names
// and a strings concatenating the values of the parameters.
func extractRequestParameters(req *http.Request) ([]string, string) {
	var (
		names  []string
		values string
	)

	for param, values := range req.URL.Query() {
		// Keep only parameters that hold exactly 1 value (i.e. no empty or multi-valued parameters)
		if len(values) == 1 {
			names = append(names, param)
		}
	}
	sort.Strings(names)

	for _, param := range names {
		values += req.URL.Query().Get(param)
	}

	return names, values
}
