package version

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"

	"github.com/linkerd/linkerd2/pkg/util"
)

// Channels provides an interface to interact with a set of release channels.
// This module is also responsible for online retrieval of the latest release
// versions.
type Channels struct {
	array []channelVersion
}

var (
	// CheckURL provides an online endpoint for Linkerd's version checks
	CheckURL = "https://versioncheck.linkerd.io/version.json"
)

// NewChannels is used primarily for testing, it returns a Channels struct that
// mimic a GetLatestVersions response.
func NewChannels(channel string) (Channels, error) {
	cv, err := parseChannelVersion(channel)
	if err != nil {
		return Channels{}, err
	}

	return Channels{
		array: []channelVersion{cv},
	}, nil
}

// Match validates whether the given version string:
// 1) is a well-formed channel-version string, for example: "edge-19.1.2"
// 2) references a known channel
// 3) matches the version in the known channel
func (c Channels) Match(actualVersion string) error {
	if actualVersion == "" {
		return errors.New("actual version is empty")
	}

	if c.Empty() {
		return errors.New("unable to determine version channel")
	}

	actual, err := parseChannelVersion(actualVersion)
	if err != nil {
		return fmt.Errorf("failed to parse actual version: %w", err)
	}

	for _, cv := range c.array {
		if cv.updateChannel() == actual.updateChannel() {
			return match(cv.String(), actualVersion)
		}
	}

	return fmt.Errorf("unsupported version channel: %s", actualVersion)
}

// Determines whether there are any release channels stored in the struct.
func (c Channels) Empty() bool {
	return len(c.array) == 0
}

// GetLatestVersions performs an online request to check for the latest Linkerd
// release channels.
func GetLatestVersions(ctx context.Context, uuid string, source string) (Channels, error) {
	url := fmt.Sprintf("%s?version=%s&uuid=%s&source=%s", CheckURL, Version, uuid, source)
	return getLatestVersions(ctx, http.DefaultClient, url)
}

func getLatestVersions(ctx context.Context, client *http.Client, url string) (Channels, error) {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return Channels{}, err
	}

	rsp, err := client.Do(req.WithContext(ctx))
	if err != nil {
		var dnsError *net.DNSError
		if errors.As(err, &dnsError) {
			return Channels{}, fmt.Errorf("failed to resolve version check server: %s", url)
		}
		return Channels{}, err
	}
	defer rsp.Body.Close()

	if rsp.StatusCode != 200 {
		return Channels{}, fmt.Errorf("unexpected versioncheck response: %s", rsp.Status)
	}

	bytes, err := util.ReadAllLimit(rsp.Body, util.MB)
	if err != nil {
		return Channels{}, err
	}

	var versionRsp map[string]string
	err = json.Unmarshal(bytes, &versionRsp)
	if err != nil {
		return Channels{}, err
	}

	channels := Channels{}
	for c, v := range versionRsp {
		cv, err := parseChannelVersion(v)
		if err != nil {
			return Channels{}, fmt.Errorf("unexpected versioncheck response: %w", err)
		}

		if c != cv.updateChannel() {
			return Channels{}, fmt.Errorf("unexpected versioncheck response: channel in %s does not match %s", cv, c)
		}

		channels.array = append(channels.array, cv)
	}

	return channels, nil
}
