package versions

import (
	"context"
	"encoding/json"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"sort"

	registryclient "github.com/docker/cli/cli/registry/client"
	clitypes "github.com/docker/cli/types"
	"github.com/docker/distribution/reference"
	ver "github.com/hashicorp/go-version"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

const (
	// defaultRuntimeMetadataDir is the location where the metadata file is stored
	defaultRuntimeMetadataDir = "/var/lib/docker-engine"
)

// GetEngineVersions reports the versions of the engine that are available
func GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, registryPrefix, imageName, versionString string) (clitypes.AvailableVersions, error) {
	if imageName == "" {
		var err error
		localMetadata, err := GetCurrentRuntimeMetadata("")
		if err != nil {
			return clitypes.AvailableVersions{}, err
		}
		imageName = localMetadata.EngineImage
	}
	imageRef, err := reference.ParseNormalizedNamed(path.Join(registryPrefix, imageName))
	if err != nil {
		return clitypes.AvailableVersions{}, err
	}

	tags, err := registryClient.GetTags(ctx, imageRef)
	if err != nil {
		return clitypes.AvailableVersions{}, err
	}

	return parseTags(tags, versionString)
}

func parseTags(tags []string, currentVersion string) (clitypes.AvailableVersions, error) {
	var ret clitypes.AvailableVersions
	currentVer, err := ver.NewVersion(currentVersion)
	if err != nil {
		return ret, errors.Wrapf(err, "failed to parse existing version %s", currentVersion)
	}
	downgrades := []clitypes.DockerVersion{}
	patches := []clitypes.DockerVersion{}
	upgrades := []clitypes.DockerVersion{}
	currentSegments := currentVer.Segments()
	for _, tag := range tags {
		tmp, err := ver.NewVersion(tag)
		if err != nil {
			logrus.Debugf("Unable to parse %s: %s", tag, err)
			continue
		}
		testVersion := clitypes.DockerVersion{Version: *tmp, Tag: tag}
		if testVersion.LessThan(currentVer) {
			downgrades = append(downgrades, testVersion)
			continue
		}
		testSegments := testVersion.Segments()
		// lib always provides min 3 segments
		if testSegments[0] == currentSegments[0] &&
			testSegments[1] == currentSegments[1] {
			patches = append(patches, testVersion)
		} else {
			upgrades = append(upgrades, testVersion)
		}
	}
	sort.Slice(downgrades, func(i, j int) bool {
		return downgrades[i].Version.LessThan(&downgrades[j].Version)
	})
	sort.Slice(patches, func(i, j int) bool {
		return patches[i].Version.LessThan(&patches[j].Version)
	})
	sort.Slice(upgrades, func(i, j int) bool {
		return upgrades[i].Version.LessThan(&upgrades[j].Version)
	})
	ret.Downgrades = downgrades
	ret.Patches = patches
	ret.Upgrades = upgrades
	return ret, nil
}

// GetCurrentRuntimeMetadata loads the current daemon runtime metadata information from the local host
func GetCurrentRuntimeMetadata(metadataDir string) (*clitypes.RuntimeMetadata, error) {
	if metadataDir == "" {
		metadataDir = defaultRuntimeMetadataDir
	}
	filename := filepath.Join(metadataDir, clitypes.RuntimeMetadataName+".json")

	data, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	var res clitypes.RuntimeMetadata
	err = json.Unmarshal(data, &res)
	if err != nil {
		return nil, errors.Wrapf(err, "malformed runtime metadata file %s", filename)
	}
	return &res, nil
}

// WriteRuntimeMetadata stores the metadata on the local system
func WriteRuntimeMetadata(metadataDir string, metadata *clitypes.RuntimeMetadata) error {
	if metadataDir == "" {
		metadataDir = defaultRuntimeMetadataDir
	}
	filename := filepath.Join(metadataDir, clitypes.RuntimeMetadataName+".json")

	data, err := json.Marshal(metadata)
	if err != nil {
		return err
	}

	os.Remove(filename)
	return ioutil.WriteFile(filename, data, 0644)
}
