package licensing

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"time"

	"github.com/docker/libtrust"
	"github.com/docker/licensing/lib/errors"
	"github.com/docker/licensing/lib/go-clientlib"
	"github.com/docker/licensing/model"
)

func (c *client) getLicenseFile(ctx context.Context, subID string) (*model.IssuedLicense, error) {
	url := c.baseURI
	url.Path += fmt.Sprintf("/api/billing/v4/subscriptions/%s/license-file", subID)

	license := new(model.IssuedLicense)
	if _, _, err := c.doReq(ctx, "GET", &url, clientlib.RecvJSON(license)); err != nil {
		return nil, err
	}

	return license, nil
}

// Check verifies that the license identified by the given key id is valid. Note that it does not
// interrogate the contents of the license.
func (c *client) check(ctx context.Context, license model.IssuedLicense) (*model.CheckResponse, error) {
	keyID := license.KeyID
	privateKey := license.PrivateKey

	authorization, err := c.getAuthorization(ctx, license)
	if err != nil {
		return nil, err
	}

	// TODO: Mason - replace this parseJWS with a non libtrust lib
	signature, err := libtrust.ParseJWS(authorization)
	if err != nil {
		return nil, errors.Wrapf(err, errors.Fields{
			"key_id": keyID,
		}, "license parse JWS failed")
	}

	keys, err := signature.Verify()
	if err != nil {
		return nil, errors.Wrapf(err, errors.Fields{
			"key_id": keyID,
		}, "license signature verification failed")
	}

	keyCnt := len(keys)
	if keyCnt != 1 {
		err = fmt.Errorf("unexpected number of signing keys (%d)", keyCnt)
		return nil, errors.WithStack(err).With(errors.Fields{
			"key_id": keyID,
		})
	}

	key := keys[0]

	if !c.recognizedSigningKey(key) {
		return nil, errors.New("unrecognized signing key")
	}

	payload, err := signature.Payload()
	if err != nil {
		return nil, errors.Wrapf(err, errors.Fields{
			"key_id": keyID,
		}, "malformed signature payload")
	}

	checkRes := new(model.CheckResponse)

	err = json.Unmarshal(payload, &checkRes)
	if err != nil {
		return nil, errors.Wrapf(err, errors.Fields{
			"key_id": keyID,
		}, "license payload unmarshal failed")
	}

	msg := checkRes.Expiration.Format(time.RFC3339)
	if err := checkToken(msg, checkRes.Token, privateKey); err != nil {
		return nil, errors.Wrap(err, errors.Fields{
			"key_id": keyID,
		})
	}

	return checkRes, nil
}

// recognizedSigningKey returns true if the given key is signed with a recognized signing key, false otherwise
func (c *client) recognizedSigningKey(key libtrust.PublicKey) bool {
	for _, publicKey := range c.publicKeys {
		if key.KeyID() == publicKey.KeyID() {
			return true
		}
	}
	return false
}

// getAuthorization returns the decoded license authorization
func (c *client) getAuthorization(ctx context.Context, license model.IssuedLicense) ([]byte, error) {
	decoded, err := base64.StdEncoding.DecodeString(license.Authorization)
	if err != nil {
		return nil, errors.Wrapf(err, errors.Fields{
			"key_id": license.KeyID,
		}, "decoding license authorization failed")
	}
	return decoded, nil
}

// All of the functions in this file assume that they are receiving a properly
// formatted private key.

// checkToken performs a MAC algorithm (where token is generated by hashing the
// message with the privateKey via GenerateToken) with the purpose of authenticating
// the validity of both the message and the private key of the person who generated
// the token.
func checkToken(message, token, privateKey string) error {
	tokenBytes, err := base64.URLEncoding.DecodeString(token)
	if err != nil {
		return errors.Wrap(err, errors.Fields{"token": token})
	}

	generatedToken, err := generateToken(message, privateKey)
	if err != nil {
		return errors.Wrap(err, errors.Fields{"token": token})
	}

	generatedBytes, err := base64.URLEncoding.DecodeString(generatedToken)
	if err != nil {
		return errors.Wrap(err, errors.Fields{"token": token})
	}

	if !hmac.Equal(tokenBytes, generatedBytes) {
		return errors.Forbidden(errors.Fields{"token": token}, "invalid token")
	}

	return nil
}

// generateToken generates a hash of the message with the privateKey via the
// sha256 algorithm.
func generateToken(message, privateKey string) (string, error) {
	key, err := base64.URLEncoding.DecodeString(privateKey)
	if err != nil {
		return "", errors.Wrap(err, errors.Fields{"msg": message})
	}

	h := hmac.New(sha256.New, key)
	h.Write([]byte(message))
	return base64.URLEncoding.EncodeToString(h.Sum(nil)), nil
}
