// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package core

import (
	"encoding/json"
	"io/ioutil"
	"net/http"
	"reflect"

	"github.com/hashicorp/go-version"
	"github.com/hashicorp/vagrant-plugin-sdk/core"
	"github.com/mitchellh/mapstructure"
)

type BoxVersionProvider struct {
	Name         string
	Url          string
	Checksum     string
	ChecksumType string
}

func (b *BoxVersionProvider) MatchesAny(p ...*BoxVersionProvider) (matches bool) {
	for _, provider := range p {
		if b.Matches(provider) {
			return true
		}
	}
	return false
}

func (b *BoxVersionProvider) Matches(p *BoxVersionProvider) (matches bool) {
	pVal := reflect.ValueOf(*p)
	bVal := reflect.ValueOf(*b)
	typeOfMatcher := pVal.Type()
	matches = true

	fields := pVal.NumField()
	for i := 0; i < fields; i++ {
		if pVal.Field(i).Interface() != "" {
			bField := bVal.FieldByName(typeOfMatcher.Field(i).Name).Interface()
			pField := pVal.Field(i).Interface()
			if pField != bField {
				return false
			}
		}
	}
	return
}

type BoxVersion struct {
	Version     string
	Status      string
	Description string
	Providers   []*BoxVersionProvider
}

func (b *BoxVersion) Provider(name string) (p *BoxVersionProvider, err error) {
	for _, provider := range b.Providers {
		if provider.Name == name {
			return provider, nil
		}
	}
	return
}

func (b *BoxVersion) ListProviders() ([]string, error) {
	p := []string{}
	for _, provider := range b.Providers {
		p = append(p, provider.Name)
	}
	return p, nil
}

type BoxMetadata struct {
	Name        string
	Description string
	Versions    []*BoxVersion
}

func LoadBoxMetadata(data []byte) (*BoxMetadata, error) {
	var metadata map[string]interface{}
	if err := json.Unmarshal(data, &metadata); err != nil {
		return nil, err
	}
	var result BoxMetadata
	err := mapstructure.Decode(metadata, &result)
	return &result, err
}

func (b *BoxMetadata) matches(version string, name string, p *core.BoxProvider) (matches bool, err error) {
	ver, err := b.version(version, &core.BoxProvider{Name: name})
	if err != nil {
		return false, err
	}
	provider, err := ver.Provider(name)
	if err != nil {
		return false, err
	}
	var boxVersionProvider *BoxVersionProvider
	mapstructure.Decode(p, &boxVersionProvider)
	return provider.Matches(boxVersionProvider), nil
}

func (b *BoxMetadata) matchesAny(version string, name string, p ...*core.BoxProvider) (matches bool, err error) {
	for _, provider := range p {
		m, err := b.matches(version, name, provider)
		if err != nil {
			return false, err
		}
		if m {
			return true, nil
		}
	}
	return false, nil
}

func (b *BoxMetadata) version(ver string, providerOpts *core.BoxProvider) (v *BoxVersion, err error) {
	matchesProvider := false
	inputVersion, err := version.NewConstraint(ver)
	if err != nil {
		return nil, err
	}
	for _, boxVer := range b.Versions {
		boxVersion, err := version.NewVersion(boxVer.Version)
		if err != nil {
			return nil, err
		}
		if inputVersion.Check(boxVersion) {
			// Check for the provider in the version
			if providerOpts == nil {
				matchesProvider = true
			} else {
				for _, p := range boxVer.Providers {
					boxVersionProvider := &BoxVersionProvider{
						Name: providerOpts.Name, Url: providerOpts.Url, Checksum: providerOpts.Checksum,
						ChecksumType: providerOpts.ChecksumType,
					}
					if p.Matches(boxVersionProvider) {
						matchesProvider = true
					}
				}
			}
			if matchesProvider {
				return boxVer, nil
			}
		}
	}
	return
}

func (b *BoxMetadata) LoadMetadata(url string) (err error) {
	client := &http.Client{}
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("Accept", "application/json")
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	raw, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	var metadata map[string]interface{}
	if err := json.Unmarshal(raw, &metadata); err != nil {
		return err
	}
	err = mapstructure.Decode(metadata, &b)
	return
}

func (b *BoxMetadata) BoxName() string {
	return b.Name
}

func (b *BoxMetadata) Version(ver string, providerOpts ...*core.BoxProvider) (v *core.BoxVersion, err error) {
	if len(providerOpts) == 0 {
		providerOpts = []*core.BoxProvider{
			nil,
		}
	}
	for _, p := range providerOpts {
		boxVer, err := b.version(ver, p)
		if err != nil {
			return nil, err
		}
		if boxVer != nil {
			var coreBoxVersion *core.BoxVersion
			mapstructure.Decode(boxVer, &coreBoxVersion)
			return coreBoxVersion, nil
		}
	}
	return
}

func (b *BoxMetadata) ListVersions(providerOpts ...*core.BoxProvider) ([]string, error) {
	v := []string{}
	for _, version := range b.Versions {
		if providerOpts != nil {
			var boxVersionProvider []*BoxVersionProvider
			mapstructure.Decode(providerOpts, &boxVersionProvider)
			for _, p := range version.Providers {
				if p.MatchesAny(boxVersionProvider...) {
					v = append(v, version.Version)
				}
			}
		} else {
			v = append(v, version.Version)
		}
	}
	return v, nil
}

func (b *BoxMetadata) Provider(version string, name string) (p *core.BoxProvider, err error) {
	ver, err := b.version(version, &core.BoxProvider{Name: name})
	if err != nil {
		return nil, err
	}
	if ver == nil {
		return nil, err
	}
	provider, err := ver.Provider(name)
	if err != nil {
		return nil, err
	}
	if provider != nil {
		var coreProvider *core.BoxProvider
		mapstructure.Decode(provider, &coreProvider)
		var coreVersion *core.BoxVersion
		mapstructure.Decode(ver, &coreVersion)
		coreProvider.Version = coreVersion
		return coreProvider, nil
	}
	return
}

func (b *BoxMetadata) ListProviders(version string) (providers []string, err error) {
	ver, err := b.version(version, &core.BoxProvider{})
	if err != nil {
		return nil, err
	}
	return ver.ListProviders()
}

var _ core.BoxMetadata = (*BoxMetadata)(nil)
