package licensing

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"net/http"
	"net/url"
	"strings"

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

const (
	trialProductID  = "docker-ee-trial"
	trialRatePlanID = "free-trial"
)

// Client represents the licensing package interface, including methods for authentication and interaction with Docker
// licensing, accounts, and billing services
type Client interface {
	LoginViaAuth(ctx context.Context, username, password string) (authToken string, err error)
	GetHubUserOrgs(ctx context.Context, authToken string) (orgs []model.Org, err error)
	GetHubUserByName(ctx context.Context, username string) (user *model.User, err error)
	VerifyLicense(ctx context.Context, license model.IssuedLicense) (res *model.CheckResponse, err error)
	GenerateNewTrialSubscription(ctx context.Context, authToken, dockerID string) (subscriptionID string, err error)
	ListSubscriptions(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error)
	ListSubscriptionsDetails(ctx context.Context, authToken, dockerID string) (response []*model.SubscriptionDetail, err error)
	DownloadLicenseFromHub(ctx context.Context, authToken, subscriptionID string) (license *model.IssuedLicense, err error)
	ParseLicense(license []byte) (parsedLicense *model.IssuedLicense, err error)
	StoreLicense(ctx context.Context, dclnt WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error
	LoadLocalLicense(ctx context.Context, dclnt WrappedDockerClient) (*model.Subscription, error)
	SummarizeLicense(res *model.CheckResponse, keyID string) *model.Subscription
}

func (c *client) LoginViaAuth(ctx context.Context, username, password string) (string, error) {
	creds, err := c.login(ctx, username, password)
	if err != nil {
		return "", errors.Wrap(err, errors.Fields{
			"username": username,
		})
	}

	return creds.Token, nil
}

func (c *client) GetHubUserOrgs(ctx context.Context, authToken string) ([]model.Org, error) {
	ctx = jwt.NewContext(ctx, authToken)

	orgs, err := c.getUserOrgs(ctx, model.PaginationParams{})
	if err != nil {
		return nil, errors.WithMessage(err, "Failed to get orgs for user")
	}

	return orgs, nil
}

func (c *client) GetHubUserByName(ctx context.Context, username string) (*model.User, error) {
	user, err := c.getUserByName(ctx, username)
	if err != nil {
		return nil, errors.Wrap(err, errors.Fields{
			"username": username,
		})
	}

	return user, nil
}

func (c *client) VerifyLicense(ctx context.Context, license model.IssuedLicense) (*model.CheckResponse, error) {
	res, err := c.check(ctx, license)
	if err != nil {
		return nil, errors.WithMessage(err, "Failed to verify license")
	}

	return res, nil
}

func (c *client) GenerateNewTrialSubscription(ctx context.Context, authToken, dockerID string) (string, error) {
	ctx = jwt.NewContext(ctx, authToken)

	sub, err := c.createSubscription(ctx, &model.SubscriptionCreationRequest{
		Name:            "Docker Enterprise Free Trial",
		DockerID:        dockerID,
		ProductID:       trialProductID,
		ProductRatePlan: trialRatePlanID,
		Eusa: &model.EusaState{
			Accepted: true,
		},
	})
	if err != nil {
		return "", errors.Wrap(err, errors.Fields{
			"docker_id": dockerID,
		})
	}

	return sub.ID, nil
}

// ListSubscriptions returns basic descriptions of all subscriptions to docker enterprise products for the given dockerID
func (c *client) ListSubscriptions(ctx context.Context, authToken, dockerID string) ([]*model.Subscription, error) {
	ctx = jwt.NewContext(ctx, authToken)

	subs, err := c.listSubscriptions(ctx, map[string]string{"docker_id": dockerID})
	if err != nil {
		return nil, errors.Wrap(err, errors.Fields{
			"docker_id": dockerID,
		})
	}

	// filter out non docker licenses
	dockerSubs := []*model.Subscription{}
	for _, sub := range subs {
		if !strings.HasPrefix(sub.ProductID, "docker-ee") {
			continue
		}

		dockerSubs = append(dockerSubs, sub)
	}

	return dockerSubs, nil
}

// ListDetailedSubscriptions returns detailed subscriptions to docker enterprise products for the given dockerID
func (c *client) ListSubscriptionsDetails(ctx context.Context, authToken, dockerID string) ([]*model.SubscriptionDetail, error) {
	ctx = jwt.NewContext(ctx, authToken)

	subs, err := c.listSubscriptionsDetails(ctx, map[string]string{"docker_id": dockerID})
	if err != nil {
		return nil, errors.Wrap(err, errors.Fields{
			"docker_id": dockerID,
		})
	}

	// filter out non docker licenses
	dockerSubs := []*model.SubscriptionDetail{}
	for _, sub := range subs {
		if !strings.HasPrefix(sub.ProductID, "docker-ee") {
			continue
		}

		dockerSubs = append(dockerSubs, sub)
	}

	return dockerSubs, nil
}

func (c *client) DownloadLicenseFromHub(ctx context.Context, authToken, subscriptionID string) (*model.IssuedLicense, error) {
	ctx = jwt.NewContext(ctx, authToken)

	license, err := c.getLicenseFile(ctx, subscriptionID)
	if err != nil {
		return nil, errors.Wrap(err, errors.Fields{
			"subscriptionID": subscriptionID,
		})
	}

	return license, nil
}

func (c *client) ParseLicense(license []byte) (*model.IssuedLicense, error) {
	parsedLicense := &model.IssuedLicense{}
	// The file may contain a leading BOM, which will choke the
	// json deserializer.
	license = bytes.Trim(license, "\xef\xbb\xbf")

	if err := json.Unmarshal(license, &parsedLicense); err != nil {
		return nil, errors.WithMessage(err, "failed to parse license")
	}

	return parsedLicense, nil
}

type client struct {
	publicKeys []libtrust.PublicKey
	hclient    *http.Client
	baseURI    url.URL
}

// Config holds licensing client configuration
type Config struct {
	BaseURI    url.URL
	HTTPClient *http.Client
	// used by licensing client to validate an issued license
	PublicKeys []string
}

func errorSummary(body []byte) string {
	var be struct {
		Message string `json:"message"`
	}

	jsonErr := json.Unmarshal(body, &be)
	if jsonErr != nil {
		return clientlib.DefaultErrorSummary(body)
	}

	return be.Message
}

// New creates a new licensing Client
func New(config *Config) (Client, error) {
	publicKeys, err := unmarshalPublicKeys(config.PublicKeys)
	if err != nil {
		return nil, err
	}

	hclient := config.HTTPClient
	if hclient == nil {
		hclient = &http.Client{}
	}

	return &client{
		baseURI:    config.BaseURI,
		hclient:    hclient,
		publicKeys: publicKeys,
	}, nil
}

func unmarshalPublicKeys(publicKeys []string) ([]libtrust.PublicKey, error) {
	trustKeys := make([]libtrust.PublicKey, len(publicKeys))

	for i, publicKey := range publicKeys {
		trustKey, err := unmarshalPublicKey(publicKey)
		if err != nil {
			return nil, err
		}

		trustKeys[i] = trustKey
	}
	return trustKeys, nil
}

func unmarshalPublicKey(publicKey string) (libtrust.PublicKey, error) {
	pemBytes, err := base64.StdEncoding.DecodeString(publicKey)
	if err != nil {
		return nil, errors.Wrapf(err, errors.Fields{
			"public_key": publicKey,
		}, "decode public key failed")
	}

	key, err := libtrust.UnmarshalPublicKeyPEM(pemBytes)
	if err != nil {
		return nil, errors.Wrapf(err, errors.Fields{
			"public_key": publicKey,
		}, "unmarshal public key failed")
	}
	return key, nil
}

func (c *client) doReq(ctx context.Context, method string, url *url.URL, opts ...clientlib.RequestOption) (*http.Request, *http.Response, error) {
	return clientlib.Do(ctx, method, url.String(), append(c.requestDefaults(), opts...)...)
}

func (c *client) doRequestNoAuth(ctx context.Context, method string, url *url.URL, opts ...clientlib.RequestOption) (*http.Request, *http.Response, error) {
	return clientlib.Do(ctx, method, url.String(), append(c.requestDefaults(), opts...)...)
}

func (c *client) requestDefaults() []clientlib.RequestOption {
	return []clientlib.RequestOption{
		func(req *clientlib.Request) {
			tok, _ := jwt.FromContext(req.Context())
			req.Header.Add("Authorization", "Bearer "+tok)
			req.ErrorSummary = errorSummary
			req.Client = c.hclient
		},
	}
}

func (c *client) StoreLicense(ctx context.Context, dclnt WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error {
	return StoreLicense(ctx, dclnt, licenses, localRootDir)
}
