// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package pki

import (
	"bytes"
	"crypto"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/go-jose/go-jose/v3"
)

var AllowedOuterJWSTypes = map[string]interface{}{
	"RS256":  true,
	"RS384":  true,
	"RS512":  true,
	"PS256":  true,
	"PS384":  true,
	"PS512":  true,
	"ES256":  true,
	"ES384":  true,
	"ES512":  true,
	"EdDSA2": true,
}

var AllowedEabJWSTypes = map[string]interface{}{
	"HS256": true,
	"HS384": true,
	"HS512": true,
}

// This wraps a JWS message structure.
type jwsCtx struct {
	Algo     string          `json:"alg"`
	Kid      string          `json:"kid"`
	Jwk      json.RawMessage `json:"jwk"`
	Nonce    string          `json:"nonce"`
	Url      string          `json:"url"`
	Key      jose.JSONWebKey `json:"-"`
	Existing bool            `json:"-"`
}

func (c *jwsCtx) GetKeyThumbprint() (string, error) {
	keyThumbprint, err := c.Key.Thumbprint(crypto.SHA256)
	if err != nil {
		return "", fmt.Errorf("failed creating thumbprint: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(keyThumbprint), nil
}

func UnmarshalEabJwsJson(eabBytes []byte) (*jwsCtx, error) {
	var eabJws jwsCtx
	var err error
	if err = json.Unmarshal(eabBytes, &eabJws); err != nil {
		return nil, err
	}

	if eabJws.Kid == "" {
		return nil, fmt.Errorf("invalid header: got missing required field 'kid': %w", ErrMalformed)
	}

	if _, present := AllowedEabJWSTypes[eabJws.Algo]; !present {
		return nil, fmt.Errorf("invalid header: unexpected value for 'algo': %w", ErrMalformed)
	}

	return &eabJws, nil
}

func (c *jwsCtx) UnmarshalOuterJwsJson(a *acmeState, ac *acmeContext, jws []byte) error {
	var err error
	if err = json.Unmarshal(jws, c); err != nil {
		return err
	}

	if c.Kid != "" && len(c.Jwk) > 0 {
		// See RFC 8555 Section 6.2. Request Authentication:
		//
		// > The "jwk" and "kid" fields are mutually exclusive.  Servers MUST
		// > reject requests that contain both.
		return fmt.Errorf("invalid header: got both account 'kid' and 'jwk' in the same message; expected only one: %w", ErrMalformed)
	}

	if c.Kid == "" && len(c.Jwk) == 0 {
		// See RFC 8555 Section 6.2. Request Authentication:
		//
		// > Either "jwk" (JSON Web Key) or "kid" (Key ID) as specified
		// > below
		return fmt.Errorf("invalid header: got neither required fields of 'kid' nor 'jwk': %w", ErrMalformed)
	}

	if _, present := AllowedOuterJWSTypes[c.Algo]; !present {
		// See RFC 8555 Section 6.2. Request Authentication:
		//
		// > The JWS Protected Header MUST include the following fields:
		// >
		// > - "alg" (Algorithm)
		// >
		// >   * This field MUST NOT contain "none" or a Message
		// >     Authentication Code (MAC) algorithm (e.g. one in which the
		// >     algorithm registry description mentions MAC/HMAC).
		return fmt.Errorf("invalid header: unexpected value for 'algo': %w", ErrMalformed)
	}

	if c.Kid != "" {
		// Load KID from storage first.
		kid := getKeyIdFromAccountUrl(c.Kid)
		c.Jwk, err = a.LoadJWK(ac, kid)
		if err != nil {
			return err
		}
		c.Kid = kid // Use the uuid itself, not the full account url that was originally provided to us.
		c.Existing = true
	}

	if err = c.Key.UnmarshalJSON(c.Jwk); err != nil {
		return err
	}

	if !c.Key.Valid() {
		return fmt.Errorf("received invalid jwk: %w", ErrMalformed)
	}

	if c.Kid == "" {
		c.Kid = genUuid()
		c.Existing = false
	}

	return nil
}

func getKeyIdFromAccountUrl(accountUrl string) string {
	pieces := strings.Split(accountUrl, "/")
	return pieces[len(pieces)-1]
}

func hasValues(h jose.Header) bool {
	return h.KeyID != "" || h.JSONWebKey != nil || h.Algorithm != "" || h.Nonce != "" || len(h.ExtraHeaders) > 0
}

func (c *jwsCtx) VerifyJWS(signature string) (map[string]interface{}, error) {
	// See RFC 8555 Section 6.2. Request Authentication:
	//
	// > The JWS Unencoded Payload Option [RFC7797] MUST NOT be used
	//
	// This is validated by go-jose.
	sig, err := jose.ParseSigned(signature)
	if err != nil {
		return nil, fmt.Errorf("error parsing signature: %s: %w", err, ErrMalformed)
	}

	if len(sig.Signatures) > 1 {
		// See RFC 8555 Section 6.2. Request Authentication:
		//
		// > The JWS MUST NOT have multiple signatures
		return nil, fmt.Errorf("request had multiple signatures: %w", ErrMalformed)
	}

	if hasValues(sig.Signatures[0].Unprotected) {
		// See RFC 8555 Section 6.2. Request Authentication:
		//
		// > The JWS Unprotected Header [RFC7515] MUST NOT be used
		return nil, fmt.Errorf("request had unprotected headers: %w", ErrMalformed)
	}

	payload, err := sig.Verify(c.Key)
	if err != nil {
		return nil, err
	}

	if len(payload) == 0 {
		// Distinguish POST-AS-GET from POST-with-an-empty-body.
		return nil, nil
	}

	var m map[string]interface{}
	if err := json.Unmarshal(payload, &m); err != nil {
		return nil, fmt.Errorf("failed to json unmarshal 'payload': %s: %w", err, ErrMalformed)
	}

	return m, nil
}

func verifyEabPayload(acmeState *acmeState, ac *acmeContext, outer *jwsCtx, expectedPath string, payload map[string]interface{}) (*eabType, error) {
	// Parse the key out.
	rawProtectedBase64, ok := payload["protected"]
	if !ok {
		return nil, fmt.Errorf("missing required field 'protected': %w", ErrMalformed)
	}
	jwkBase64 := rawProtectedBase64.(string)

	jwkBytes, err := base64.RawURLEncoding.DecodeString(jwkBase64)
	if err != nil {
		return nil, fmt.Errorf("failed to base64 parse eab 'protected': %s: %w", err, ErrMalformed)
	}

	eabJws, err := UnmarshalEabJwsJson(jwkBytes)
	if err != nil {
		return nil, fmt.Errorf("failed to json unmarshal eab 'protected': %w", err)
	}

	if len(eabJws.Url) == 0 {
		return nil, fmt.Errorf("missing required parameter 'url' in eab 'protected': %w", ErrMalformed)
	}
	expectedUrl := ac.clusterUrl.JoinPath(expectedPath).String()
	if expectedUrl != eabJws.Url {
		return nil, fmt.Errorf("invalid value for 'url' in eab 'protected': got '%v' expected '%v': %w", eabJws.Url, expectedUrl, ErrUnauthorized)
	}

	if len(eabJws.Nonce) != 0 {
		return nil, fmt.Errorf("nonce should not be provided in eab 'protected': %w", ErrMalformed)
	}

	rawPayloadBase64, ok := payload["payload"]
	if !ok {
		return nil, fmt.Errorf("missing required field eab 'payload': %w", ErrMalformed)
	}
	payloadBase64, ok := rawPayloadBase64.(string)
	if !ok {
		return nil, fmt.Errorf("failed to parse 'payload' field: %w", ErrMalformed)
	}

	rawSignatureBase64, ok := payload["signature"]
	if !ok {
		return nil, fmt.Errorf("missing required field 'signature': %w", ErrMalformed)
	}
	signatureBase64, ok := rawSignatureBase64.(string)
	if !ok {
		return nil, fmt.Errorf("failed to parse 'signature' field: %w", ErrMalformed)
	}

	// go-jose only seems to support compact signature encodings.
	compactSig := fmt.Sprintf("%v.%v.%v", jwkBase64, payloadBase64, signatureBase64)
	sig, err := jose.ParseSigned(compactSig)
	if err != nil {
		return nil, fmt.Errorf("error parsing eab signature: %s: %w", err, ErrMalformed)
	}

	if len(sig.Signatures) > 1 {
		// See RFC 8555 Section 6.2. Request Authentication:
		//
		// > The JWS MUST NOT have multiple signatures
		return nil, fmt.Errorf("eab had multiple signatures: %w", ErrMalformed)
	}

	if hasValues(sig.Signatures[0].Unprotected) {
		// See RFC 8555 Section 6.2. Request Authentication:
		//
		// > The JWS Unprotected Header [RFC7515] MUST NOT be used
		return nil, fmt.Errorf("eab had unprotected headers: %w", ErrMalformed)
	}

	// Load the EAB to validate the signature against
	eabEntry, err := acmeState.LoadEab(ac.sc, eabJws.Kid)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to verify eab", ErrUnauthorized)
	}

	verifiedPayload, err := sig.Verify(eabEntry.PrivateBytes)
	if err != nil {
		return nil, err
	}

	// Make sure how eab payload matches the outer JWK key value
	if !bytes.Equal(outer.Jwk, verifiedPayload) {
		return nil, fmt.Errorf("eab payload does not match outer JWK key: %w", ErrMalformed)
	}

	if eabEntry.AcmeDirectory != ac.acmeDirectory {
		// This EAB was not created for this specific ACME directory, reject it
		return nil, fmt.Errorf("%w: failed to verify eab", ErrUnauthorized)
	}

	return eabEntry, nil
}
