/*
   Copyright 2020 The Compose Specification Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package loader

import (
	"reflect"
	"sort"

	"github.com/compose-spec/compose-go/types"
	"github.com/imdario/mergo"
	"github.com/pkg/errors"
)

type specials struct {
	m map[reflect.Type]func(dst, src reflect.Value) error
}

var serviceSpecials = &specials{
	m: map[reflect.Type]func(dst, src reflect.Value) error{
		reflect.TypeOf(&types.LoggingConfig{}):           safelyMerge(mergeLoggingConfig),
		reflect.TypeOf(&types.UlimitsConfig{}):           safelyMerge(mergeUlimitsConfig),
		reflect.TypeOf([]types.ServiceVolumeConfig{}):    mergeSlice(toServiceVolumeConfigsMap, toServiceVolumeConfigsSlice),
		reflect.TypeOf([]types.ServicePortConfig{}):      mergeSlice(toServicePortConfigsMap, toServicePortConfigsSlice),
		reflect.TypeOf([]types.ServiceSecretConfig{}):    mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice),
		reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice),
		reflect.TypeOf(&types.UlimitsConfig{}):           mergeUlimitsConfig,
	},
}

func (s *specials) Transformer(t reflect.Type) func(dst, src reflect.Value) error {
	// TODO this is a workaround waiting for imdario/mergo#131
	if t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Bool {
		return func(dst, src reflect.Value) error {
			if dst.CanSet() && !src.IsNil() {
				dst.Set(src)
			}
			return nil
		}
	}
	if fn, ok := s.m[t]; ok {
		return fn
	}
	return nil
}

func merge(configs []*types.Config) (*types.Config, error) {
	base := configs[0]
	for _, override := range configs[1:] {
		var err error
		base.Name = mergeNames(base.Name, override.Name)
		base.Services, err = mergeServices(base.Services, override.Services)
		if err != nil {
			return base, errors.Wrapf(err, "cannot merge services from %s", override.Filename)
		}
		base.Volumes, err = mergeVolumes(base.Volumes, override.Volumes)
		if err != nil {
			return base, errors.Wrapf(err, "cannot merge volumes from %s", override.Filename)
		}
		base.Networks, err = mergeNetworks(base.Networks, override.Networks)
		if err != nil {
			return base, errors.Wrapf(err, "cannot merge networks from %s", override.Filename)
		}
		base.Secrets, err = mergeSecrets(base.Secrets, override.Secrets)
		if err != nil {
			return base, errors.Wrapf(err, "cannot merge secrets from %s", override.Filename)
		}
		base.Configs, err = mergeConfigs(base.Configs, override.Configs)
		if err != nil {
			return base, errors.Wrapf(err, "cannot merge configs from %s", override.Filename)
		}
		base.Extensions, err = mergeExtensions(base.Extensions, override.Extensions)
		if err != nil {
			return base, errors.Wrapf(err, "cannot merge extensions from %s", override.Filename)
		}
	}
	return base, nil
}

func mergeNames(base, override string) string {
	if override != "" {
		return override
	}
	return base
}

func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, error) {
	baseServices := mapByName(base)
	overrideServices := mapByName(override)
	for name, overrideService := range overrideServices {
		overrideService := overrideService
		if baseService, ok := baseServices[name]; ok {
			merged, err := _merge(&baseService, &overrideService)
			if err != nil {
				return nil, errors.Wrapf(err, "cannot merge service %s", name)
			}
			baseServices[name] = *merged
			continue
		}
		baseServices[name] = overrideService
	}
	services := []types.ServiceConfig{}
	for _, baseService := range baseServices {
		services = append(services, baseService)
	}
	sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name })
	return services, nil
}

func _merge(baseService *types.ServiceConfig, overrideService *types.ServiceConfig) (*types.ServiceConfig, error) {
	if err := mergo.Merge(baseService, overrideService,
		mergo.WithAppendSlice,
		mergo.WithOverride,
		mergo.WithTransformers(serviceSpecials)); err != nil {
		return nil, err
	}
	if overrideService.Command != nil {
		baseService.Command = overrideService.Command
	}
	if overrideService.HealthCheck != nil && overrideService.HealthCheck.Test != nil {
		baseService.HealthCheck.Test = overrideService.HealthCheck.Test
	}
	if overrideService.Entrypoint != nil {
		baseService.Entrypoint = overrideService.Entrypoint
	}
	if baseService.Environment != nil {
		baseService.Environment.OverrideBy(overrideService.Environment)
	} else {
		baseService.Environment = overrideService.Environment
	}
	baseService.Expose = unique(baseService.Expose)
	return baseService, nil
}

func unique(slice []string) []string {
	if slice == nil {
		return nil
	}
	uniqMap := make(map[string]struct{})
	var uniqSlice []string
	for _, v := range slice {
		if _, ok := uniqMap[v]; !ok {
			uniqSlice = append(uniqSlice, v)
			uniqMap[v] = struct{}{}
		}
	}
	return uniqSlice
}

func toServiceSecretConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
	secrets, ok := s.([]types.ServiceSecretConfig)
	if !ok {
		return nil, errors.Errorf("not a serviceSecretConfig: %v", s)
	}
	m := map[interface{}]interface{}{}
	for _, secret := range secrets {
		m[secret.Source] = secret
	}
	return m, nil
}

func toServiceConfigObjConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
	secrets, ok := s.([]types.ServiceConfigObjConfig)
	if !ok {
		return nil, errors.Errorf("not a serviceSecretConfig: %v", s)
	}
	m := map[interface{}]interface{}{}
	for _, secret := range secrets {
		m[secret.Source] = secret
	}
	return m, nil
}

func toServicePortConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
	ports, ok := s.([]types.ServicePortConfig)
	if !ok {
		return nil, errors.Errorf("not a servicePortConfig slice: %v", s)
	}
	m := map[interface{}]interface{}{}
	type port struct {
		target    uint32
		published string
		ip        string
		protocol  string
	}

	for _, p := range ports {
		mergeKey := port{
			target:    p.Target,
			published: p.Published,
			ip:        p.HostIP,
			protocol:  p.Protocol,
		}
		m[mergeKey] = p
	}
	return m, nil
}

func toServiceVolumeConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
	volumes, ok := s.([]types.ServiceVolumeConfig)
	if !ok {
		return nil, errors.Errorf("not a ServiceVolumeConfig slice: %v", s)
	}
	m := map[interface{}]interface{}{}
	for _, v := range volumes {
		m[v.Target] = v
	}
	return m, nil
}

func toServiceSecretConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
	var s []types.ServiceSecretConfig
	for _, v := range m {
		s = append(s, v.(types.ServiceSecretConfig))
	}
	sort.Slice(s, func(i, j int) bool { return s[i].Source < s[j].Source })
	dst.Set(reflect.ValueOf(s))
	return nil
}

func toSServiceConfigObjConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
	var s []types.ServiceConfigObjConfig
	for _, v := range m {
		s = append(s, v.(types.ServiceConfigObjConfig))
	}
	sort.Slice(s, func(i, j int) bool { return s[i].Source < s[j].Source })
	dst.Set(reflect.ValueOf(s))
	return nil
}

func toServicePortConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
	var s []types.ServicePortConfig
	for _, v := range m {
		s = append(s, v.(types.ServicePortConfig))
	}
	sort.Slice(s, func(i, j int) bool {
		if s[i].Target != s[j].Target {
			return s[i].Target < s[j].Target
		}
		if s[i].Published != s[j].Published {
			return s[i].Published < s[j].Published
		}
		if s[i].HostIP != s[j].HostIP {
			return s[i].HostIP < s[j].HostIP
		}
		return s[i].Protocol < s[j].Protocol
	})
	dst.Set(reflect.ValueOf(s))
	return nil
}

func toServiceVolumeConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
	var s []types.ServiceVolumeConfig
	for _, v := range m {
		s = append(s, v.(types.ServiceVolumeConfig))
	}
	sort.Slice(s, func(i, j int) bool { return s[i].Target < s[j].Target })
	dst.Set(reflect.ValueOf(s))
	return nil
}

type toMapFn func(s interface{}) (map[interface{}]interface{}, error)
type writeValueFromMapFn func(reflect.Value, map[interface{}]interface{}) error

func safelyMerge(mergeFn func(dst, src reflect.Value) error) func(dst, src reflect.Value) error {
	return func(dst, src reflect.Value) error {
		if src.IsNil() {
			return nil
		}
		if dst.IsNil() {
			dst.Set(src)
			return nil
		}
		return mergeFn(dst, src)
	}
}

func mergeSlice(toMap toMapFn, writeValue writeValueFromMapFn) func(dst, src reflect.Value) error {
	return func(dst, src reflect.Value) error {
		dstMap, err := sliceToMap(toMap, dst)
		if err != nil {
			return err
		}
		srcMap, err := sliceToMap(toMap, src)
		if err != nil {
			return err
		}
		if err := mergo.Map(&dstMap, srcMap, mergo.WithOverride); err != nil {
			return err
		}
		return writeValue(dst, dstMap)
	}
}

func sliceToMap(toMap toMapFn, v reflect.Value) (map[interface{}]interface{}, error) {
	// check if valid
	if !v.IsValid() {
		return nil, errors.Errorf("invalid value : %+v", v)
	}
	return toMap(v.Interface())
}

func mergeLoggingConfig(dst, src reflect.Value) error {
	// Same driver, merging options
	if getLoggingDriver(dst.Elem()) == getLoggingDriver(src.Elem()) ||
		getLoggingDriver(dst.Elem()) == "" || getLoggingDriver(src.Elem()) == "" {
		if getLoggingDriver(dst.Elem()) == "" {
			dst.Elem().FieldByName("Driver").SetString(getLoggingDriver(src.Elem()))
		}
		dstOptions := dst.Elem().FieldByName("Options").Interface().(map[string]string)
		srcOptions := src.Elem().FieldByName("Options").Interface().(map[string]string)
		return mergo.Merge(&dstOptions, srcOptions, mergo.WithOverride)
	}
	// Different driver, override with src
	dst.Set(src)
	return nil
}

// nolint: unparam
func mergeUlimitsConfig(dst, src reflect.Value) error {
	if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
		dst.Elem().Set(src.Elem())
	}
	return nil
}

func getLoggingDriver(v reflect.Value) string {
	return v.FieldByName("Driver").String()
}

func mapByName(services []types.ServiceConfig) map[string]types.ServiceConfig {
	m := map[string]types.ServiceConfig{}
	for _, service := range services {
		m[service.Name] = service
	}
	return m
}

func mergeVolumes(base, override map[string]types.VolumeConfig) (map[string]types.VolumeConfig, error) {
	err := mergo.Map(&base, &override, mergo.WithOverride)
	return base, err
}

func mergeNetworks(base, override map[string]types.NetworkConfig) (map[string]types.NetworkConfig, error) {
	err := mergo.Map(&base, &override, mergo.WithOverride)
	return base, err
}

func mergeSecrets(base, override map[string]types.SecretConfig) (map[string]types.SecretConfig, error) {
	err := mergo.Map(&base, &override, mergo.WithOverride)
	return base, err
}

func mergeConfigs(base, override map[string]types.ConfigObjConfig) (map[string]types.ConfigObjConfig, error) {
	err := mergo.Map(&base, &override, mergo.WithOverride)
	return base, err
}

func mergeExtensions(base, override map[string]interface{}) (map[string]interface{}, error) {
	if base == nil {
		base = map[string]interface{}{}
	}
	err := mergo.Map(&base, &override, mergo.WithOverride)
	return base, err
}
