package crypto

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/binary"
	"errors"
	"time"
)

var (
	errMACExpired = errors.New("mac: expired")
	errMACInvalid = errors.New("mac: the value is not valid")
)

const (
	macLen  = 32 // sha256 hash size
	timeLen = 8  // int64 for unix timestamp in seconds
)

// MACConfig contains all the options to encode or decode a message along with
// a proof of integrity and authenticity.
//
// Key is the secret used for the HMAC key. It should contain at least 16 bytes
// and should be generated by a PRNG.
//
// Name is an optional message name that won't be contained in the MACed
// messaged itself but will be MACed against.
type MACConfig struct {
	Name   string
	MaxAge time.Duration
	MaxLen int
}

// EncodeAuthMessage associates the given value with a message authentication
// code for integrity and authenticity.
//
// If the value, when base64 encoded with a fixed size header is longer than
// the configured maximum length, it will panic.
//
// Message format (name prefix is in MAC but removed from message):
//
//  <---------------- MAC input ---------------->
//                           <---------- Message ---------->
//  | name | additional data |    time |  value  |     hmac |
//  | ---- |       ---       | 8 bytes |   ---   | 32 bytes |
//
func EncodeAuthMessage(c MACConfig, key, value, additionalData []byte) ([]byte, error) {
	// Create message with MAC
	preludeLen := len(c.Name) + len(additionalData)
	messageAndMACLen := timeLen + len(value) + macLen
	totalCap := preludeLen + messageAndMACLen

	buf := make([]byte, 0, totalCap)

	// Append name and additional data if any
	if len(c.Name) > 0 {
		buf = append(buf, c.Name...)
	}
	if len(additionalData) > 0 {
		buf = append(buf, additionalData...)
	}

	// Append timestamp.
	// Increase the len of the buffer of 8 bytes; its capacity allows it.
	buf = buf[:preludeLen+timeLen]
	binary.BigEndian.PutUint64(buf[preludeLen:], uint64(Timestamp()))

	// Append value if any
	if len(value) > 0 {
		buf = append(buf, value...)
	}

	// Append MAC signature
	buf = append(buf, createMAC(key, buf)...)

	// Skip name and additional data prelude
	buf = buf[preludeLen:]

	// Check length
	if c.MaxLen > 0 {
		if base64.RawURLEncoding.EncodedLen(len(buf)) > c.MaxLen {
			panic("the value is too long")
		}
	}

	// Encode to base64
	return Base64Encode(buf), nil
}

// DecodeAuthMessage verifies a message authentified with message
// authentication code and returns the message value algon with the issued time
// of the message.
func DecodeAuthMessage(c MACConfig, key, enc, additionalData []byte) ([]byte, error) {
	// Check length
	if c.MaxLen > 0 {
		if len(enc) > c.MaxLen {
			return nil, errMACInvalid
		}
	}

	preludeLen := len(c.Name) + len(additionalData)
	decCap := base64.RawURLEncoding.DecodedLen(len(enc))
	totalCap := decCap + preludeLen

	// decCap is the maximum size of the decoded value. The real decoded size may
	// be inferior. This is an early check only.
	if decCap < macLen+timeLen {
		return nil, errMACInvalid
	}

	buf := make([]byte, 0, totalCap)

	// Prepend name and additional data if any
	if len(c.Name) > 0 {
		buf = append(buf, c.Name...)
	}
	if len(additionalData) > 0 {
		buf = append(buf, additionalData...)
	}

	// Decode the base64-encoded MAC
	{
		// Increase len of buffer to write the decoded value, its capacity allows
		// it, using the maximum buffer size calculated.
		buf = buf[:preludeLen+decCap]
		decLen, err := base64.RawURLEncoding.Decode(buf[preludeLen:], enc)
		if err != nil {
			return nil, errMACInvalid
		}
		// We re-check the len of the buffer, that is already checked against the
		// maximum size of the decoded value `decCap`.
		if decLen < macLen+timeLen {
			return nil, errMACInvalid
		}
		// Shrink the buffer back to the exact size of the base64 decoded value.
		buf = buf[:preludeLen+decLen]
	}

	// Verify message with MAC
	{
		mac := buf[len(buf)-macLen:]
		buf = buf[:len(buf)-macLen]
		if !verifyMAC(key, buf, mac) {
			return nil, errMACInvalid
		}
	}

	// Skip hidden prefix
	buf = buf[preludeLen:]

	// Read time and verify time ranges
	var timeBuf []byte
	timeBuf, buf = buf[:timeLen], buf[timeLen:]
	if c.MaxAge != 0 {
		t := time.Unix(int64(binary.BigEndian.Uint64(timeBuf)), 0)
		if t.Add(c.MaxAge).Before(time.Now()) {
			return nil, errMACExpired
		}
	}

	// Returns the value
	return buf, nil
}

// createMAC creates a MAC with HMAC-SHA256
func createMAC(key, value []byte) []byte {
	mac := hmac.New(sha256.New, key)
	_, _ = mac.Write(value)
	return mac.Sum(nil)
}

// verifyMAC returns true is the MAC is valid
func verifyMAC(key, value []byte, mac []byte) bool {
	expectedMAC := createMAC(key, value)
	return hmac.Equal(mac, expectedMAC)
}
