package common

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	tcerr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
	tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http"
	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
)

const (
	octetStream = "application/octet-stream"
)

var DefaultHttpClient *http.Client

type Client struct {
	region          string
	httpClient      *http.Client
	httpProfile     *profile.HttpProfile
	profile         *profile.ClientProfile
	credential      CredentialIface
	signMethod      string
	unsignedPayload bool
	debug           bool
	rb              *circuitBreaker
	logger          Logger
	requestClient   string
}

func (c *Client) Send(request tchttp.Request, response tchttp.Response) (err error) {
	if request.GetScheme() == "" {
		request.SetScheme(c.httpProfile.Scheme)
	}

	if request.GetRootDomain() == "" {
		request.SetRootDomain(c.httpProfile.RootDomain)
	}

	if request.GetDomain() == "" {
		domain := c.httpProfile.Endpoint
		if domain == "" {
			domain = request.GetServiceDomain(request.GetService())
		}
		request.SetDomain(domain)
	}

	if request.GetHttpMethod() == "" {
		request.SetHttpMethod(c.httpProfile.ReqMethod)
	}

	tchttp.CompleteCommonParams(request, c.GetRegion(), c.requestClient)

	// reflect to inject client if field ClientToken exists and retry feature is enabled
	if c.profile.NetworkFailureMaxRetries > 0 || c.profile.RateLimitExceededMaxRetries > 0 {
		safeInjectClientToken(request)
	}

	if request.GetSkipSign() {
		// Some APIs can skip signature.
		return c.sendWithoutSignature(request, response)
	} else if c.profile.DisableRegionBreaker == true || c.rb == nil {
		return c.sendWithSignature(request, response)
	} else {
		return c.sendWithRegionBreaker(request, response)
	}
}

func (c *Client) sendWithRegionBreaker(request tchttp.Request, response tchttp.Response) (err error) {
	defer func() {
		e := recover()
		if e != nil {
			msg := fmt.Sprintf("%s", e)
			err = tcerr.NewTencentCloudSDKError("ClientError.CircuitBreakerError", msg, "")
		}
	}()

	ge, err := c.rb.beforeRequest()

	if err == errOpenState {
		newEndpoint := request.GetService() + "." + c.rb.backupEndpoint
		request.SetDomain(newEndpoint)
	}
	err = c.sendWithSignature(request, response)
	isSuccess := false
	// Success is considered only when the server returns an effective response (have requestId and the code is not InternalError )
	if e, ok := err.(*tcerr.TencentCloudSDKError); ok {
		if e.GetRequestId() != "" && e.GetCode() != "InternalError" {
			isSuccess = true
		}
	}
	c.rb.afterRequest(ge, isSuccess)
	return err
}

func (c *Client) sendWithSignature(request tchttp.Request, response tchttp.Response) (err error) {
	if c.signMethod == "HmacSHA1" || c.signMethod == "HmacSHA256" {
		return c.sendWithSignatureV1(request, response)
	} else {
		return c.sendWithSignatureV3(request, response)
	}
}

func (c *Client) sendWithoutSignature(request tchttp.Request, response tchttp.Response) error {
	headers := map[string]string{
		"Host":               request.GetDomain(),
		"X-TC-Action":        request.GetAction(),
		"X-TC-Version":       request.GetVersion(),
		"X-TC-Timestamp":     request.GetParams()["Timestamp"],
		"X-TC-RequestClient": request.GetParams()["RequestClient"],
		"X-TC-Language":      c.profile.Language,
		"Authorization":      "SKIP",
	}
	if c.region != "" {
		headers["X-TC-Region"] = c.region
	}
	if c.credential != nil && c.credential.GetToken() != "" {
		headers["X-TC-Token"] = c.credential.GetToken()
	}
	if request.GetHttpMethod() == "GET" {
		headers["Content-Type"] = "application/x-www-form-urlencoded"
	} else {
		headers["Content-Type"] = "application/json"
	}
	isOctetStream := false
	cr := &tchttp.CommonRequest{}
	ok := false
	var octetStreamBody []byte
	if cr, ok = request.(*tchttp.CommonRequest); ok {
		if cr.IsOctetStream() {
			isOctetStream = true
			// custom headers must contain Content-Type : application/octet-stream
			// todo:the custom header may overwrite headers
			for k, v := range cr.GetHeader() {
				headers[k] = v
			}
			octetStreamBody = cr.GetOctetStreamBody()
		}
	}

	for k, v := range request.GetHeader() {
		switch k {
		case "X-TC-Action", "X-TC-Version", "X-TC-Timestamp", "X-TC-RequestClient",
			"X-TC-Language", "Content-Type", "X-TC-Region", "X-TC-Token":
			c.logger.Printf("Skip header \"%s\": can not specify built-in header", k)
		default:
			headers[k] = v
		}
	}

	if !isOctetStream && request.GetContentType() == octetStream {
		isOctetStream = true
		b, _ := json.Marshal(request)
		var m map[string]string
		_ = json.Unmarshal(b, &m)
		for k, v := range m {
			key := "X-" + strings.ToUpper(request.GetService()) + "-" + k
			headers[key] = v
		}

		headers["Content-Type"] = octetStream
		octetStreamBody = request.GetBody()
	}
	// start signature v3 process

	// build canonical request string
	httpRequestMethod := request.GetHttpMethod()
	canonicalQueryString := ""
	if httpRequestMethod == "GET" {
		err := tchttp.ConstructParams(request)
		if err != nil {
			return err
		}
		params := make(map[string]string)
		for key, value := range request.GetParams() {
			params[key] = value
		}
		delete(params, "Action")
		delete(params, "Version")
		delete(params, "Nonce")
		delete(params, "Region")
		delete(params, "RequestClient")
		delete(params, "Timestamp")
		canonicalQueryString = tchttp.GetUrlQueriesEncoded(params)
	}
	requestPayload := ""
	if httpRequestMethod == "POST" {
		if isOctetStream {
			// todo Conversion comparison between string and []byte affects performance much
			requestPayload = string(octetStreamBody)
		} else {
			b, err := json.Marshal(request)
			if err != nil {
				return err
			}
			requestPayload = string(b)
		}
	}
	if c.unsignedPayload {
		headers["X-TC-Content-SHA256"] = "UNSIGNED-PAYLOAD"
	}

	url := request.GetScheme() + "://" + request.GetDomain() + request.GetPath()
	if canonicalQueryString != "" {
		url = url + "?" + canonicalQueryString
	}
	httpRequest, err := http.NewRequestWithContext(request.GetContext(), httpRequestMethod, url, strings.NewReader(requestPayload))
	if err != nil {
		return err
	}
	for k, v := range headers {
		httpRequest.Header[k] = []string{v}
	}
	httpResponse, err := c.sendWithRateLimitRetry(httpRequest, isRetryable(request))
	if err != nil {
		return err
	}
	err = tchttp.ParseFromHttpResponse(httpResponse, response)
	return err
}

func (c *Client) sendWithSignatureV1(request tchttp.Request, response tchttp.Response) (err error) {
	// TODO: not an elegant way, it should be done in common params, but finally it need to refactor
	request.GetParams()["Language"] = c.profile.Language
	err = tchttp.ConstructParams(request)
	if err != nil {
		return err
	}
	err = signRequest(request, c.credential, c.signMethod)
	if err != nil {
		return err
	}
	httpRequest, err := http.NewRequestWithContext(request.GetContext(), request.GetHttpMethod(), request.GetUrl(), request.GetBodyReader())
	if err != nil {
		return err
	}
	if request.GetHttpMethod() == "POST" {
		httpRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}

	for k, v := range request.GetHeader() {
		httpRequest.Header.Set(k, v)
	}

	httpResponse, err := c.sendWithRateLimitRetry(httpRequest, isRetryable(request))
	if err != nil {
		return err
	}
	err = tchttp.ParseFromHttpResponse(httpResponse, response)
	return err
}

func (c *Client) sendWithSignatureV3(request tchttp.Request, response tchttp.Response) (err error) {
	headers := map[string]string{
		"Host":               request.GetDomain(),
		"X-TC-Action":        request.GetAction(),
		"X-TC-Version":       request.GetVersion(),
		"X-TC-Timestamp":     request.GetParams()["Timestamp"],
		"X-TC-RequestClient": request.GetParams()["RequestClient"],
		"X-TC-Language":      c.profile.Language,
	}
	if c.region != "" {
		headers["X-TC-Region"] = c.region
	}
	if c.credential.GetToken() != "" {
		headers["X-TC-Token"] = c.credential.GetToken()
	}
	if request.GetHttpMethod() == "GET" {
		headers["Content-Type"] = "application/x-www-form-urlencoded"
	} else {
		headers["Content-Type"] = "application/json"
	}
	isOctetStream := false
	cr := &tchttp.CommonRequest{}
	ok := false
	var octetStreamBody []byte
	if cr, ok = request.(*tchttp.CommonRequest); ok {
		if cr.IsOctetStream() {
			isOctetStream = true
			// custom headers must contain Content-Type : application/octet-stream
			// todo:the custom header may overwrite headers
			for k, v := range cr.GetHeader() {
				headers[k] = v
			}
			octetStreamBody = cr.GetOctetStreamBody()
		}
	}

	for k, v := range request.GetHeader() {
		switch k {
		case "X-TC-Action", "X-TC-Version", "X-TC-Timestamp", "X-TC-RequestClient",
			"X-TC-Language", "X-TC-Region", "X-TC-Token":
			c.logger.Printf("Skip header \"%s\": can not specify built-in header", k)
		default:
			headers[k] = v
		}
	}

	if !isOctetStream && request.GetContentType() == octetStream {
		isOctetStream = true
		b, _ := json.Marshal(request)
		var m map[string]string
		_ = json.Unmarshal(b, &m)
		for k, v := range m {
			key := "X-" + strings.ToUpper(request.GetService()) + "-" + k
			headers[key] = v
		}

		headers["Content-Type"] = octetStream
		octetStreamBody = request.GetBody()
	}
	// start signature v3 process

	// build canonical request string
	httpRequestMethod := request.GetHttpMethod()
	canonicalURI := "/"
	canonicalQueryString := ""
	if httpRequestMethod == "GET" {
		err = tchttp.ConstructParams(request)
		if err != nil {
			return err
		}
		params := make(map[string]string)
		for key, value := range request.GetParams() {
			params[key] = value
		}
		delete(params, "Action")
		delete(params, "Version")
		delete(params, "Nonce")
		delete(params, "Region")
		delete(params, "RequestClient")
		delete(params, "Timestamp")
		canonicalQueryString = tchttp.GetUrlQueriesEncoded(params)
	}
	canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\n", headers["Content-Type"], headers["Host"])
	signedHeaders := "content-type;host"
	requestPayload := ""
	if httpRequestMethod == "POST" {
		if isOctetStream {
			// todo Conversion comparison between string and []byte affects performance much
			requestPayload = string(octetStreamBody)
		} else {
			b, err := json.Marshal(request)
			if err != nil {
				return err
			}
			requestPayload = string(b)
		}
	}
	hashedRequestPayload := ""
	if c.unsignedPayload {
		hashedRequestPayload = sha256hex("UNSIGNED-PAYLOAD")
		headers["X-TC-Content-SHA256"] = "UNSIGNED-PAYLOAD"
	} else {
		hashedRequestPayload = sha256hex(requestPayload)
	}
	canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
		httpRequestMethod,
		canonicalURI,
		canonicalQueryString,
		canonicalHeaders,
		signedHeaders,
		hashedRequestPayload)
	//log.Println("canonicalRequest:", canonicalRequest)

	// build string to sign
	algorithm := "TC3-HMAC-SHA256"
	requestTimestamp := headers["X-TC-Timestamp"]
	timestamp, _ := strconv.ParseInt(requestTimestamp, 10, 64)
	t := time.Unix(timestamp, 0).UTC()
	// must be the format 2006-01-02, ref to package time for more info
	date := t.Format("2006-01-02")
	credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, request.GetService())
	hashedCanonicalRequest := sha256hex(canonicalRequest)
	string2sign := fmt.Sprintf("%s\n%s\n%s\n%s",
		algorithm,
		requestTimestamp,
		credentialScope,
		hashedCanonicalRequest)
	//log.Println("string2sign", string2sign)

	// sign string
	secretDate := hmacsha256(date, "TC3"+c.credential.GetSecretKey())
	secretService := hmacsha256(request.GetService(), secretDate)
	secretKey := hmacsha256("tc3_request", secretService)
	signature := hex.EncodeToString([]byte(hmacsha256(string2sign, secretKey)))
	//log.Println("signature", signature)

	// build authorization
	authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
		algorithm,
		c.credential.GetSecretId(),
		credentialScope,
		signedHeaders,
		signature)
	//log.Println("authorization", authorization)

	headers["Authorization"] = authorization
	url := request.GetScheme() + "://" + request.GetDomain() + request.GetPath()
	if canonicalQueryString != "" {
		url = url + "?" + canonicalQueryString
	}
	httpRequest, err := http.NewRequestWithContext(request.GetContext(), httpRequestMethod, url, strings.NewReader(requestPayload))
	if err != nil {
		return err
	}
	for k, v := range headers {
		httpRequest.Header[k] = []string{v}
	}
	httpResponse, err := c.sendWithRateLimitRetry(httpRequest, isRetryable(request))
	if err != nil {
		return err
	}
	err = tchttp.ParseFromHttpResponse(httpResponse, response)
	return err
}

// send http request
func (c *Client) sendHttp(request *http.Request) (response *http.Response, err error) {
	if c.debug && request != nil {
		outBytes, err := httputil.DumpRequest(request, true)
		if err != nil {
			c.logger.Printf("[ERROR] dump request failed: %s", err)
		} else {
			c.logger.Printf("[DEBUG] http request: %s", outBytes)
		}
	}

	response, err = c.httpClient.Do(request)

	if c.debug && response != nil {
		out, err := httputil.DumpResponse(response, true)
		if err != nil {
			c.logger.Printf("[ERROR] dump response failed: %s", err)
		} else {
			c.logger.Printf("[DEBUG] http response: %s", out)
		}
	}

	return response, err
}

func (c *Client) GetRegion() string {
	return c.region
}

func (c *Client) Init(region string) *Client {

	if DefaultHttpClient == nil {
		// try not to modify http.DefaultTransport if possible
		// since we could possibly modify Transport.Proxy
		transport := http.DefaultTransport
		if ht, ok := transport.(*http.Transport); ok {
			transport = ht.Clone()
		}

		c.httpClient = &http.Client{Transport: transport}
	} else {
		c.httpClient = DefaultHttpClient
	}

	c.region = region
	c.signMethod = "TC3-HMAC-SHA256"
	c.debug = false
	c.logger = log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile)
	return c
}

func (c *Client) WithSecretId(secretId, secretKey string) *Client {
	c.credential = NewCredential(secretId, secretKey)
	return c
}

func (c *Client) WithCredential(cred CredentialIface) *Client {
	c.credential = cred
	return c
}

func (c *Client) WithRequestClient(rc string) *Client {
	const reRequestClient = "^[0-9a-zA-Z-_ ,;.]+$"

	if len(rc) > 128 {
		c.logger.Printf("the length of RequestClient should be within 128 characters, it will be truncated")
		rc = rc[:128]
	}

	match, err := regexp.MatchString(reRequestClient, rc)
	if err != nil {
		c.logger.Printf("regexp is wrong: %s", reRequestClient)
		return c
	}
	if !match {
		c.logger.Printf("RequestClient not match the regexp: %s, ignored", reRequestClient)
		return c
	}

	c.requestClient = rc
	return c
}

func (c *Client) GetCredential() CredentialIface {
	return c.credential
}

func (c *Client) WithProfile(clientProfile *profile.ClientProfile) *Client {
	c.profile = clientProfile
	if c.profile.DisableRegionBreaker == false {
		c.withRegionBreaker()
	}
	c.signMethod = clientProfile.SignMethod
	c.unsignedPayload = clientProfile.UnsignedPayload
	c.httpProfile = clientProfile.HttpProfile
	c.debug = clientProfile.Debug
	c.httpClient.Timeout = time.Duration(c.httpProfile.ReqTimeout) * time.Second
	if c.httpProfile.Proxy != "" {
		u, err := url.Parse(c.httpProfile.Proxy)
		if err != nil {
			panic(err)
		}

		if c.httpClient.Transport == nil {
			c.logger.Printf("trying to set proxy when httpClient.Transport is nil")
		}

		if _, ok := c.httpClient.Transport.(*http.Transport); ok {
			c.httpClient.Transport.(*http.Transport).Proxy = http.ProxyURL(u)
		} else {
			c.logger.Printf("setting proxy while httpClient.Transport is not a http.Transport is not supported")
		}
	}
	return c
}

func (c *Client) WithSignatureMethod(method string) *Client {
	c.signMethod = method
	return c
}

func (c *Client) WithHttpTransport(transport http.RoundTripper) *Client {
	c.httpClient.Transport = transport
	return c
}

func (c *Client) WithDebug(flag bool) *Client {
	c.debug = flag
	return c
}

// WithProvider use specify provider to get a credential and use it to build a client
func (c *Client) WithProvider(provider Provider) (*Client, error) {
	cred, err := provider.GetCredential()
	if err != nil {
		return nil, err
	}
	return c.WithCredential(cred), nil
}

func (c *Client) withRegionBreaker() *Client {
	rb := defaultRegionBreaker()
	if c.profile.BackupEndpoint != "" {
		rb.backupEndpoint = c.profile.BackupEndpoint
	} else if c.profile.BackupEndPoint != "" {
		rb.backupEndpoint = c.profile.BackupEndPoint
	}
	c.rb = rb
	return c
}

func NewClientWithSecretId(secretId, secretKey, region string) (client *Client, err error) {
	client = &Client{}
	client.Init(region).WithSecretId(secretId, secretKey)
	return
}

// NewClientWithProviders build client with your custom providers;
// If you don't specify the providers, it will use the DefaultProviderChain to find credential
func NewClientWithProviders(region string, providers ...Provider) (client *Client, err error) {
	client = (&Client{}).Init(region)
	var pc Provider
	if len(providers) == 0 {
		pc = DefaultProviderChain()
	} else {
		pc = NewProviderChain(providers)
	}
	return client.WithProvider(pc)
}
