package clientconfig

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"os/user"
	"path/filepath"
	"reflect"

	"github.com/gophercloud/gophercloud"
	"github.com/gophercloud/utils/env"
)

// defaultIfEmpty is a helper function to make it cleaner to set default value
// for strings.
func defaultIfEmpty(value string, defaultValue string) string {
	if value == "" {
		return defaultValue
	}
	return value
}

// mergeCLouds merges two Clouds recursively (the AuthInfo also gets merged).
// In case both Clouds define a value, the value in the 'override' cloud takes precedence
func mergeClouds(override, cloud interface{}) (*Cloud, error) {
	overrideJson, err := json.Marshal(override)
	if err != nil {
		return nil, err
	}
	cloudJson, err := json.Marshal(cloud)
	if err != nil {
		return nil, err
	}
	var overrideInterface interface{}
	err = json.Unmarshal(overrideJson, &overrideInterface)
	if err != nil {
		return nil, err
	}
	var cloudInterface interface{}
	err = json.Unmarshal(cloudJson, &cloudInterface)
	if err != nil {
		return nil, err
	}
	var mergedCloud Cloud
	mergedInterface := mergeInterfaces(overrideInterface, cloudInterface)
	mergedJson, err := json.Marshal(mergedInterface)
	err = json.Unmarshal(mergedJson, &mergedCloud)
	if err != nil {
		return nil, err
	}
	return &mergedCloud, nil
}

// merges two interfaces. In cases where a value is defined for both 'overridingInterface' and
// 'inferiorInterface' the value in 'overridingInterface' will take precedence.
func mergeInterfaces(overridingInterface, inferiorInterface interface{}) interface{} {
	switch overriding := overridingInterface.(type) {
	case map[string]interface{}:
		interfaceMap, ok := inferiorInterface.(map[string]interface{})
		if !ok {
			return overriding
		}
		for k, v := range interfaceMap {
			if overridingValue, ok := overriding[k]; ok {
				overriding[k] = mergeInterfaces(overridingValue, v)
			} else {
				overriding[k] = v
			}
		}
	case []interface{}:
		list, ok := inferiorInterface.([]interface{})
		if !ok {
			return overriding
		}
		for i := range list {
			overriding = append(overriding, list[i])
		}
		return overriding
	case nil:
		// mergeClouds(nil, map[string]interface{...}) -> map[string]interface{...}
		v, ok := inferiorInterface.(map[string]interface{})
		if ok {
			return v
		}
	}
	// We don't want to override with empty values
	if reflect.DeepEqual(overridingInterface, nil) || reflect.DeepEqual(reflect.Zero(reflect.TypeOf(overridingInterface)).Interface(), overridingInterface) {
		return inferiorInterface
	} else {
		return overridingInterface
	}
}

// FindAndReadCloudsYAML attempts to locate a clouds.yaml file in the following
// locations:
//
// 1. OS_CLIENT_CONFIG_FILE
// 2. Current directory.
// 3. unix-specific user_config_dir (~/.config/openstack/clouds.yaml)
// 4. unix-specific site_config_dir (/etc/openstack/clouds.yaml)
//
// If found, the contents of the file is returned.
func FindAndReadCloudsYAML() (string, []byte, error) {
	// OS_CLIENT_CONFIG_FILE
	if v := env.Getenv("OS_CLIENT_CONFIG_FILE"); v != "" {
		if ok := fileExists(v); ok {
			content, err := ioutil.ReadFile(v)
			return v, content, err
		}
	}

	s, b, err := FindAndReadYAML("clouds.yaml")
	if s == "" {
		return FindAndReadYAML("clouds.yml")
	}
	return s, b, err
}

func FindAndReadPublicCloudsYAML() (string, []byte, error) {
	s, b, err := FindAndReadYAML("clouds-public.yaml")
	if s == "" {
		return FindAndReadYAML("clouds-public.yml")
	}
	return s, b, err
}

func FindAndReadSecureCloudsYAML() (string, []byte, error) {
	s, b, err := FindAndReadYAML("secure.yaml")
	if s == "" {
		return FindAndReadYAML("secure.yml")
	}
	return s, b, err
}

func FindAndReadYAML(yamlFile string) (string, []byte, error) {
	// current directory
	cwd, err := os.Getwd()
	if err != nil {
		return "", nil, fmt.Errorf("unable to determine working directory: %w", err)
	}

	filename := filepath.Join(cwd, yamlFile)
	if ok := fileExists(filename); ok {
		content, err := ioutil.ReadFile(filename)
		return filename, content, err
	}

	// unix user config directory: ~/.config/openstack.
	if currentUser, err := user.Current(); err == nil {
		homeDir := currentUser.HomeDir
		if homeDir != "" {
			filename := filepath.Join(homeDir, ".config/openstack/"+yamlFile)
			if ok := fileExists(filename); ok {
				content, err := ioutil.ReadFile(filename)
				return filename, content, err
			}
		}
	}

	// unix-specific site config directory: /etc/openstack.
	filename = "/etc/openstack/" + yamlFile
	if ok := fileExists(filename); ok {
		content, err := ioutil.ReadFile(filename)
		return filename, content, err
	}

	return "", nil, fmt.Errorf("no %s file found: %w", yamlFile, os.ErrNotExist)
}

// fileExists checks for the existence of a file at a given location.
func fileExists(filename string) bool {
	if _, err := os.Stat(filename); err == nil {
		return true
	}
	return false
}

// GetEndpointType is a helper method to determine the endpoint type
// requested by the user.
func GetEndpointType(endpointType string) gophercloud.Availability {
	if endpointType == "internal" || endpointType == "internalURL" {
		return gophercloud.AvailabilityInternal
	}
	if endpointType == "admin" || endpointType == "adminURL" {
		return gophercloud.AvailabilityAdmin
	}
	return gophercloud.AvailabilityPublic
}
