// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or 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 config

import (
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/yunify/qingcloud-sdk-go/logger"
	"github.com/yunify/qingcloud-sdk-go/utils"
)

// A Config stores a configuration of this sdk.
type Config struct {
	AccessKeyID     string `yaml:"qy_access_key_id"`
	SecretAccessKey string `yaml:"qy_secret_access_key"`

	Host              string `yaml:"host"`
	Port              int    `yaml:"port"`
	Protocol          string `yaml:"protocol"`
	URI               string `yaml:"uri"`
	ConnectionRetries int    `yaml:"connection_retries"`
	ConnectionTimeout int    `yaml:"connection_timeout"`

	LogLevel string `yaml:"log_level"`

	Zone string `yaml:"zone"`

	Connection *http.Client
}

// New create a Config with given AccessKeyID and SecretAccessKey.
func New(accessKeyID, secretAccessKey string) (*Config, error) {
	config, err := NewDefault()
	if err != nil {
		return nil, err
	}

	config.AccessKeyID = accessKeyID
	config.SecretAccessKey = secretAccessKey

	config.Connection = &http.Client{}

	return config, nil
}

// NewWithEndpoint create a Config with given AccessKeyID, SecretAccessKey and endpoint
func NewWithEndpoint(accessKeyID, secretAccessKey, endpoint string) (*Config, error) {
	qcURL, err := url.Parse(endpoint)
	if err != nil {
		return nil, err
	}
	if qcURL.Opaque != "" {
		return nil, fmt.Errorf("wrong URL format")
	}

	if !strings.Contains(qcURL.Host, ":") {
		if qcURL.Scheme == "https" {
			qcURL.Host += ":443"
		} else if qcURL.Scheme == "http" {
			qcURL.Host += ":80"
		} else {
			return nil, fmt.Errorf("can not find default port ")
		}
	}

	config, err := NewDefault()
	if err != nil {
		return nil, err
	}

	host, port, err := net.SplitHostPort(qcURL.Host)
	if err != nil {
		return nil, err
	}

	config.Port, _ = strconv.Atoi(port)
	config.Host = host
	config.AccessKeyID = accessKeyID
	config.SecretAccessKey = secretAccessKey
	config.Protocol = qcURL.Scheme
	config.URI = qcURL.Path
	config.Connection = &http.Client{}
	return config, nil
}

// NewDefault create a Config with default configuration.
func NewDefault() (*Config, error) {
	config := &Config{}
	err := config.LoadDefaultConfig()
	if err != nil {
		return nil, err
	}

	timeout := time.Duration(config.ConnectionTimeout) * time.Second
	transport := &http.Transport{
		Dial: func(network, addr string) (net.Conn, error) {
			return net.DialTimeout(network, addr, timeout)
		},
	}
	config.Connection = &http.Client{
		Transport: transport,
	}

	return config, nil
}

// LoadDefaultConfig loads the default configuration for Config.
// It returns error if yaml decode failed.
func (c *Config) LoadDefaultConfig() error {
	_, err := utils.YAMLDecode([]byte(DefaultConfigFileContent), c)
	if err != nil {
		logger.Error("Config parse error: " + err.Error())
		return err
	}

	logger.SetLevel(c.LogLevel)

	return nil
}

// LoadUserConfig loads user configuration in ~/.qingcloud/config.yaml for Config.
// It returns error if file not found.
func (c *Config) LoadUserConfig() error {
	_, err := os.Stat(GetUserConfigFilePath())
	if err != nil {
		logger.Warn("Installing default config file to \"" + GetUserConfigFilePath() + "\"")
		InstallDefaultUserConfig()
	}

	return c.LoadConfigFromFilepath(GetUserConfigFilePath())
}

// LoadConfigFromFilepath loads configuration from a specified local path.
// It returns error if file not found or yaml decode failed.
func (c *Config) LoadConfigFromFilepath(filepath string) error {
	if strings.Index(filepath, "~/") == 0 {
		filepath = strings.Replace(filepath, "~/", getHome()+"/", 1)
	}

	configYAML, err := ioutil.ReadFile(filepath)
	if err != nil {
		logger.Error("File not found: " + filepath)
		return err
	}

	return c.LoadConfigFromContent(configYAML)
}

// LoadConfigFromContent loads configuration from a given byte slice.
// It returns error if yaml decode failed.
func (c *Config) LoadConfigFromContent(content []byte) error {
	c.LoadDefaultConfig()

	_, err := utils.YAMLDecode(content, c)
	if err != nil {
		logger.Error("Config parse error: " + err.Error())
		return err
	}

	logger.SetLevel(c.LogLevel)

	timeout := time.Duration(c.ConnectionTimeout) * time.Second
	transport := &http.Transport{
		Dial: func(network, addr string) (net.Conn, error) {
			return net.DialTimeout(network, addr, timeout)
		},
	}
	c.Connection = &http.Client{
		Transport: transport,
	}

	return nil
}
