// Package jws implements the digital Signature on JSON based data
// structures as described in https://tools.ietf.org/html/rfc7515
//
// If you do not care about the details, the only things that you
// would need to use are the following functions:
//
//	jws.SignWithOption(Payload, algorithm, key)
//	jws.Verify(encodedjws, algorithm, key)
//
// To sign, simply use `jws.SignWithOption`. `Payload` is a []byte buffer that
// contains whatever data you want to sign. `alg` is one of the
// jwa.SignatureAlgorithm constants from package jwa. For RSA and
// ECDSA family of algorithms, you will need to prepare a private key.
// For HMAC family, you just need a []byte value. The `jws.SignWithOption`
// function will return the encoded JWS message on success.
//
// To verify, use `jws.Verify`. It will parse the `encodedjws` buffer
// and verify the result using `algorithm` and `key`. Upon successful
// verification, the original Payload is returned, so you can work on it.
package jws

import (
	"bytes"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/open-policy-agent/opa/internal/jwx/jwa"
	"github.com/open-policy-agent/opa/internal/jwx/jwk"
	"github.com/open-policy-agent/opa/internal/jwx/jws/sign"
	"github.com/open-policy-agent/opa/internal/jwx/jws/verify"
)

// SignLiteral generates a Signature for the given Payload and Headers, and serializes
// it in compact serialization format. In this format you may NOT use
// multiple signers.
func SignLiteral(payload []byte, alg jwa.SignatureAlgorithm, key interface{}, hdrBuf []byte, rnd io.Reader) ([]byte, error) {
	encodedHdr := base64.RawURLEncoding.EncodeToString(hdrBuf)
	encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
	signingInput := strings.Join(
		[]string{
			encodedHdr,
			encodedPayload,
		}, ".",
	)
	signer, err := sign.New(alg)
	if err != nil {
		return nil, fmt.Errorf("failed to create signer: %w", err)
	}

	var signature []byte
	switch s := signer.(type) {
	case *sign.ECDSASigner:
		signature, err = s.SignWithRand([]byte(signingInput), key, rnd)
	default:
		signature, err = signer.Sign([]byte(signingInput), key)
	}
	if err != nil {
		return nil, fmt.Errorf("failed to sign Payload: %w", err)
	}
	encodedSignature := base64.RawURLEncoding.EncodeToString(signature)
	compactSerialization := strings.Join(
		[]string{
			signingInput,
			encodedSignature,
		}, ".",
	)
	return []byte(compactSerialization), nil
}

// SignWithOption generates a Signature for the given Payload, and serializes
// it in compact serialization format. In this format you may NOT use
// multiple signers.
//
// If you would like to pass custom Headers, use the WithHeaders option.
func SignWithOption(payload []byte, alg jwa.SignatureAlgorithm, key interface{}) ([]byte, error) {
	var headers Headers = &StandardHeaders{}

	err := headers.Set(AlgorithmKey, alg)
	if err != nil {
		return nil, fmt.Errorf("failed to set alg value: %w", err)
	}

	hdrBuf, err := json.Marshal(headers)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal Headers: %w", err)
	}
	// NOTE(sr): we don't use SignWithOption -- if we did, this rand.Reader
	// should come from the BuiltinContext's Seed, too.
	return SignLiteral(payload, alg, key, hdrBuf, rand.Reader)
}

// Verify checks if the given JWS message is verifiable using `alg` and `key`.
// If the verification is successful, `err` is nil, and the content of the
// Payload that was signed is returned. If you need more fine-grained
// control of the verification process, manually call `Parse`, generate a
// verifier, and call `Verify` on the parsed JWS message object.
func Verify(buf []byte, alg jwa.SignatureAlgorithm, key interface{}) (ret []byte, err error) {

	verifier, err := verify.New(alg)
	if err != nil {
		return nil, fmt.Errorf("failed to create verifier: %w", err)
	}

	buf = bytes.TrimSpace(buf)
	if len(buf) == 0 {
		return nil, errors.New(`attempt to verify empty buffer`)
	}

	parts, err := SplitCompact(string(buf[:]))
	if err != nil {
		return nil, fmt.Errorf("failed extract from compact serialization format: %w", err)
	}

	signingInput := strings.Join(
		[]string{
			parts[0],
			parts[1],
		}, ".",
	)

	decodedSignature, err := base64.RawURLEncoding.DecodeString(parts[2])
	if err != nil {
		return nil, fmt.Errorf("failed to decode signature: %w", err)
	}
	if err := verifier.Verify([]byte(signingInput), decodedSignature, key); err != nil {
		return nil, fmt.Errorf("failed to verify message: %w", err)
	}

	if decodedPayload, err := base64.RawURLEncoding.DecodeString(parts[1]); err == nil {
		return decodedPayload, nil
	}
	return nil, fmt.Errorf("failed to decode Payload: %w", err)
}

// VerifyWithJWK verifies the JWS message using the specified JWK
func VerifyWithJWK(buf []byte, key jwk.Key) (payload []byte, err error) {

	keyVal, err := key.Materialize()
	if err != nil {
		return nil, fmt.Errorf("failed to materialize key: %w", err)
	}
	return Verify(buf, key.GetAlgorithm(), keyVal)
}

// VerifyWithJWKSet verifies the JWS message using JWK key set.
// By default it will only pick up keys that have the "use" key
// set to either "sig" or "enc", but you can override it by
// providing a keyaccept function.
func VerifyWithJWKSet(buf []byte, keyset *jwk.Set) (payload []byte, err error) {

	for _, key := range keyset.Keys {
		payload, err := VerifyWithJWK(buf, key)
		if err == nil {
			return payload, nil
		}
	}
	return nil, errors.New("failed to verify with any of the keys")
}

// ParseByte parses a JWS value serialized via compact serialization and provided as []byte.
func ParseByte(jwsCompact []byte) (m *Message, err error) {
	return parseCompact(string(jwsCompact[:]))
}

// ParseString parses a JWS value serialized via compact serialization and provided as string.
func ParseString(s string) (*Message, error) {
	return parseCompact(s)
}

// SplitCompact splits a JWT and returns its three parts
// separately: Protected Headers, Payload and Signature.
func SplitCompact(jwsCompact string) ([]string, error) {

	parts := strings.Split(jwsCompact, ".")
	if len(parts) < 3 {
		return nil, errors.New("failed to split compact serialization")
	}
	return parts, nil
}

// parseCompact parses a JWS value serialized via compact serialization.
func parseCompact(str string) (m *Message, err error) {

	var decodedHeader, decodedPayload, decodedSignature []byte
	parts, err := SplitCompact(str)
	if err != nil {
		return nil, fmt.Errorf("invalid compact serialization format: %w", err)
	}

	if decodedHeader, err = base64.RawURLEncoding.DecodeString(parts[0]); err != nil {
		return nil, fmt.Errorf("failed to decode Headers: %w", err)
	}
	var hdr StandardHeaders
	if err := json.Unmarshal(decodedHeader, &hdr); err != nil {
		return nil, fmt.Errorf("failed to parse JOSE Headers: %w", err)
	}

	if decodedPayload, err = base64.RawURLEncoding.DecodeString(parts[1]); err != nil {
		return nil, fmt.Errorf("failed to decode Payload: %w", err)
	}

	if len(parts) > 2 {
		if decodedSignature, err = base64.RawURLEncoding.DecodeString(parts[2]); err != nil {
			return nil, fmt.Errorf("failed to decode Signature: %w", err)
		}
	}

	var msg Message
	msg.Payload = decodedPayload
	msg.Signatures = append(msg.Signatures, &Signature{
		Protected: &hdr,
		Signature: decodedSignature,
	})
	return &msg, nil
}
