package sdk

import (
	"bytes"
	"context"
	"crypto/tls"
	"fmt"
	"github.com/json-iterator/go/extra"
	"golib.gaore.com/GaoreGo/gaore-common-sdk-go/sdk/auth"
	"golib.gaore.com/GaoreGo/gaore-common-sdk-go/sdk/auth/credentials"
	"golib.gaore.com/GaoreGo/gaore-common-sdk-go/sdk/requests"
	"golib.gaore.com/GaoreGo/gaore-common-sdk-go/sdk/responses"
	"golib.gaore.com/GaoreGo/gaore-common-sdk-go/sdk/utils"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"os"
	"regexp"
	"runtime"
	"strings"
	"sync"
	"time"
)

var Version = "0.0.1"
var defaultConnectTimeout = 5 * time.Second
var defaultReadTimeout = 10 * time.Second
var defaultDomain = ".gaore.com"
var DefaultUserAgent = fmt.Sprintf("GaoreGoSdk (%s;%s) Golang/%s Core/%s", runtime.GOOS, runtime.GOARCH, strings.Trim(runtime.Version(), "go"), Version)

var debug utils.Debug

func init() {
	debug = utils.Init("sdk")
}

type Client struct {
	Host           string
	httpClient     *http.Client
	isInsecure     bool
	signer         auth.Signer
	readTimeout    time.Duration
	connectTimeout time.Duration
	config         *Config
	httpProxy      string
	httpsProxy     string
	noProxy        string
}

func (client *Client) GetNoProxy() string {
	return client.noProxy
}

func (client *Client) SetNoProxy(noProxy string) {
	client.noProxy = noProxy
}

func (client *Client) GetHttpsProxy() string {
	return client.httpsProxy
}

func (client *Client) SetHttpsProxy(httpsProxy string) {
	client.httpsProxy = httpsProxy
}

func (client *Client) SetHttpProxy(httpProxy string) {
	client.httpProxy = httpProxy
}

func (client *Client) GetHttpProxy() string {
	return client.httpProxy
}

func (client *Client) GetHTTPSInsecure() bool {
	return client.isInsecure
}

func (client *Client) InitClientConfig() (config *Config) {
	if client.config != nil {
		return client.config
	} else {
		return NewConfig()
	}

}

func (client *Client) Init() (err error) {
	return client.InitWithAccessKey("", "", "")
}

func (client *Client) InitWithAccessKey(accessKeyId, accessKeySecret, accessKeyFrom string) (err error) {
	config := client.InitWithConfig()
	credential := &credentials.BaseCredential{
		AccessKeyId:     accessKeyId,
		AccessKeySecret: accessKeySecret,
		AccessKeyFrom:   accessKeyFrom,
	}
	return client.InitWithOptions(config, credential)
}

func (client *Client) InitWithAliAppcode(accessKeyId, accessKeySecret string, env ...string) (err error) {
	config := client.InitWithConfig()
	credential := credentials.NewAliAppcodeCredential(accessKeyId, accessKeySecret)

	if len(env) > 0 {
		config.Env = env[0]
	}

	return client.InitWithOptions(config, credential)
}

func (client *Client) InitWithStsToken(accessKeyId, accessKeySecret, accessKeyFrom string) (err error) {
	config := client.InitWithConfig()
	credential := &credentials.StdTokenCredential{
		AccessKeyId:     accessKeyId,
		AccessKeySecret: accessKeySecret,
		AccessKeyFrom:   accessKeyFrom,
	}
	return client.InitWithOptions(config, credential)
}

func (client *Client) InitWithOptions(config *Config, credential auth.Credential) (err error) {
	client.httpClient = &http.Client{}
	client.config = config

	if config.Transport != nil {
		client.httpClient.Transport = config.Transport
	} else if config.HttpTransport != nil {
		client.httpClient.Transport = config.HttpTransport
	}

	if config.Timeout > 0 {
		client.httpClient.Timeout = config.Timeout
	}

	client.signer, err = auth.NewSignerWithCredential(credential, client.ProcessCommonRequestWithSigner)
	return
}

func (client *Client) InitWithConfig() (config *Config) {
	if client.config != nil {
		return client.config
	} else {
		return NewConfig()
	}
}

func (client *Client) ProcessCommonRequestWithSigner(request *requests.CommonRequest, signerInterface interface{}) (response *responses.CommonResponse, err error) {
	if signer, isSigner := signerInterface.(auth.Signer); isSigner {
		response = responses.NewCommonResponse()
		err = client.DoActionWithSigner(request, response, signer)
		return
	}
	panic("should not be here")
}

func Timeout(connectTimeout time.Duration) func(ctx context.Context, net, addr string) (c net.Conn, err error) {
	return func(ctx context.Context, network, address string) (c net.Conn, err error) {
		return (&net.Dialer{
			Timeout:   connectTimeout,
			DualStack: true,
		}).DialContext(ctx, network, address)
	}
}

func (client *Client) setTimeOut(request requests.AcsRequest) {
	readTimeout, connectTimeout := client.getTimeOut(request)
	client.httpClient.Timeout = readTimeout
	if trans, ok := client.httpClient.Transport.(*http.Transport); ok && trans != nil {
		trans.DialContext = Timeout(connectTimeout)
		client.httpClient.Transport = trans
	} else if client.httpClient.Transport == nil {
		client.httpClient.Transport = &http.Transport{
			DialContext: Timeout(connectTimeout),
		}
	}
}

func (client *Client) getTimeOut(request requests.AcsRequest) (time.Duration, time.Duration) {
	readTimeOut := defaultReadTimeout
	connectTimeOut := defaultConnectTimeout

	reqReadTimeout := request.GetReadTimeout()
	reqConnectTimeout := request.GetConnectTimeout()
	if reqReadTimeout != 0*time.Millisecond {
		readTimeOut = reqReadTimeout
	} else if client.readTimeout != 0*time.Microsecond {
		readTimeOut = client.readTimeout
	} else if client.httpClient.Timeout != 0 {
		readTimeOut = client.httpClient.Timeout
	}

	if reqConnectTimeout != 0*time.Microsecond {
		connectTimeOut = reqConnectTimeout
	} else if client.connectTimeout != 0*time.Millisecond {
		connectTimeOut = client.connectTimeout
	}
	return readTimeOut, connectTimeOut
}

func (client *Client) getHTTPSInsecure(request requests.AcsRequest) (insecure bool) {
	if request.GetHTTPSInsecure() != nil {
		insecure = *request.GetHTTPSInsecure()
	} else {
		insecure = client.GetHTTPSInsecure()
	}
	return
}

func (client *Client) DoAction(request requests.AcsRequest, response responses.AcsResponse) (err error) {
	return client.DoActionWithSigner(request, response, nil)
}

func (client *Client) DoActionWithSigner(request requests.AcsRequest, response responses.AcsResponse, signer auth.Signer) (err error) {

	httpRequest, err := client.buildRequestWithSigner(request, signer)
	if err != nil {
		return err
	}

	client.setTimeOut(request)
	proxy, err := client.getHttpProxy(httpRequest.URL.Scheme)
	if err != nil {
		return err
	}
	noProxy := client.getNoProxy(httpRequest.URL.Scheme)
	var flag bool
	for _, value := range noProxy {
		if strings.HasPrefix(value, "*") {
			value = fmt.Sprint(".%s", value)
		}
		noProxyReg, err := regexp.Compile(value)
		if err != nil {
			return err
		}
		if noProxyReg.MatchString(httpRequest.Host) {
			flag = true
			break
		}
	}

	if trans, ok := client.httpClient.Transport.(*http.Transport); ok && trans != nil {
		if trans.TLSClientConfig != nil {
			trans.TLSClientConfig.InsecureSkipVerify = client.getHTTPSInsecure(request)
		} else {
			trans.TLSClientConfig = &tls.Config{
				InsecureSkipVerify: client.getHTTPSInsecure(request),
			}
		}

		if proxy != nil && !flag {
			trans.Proxy = http.ProxyURL(proxy)
		}

		client.httpClient.Transport = trans
	}

	dump, err := httputil.DumpRequest(httpRequest, true)
	debug("client %s", bytes.NewBuffer(dump).String())

	var httpResponse *http.Response
	httpResponse, err = hookDo(client.httpClient.Do)(httpRequest)
	if err != nil {
		return
	}

	err = responses.Unmarshal(response, httpResponse, request.GetAcceptFormat())
	return
}

func (client *Client) buildRequestWithSigner(request requests.AcsRequest, signer auth.Signer) (httpRequest *http.Request, err error) {
	// init param
	domain := request.GetDomain()
	if strings.Index(domain.Default, ".") < 0 {
		domain.Default += defaultDomain
		request.SetDomain(domain)
	}

	if request.GetScheme() == "" {
		request.SetScheme(client.config.Scheme)
	}

	if request.GetEnv() == "" && client.config.Env != "" {
		request.SetEnv(client.config.Env)
	}

	err = requests.InitParam(request)
	if err != nil {
		return
	}
	// build signature
	var finalSigner auth.Signer
	if signer != nil {
		finalSigner = signer
	} else {
		finalSigner = client.signer
	}
	err = auth.Sign(request, finalSigner)
	if err != nil {
		return
	}

	// build request
	requestMethod := request.GetMethod()
	requestUrl := request.BuildUrl()
	body := request.GetBodyReader()
	httpRequest, err = http.NewRequest(requestMethod, requestUrl, body)
	if err != nil {
		return
	}

	for key, val := range request.GetHeaders() {
		httpRequest.Header[key] = []string{val}
	}

	if host, isContainsHost := request.GetHeaders()["host"]; isContainsHost {
		httpRequest.Host = host
	}

	userAgent := DefaultUserAgent
	httpRequest.Header.Set("User-Agent", userAgent)

	return
}

func (client *Client) getHttpProxy(scheme string) (proxy *url.URL, err error) {
	switch scheme {
	case "https":
		if client.GetHttpsProxy() != "" {
			proxy, err = url.Parse(client.httpsProxy)
		} else if rawurl := os.Getenv("HTTPS_PROXY"); rawurl != "" {
			proxy, err = url.Parse(rawurl)
		} else if rawurl := os.Getenv("https_proxy"); rawurl != "" {
			proxy, err = url.Parse(rawurl)
		}
	default:
		if client.GetHttpProxy() != "" {
			proxy, err = url.Parse(client.httpProxy)
		} else if rawurl := os.Getenv("HTTP_PROXY"); rawurl != "" {
			proxy, err = url.Parse(rawurl)
		} else if rawurl := os.Getenv("http_proxy"); rawurl != "" {
			proxy, err = url.Parse(rawurl)
		}
	}
	return
}

func (client *Client) getNoProxy(scheme string) []string {
	var urls []string
	if client.GetNoProxy() != "" {
		urls = strings.Split(client.noProxy, ",")
	} else if rawurl := os.Getenv("NO_PROXY"); rawurl != "" {
		urls = strings.Split(rawurl, ",")
	} else if rawurl := os.Getenv("no_proxy"); rawurl != "" {
		urls = strings.Split(rawurl, ",")
	}
	return urls
}

func hookDo(fn func(req *http.Request) (*http.Response, error)) func(req *http.Request) (*http.Response, error) {
	return fn
}

func init() {
	once := sync.Once{}
	once.Do(func() {
		extra.RegisterFuzzyDecoders()
	})
}