package licensing

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/docker/licensing/model"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/swarm"
)

var (
	licenseNamePrefix = "com.docker.license"
	licenseFilename   = "docker.lic"

	// ErrWorkerNode returned on a swarm worker node - lookup licenses on swarm managers
	ErrWorkerNode = fmt.Errorf("this node is not a swarm manager - check license status on a manager node")
	// ErrUnlicensed returned when no license found
	ErrUnlicensed = fmt.Errorf("no license found")
)

// WrappedDockerClient provides methods useful for installing licenses to the wrapped docker engine or cluster
type WrappedDockerClient interface {
	Info(ctx context.Context) (types.Info, error)
	NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error)
	ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error)
	ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error)
	ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error)
}

// StoreLicense will store the license on the host filesystem and swarm (if swarm is active)
func StoreLicense(ctx context.Context, clnt WrappedDockerClient, license *model.IssuedLicense, rootDir string) error {

	licenseData, err := json.Marshal(*license)
	if err != nil {
		return err
	}

	// First determine if we're in swarm-mode or a stand-alone engine
	_, err = clnt.NodeList(ctx, types.NodeListOptions{})
	if err != nil { // TODO - check for the specific error message
		return writeLicenseToHost(ctx, clnt, licenseData, rootDir)
	}
	// Load this in the latest license index
	latestVersion, err := getLatestNamedConfig(clnt, licenseNamePrefix)
	if err != nil {
		return fmt.Errorf("unable to get latest license version: %s", err)
	}
	spec := swarm.ConfigSpec{
		Annotations: swarm.Annotations{
			Name: fmt.Sprintf("%s-%d", licenseNamePrefix, latestVersion+1),
			Labels: map[string]string{
				"com.docker.ucp.access.label":     "/",
				"com.docker.ucp.collection":       "swarm",
				"com.docker.ucp.collection.root":  "true",
				"com.docker.ucp.collection.swarm": "true",
			},
		},
		Data: licenseData,
	}
	_, err = clnt.ConfigCreate(context.Background(), spec)
	if err != nil {

		return fmt.Errorf("Failed to create license: %s", err)
	}

	return nil
}

func (c *client) LoadLocalLicense(ctx context.Context, clnt WrappedDockerClient) (*model.Subscription, error) {
	info, err := clnt.Info(ctx)
	if err != nil {
		return nil, err
	}

	var licenseData []byte
	if info.Swarm.LocalNodeState != "active" {
		licenseData, err = readLicenseFromHost(ctx, info.DockerRootDir)
	} else {
		// Load the latest license index
		var latestVersion int
		latestVersion, err = getLatestNamedConfig(clnt, licenseNamePrefix)
		if err != nil {
			if strings.Contains(err.Error(), "not a swarm manager.") {
				return nil, ErrWorkerNode
			}
			return nil, fmt.Errorf("unable to get latest license version: %s", err)
		}
		if latestVersion >= 0 {
			cfg, _, err := clnt.ConfigInspectWithRaw(ctx, fmt.Sprintf("%s-%d", licenseNamePrefix, latestVersion))
			if err != nil {
				return nil, fmt.Errorf("unable to load license from swarm config: %s", err)
			}
			licenseData = cfg.Spec.Data
		} else {
			licenseData, err = readLicenseFromHost(ctx, info.DockerRootDir)
		}
	}
	if err != nil {
		if os.IsNotExist(err) {
			return nil, ErrUnlicensed
		}
		return nil, fmt.Errorf("Failed to create license: %s", err)
	}

	parsedLicense, err := c.ParseLicense(licenseData)
	if err != nil {
		return nil, err
	}
	checkResponse, err := c.VerifyLicense(ctx, *parsedLicense)
	if err != nil {
		return nil, err
	}
	return checkResponseToSubscription(checkResponse, parsedLicense.KeyID), nil
}

func checkResponseToSubscription(checkResponse *model.CheckResponse, keyID string) *model.Subscription {

	// TODO - this translation still needs some work
	// Primary missing piece is how to distinguish from basic, vs std/advanced
	var productID string
	var ratePlan string
	var state string
	switch strings.ToLower(checkResponse.Tier) {
	case "internal":
		productID = "docker-ee-trial"
		ratePlan = "free-trial"
	case "production":
		productID = "docker-ee"
		if checkResponse.ScanningEnabled {
			ratePlan = "nfr-advanced"
		} else {
			ratePlan = "nfr-standard"
		}
	}

	// Determine if the license has already expired
	if checkResponse.Expiration.Before(time.Now()) {
		state = "expired"
	} else {
		state = "active"
	}

	// Translate the legacy structure into the new Subscription fields
	return &model.Subscription{
		// Name
		ID: keyID, // This is not actually the same, but is unique
		// DockerID
		ProductID:       productID,
		ProductRatePlan: ratePlan,
		// ProductRatePlanID
		// Start
		Expires: &checkResponse.Expiration,
		State:   state,
		// Eusa
		PricingComponents: model.PricingComponents{
			{
				Name:  "Nodes",
				Value: checkResponse.MaxEngines,
			},
		},
	}
}

func (c *client) SummarizeLicense(checkResponse *model.CheckResponse, keyID string) *model.Subscription {
	return checkResponseToSubscription(checkResponse, keyID)
}

// getLatestNamedConfig looks for versioned instances of configs with the
// given name prefix which have a `-NUM` integer version suffix. Returns the
// config with the higest version number found or nil if no such configs exist
// along with its version number.
func getLatestNamedConfig(dclient WrappedDockerClient, namePrefix string) (int, error) {
	latestVersion := -1
	// List any/all existing configs so that we create a newer version than
	// any that already exist.
	filter := filters.NewArgs()
	filter.Add("name", namePrefix)
	existingConfigs, err := dclient.ConfigList(context.Background(), types.ConfigListOptions{Filters: filter})
	if err != nil {
		return latestVersion, fmt.Errorf("unable to list existing configs: %s", err)
	}

	for _, existingConfig := range existingConfigs {
		existingConfigName := existingConfig.Spec.Name
		nameSuffix := strings.TrimPrefix(existingConfigName, namePrefix)
		if nameSuffix == "" || nameSuffix[0] != '-' {
			continue // No version specifier?
		}

		versionSuffix := nameSuffix[1:] // Trim the version separator.
		existingVersion, err := strconv.Atoi(versionSuffix)
		if err != nil {
			continue // Unable to parse version as integer.
		}
		if existingVersion > latestVersion {
			latestVersion = existingVersion
		}
	}

	return latestVersion, nil
}

func writeLicenseToHost(ctx context.Context, dclient WrappedDockerClient, license []byte, rootDir string) error {
	// TODO we should write the file out over the clnt instead of to the local filesystem
	return ioutil.WriteFile(filepath.Join(rootDir, licenseFilename), license, 0644)
}

func readLicenseFromHost(ctx context.Context, rootDir string) ([]byte, error) {
	// TODO we should read the file in over the clnt instead of to the local filesystem
	return ioutil.ReadFile(filepath.Join(rootDir, licenseFilename))
}
