/*
 * Copyright 2017 Baidu, Inc.
 *
 * 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.
 */

// client.go - definiton the BceClientConfiguration and BceClient structure

// Package bce implements the infrastructure to access BCE services.
//
//   - BceClient:
//     It is the general client of BCE to access all services. It builds http request to access the
//     services based on the given client configuration.
//
//   - BceClientConfiguration:
//     The client configuration data structure which contains endpoint, region, credentials, retry
//     policy, sign options and so on. It supports most of the default value and user can also
//     access or change the default with its public fields' name.
//
//   - Error types:
//     The error types when making request or receiving response to the BCE services contains two
//     types: the BceClientError when making request to BCE services and the BceServiceError when
//     recieving response from them.
//
//   - BceRequest:
//     The request instance stands for an request to access the BCE services.
//
//   - BceResponse:
//     The response instance stands for an response from the BCE services.
package bce

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"time"

	"github.com/baidubce/bce-sdk-go/auth"
	"github.com/baidubce/bce-sdk-go/http"
	"github.com/baidubce/bce-sdk-go/util"
	"github.com/baidubce/bce-sdk-go/util/log"
)

// Client is the general interface which can perform sending request. Different service
// will define its own client in case of specific extension.
type Client interface {
	SendRequest(*BceRequest, *BceResponse) error
	SendRequestFromBytes(*BceRequest, *BceResponse, []byte) error
	GetBceClientConfig() *BceClientConfiguration
}

// BceClient defines the general client to access the BCE services.
type BceClient struct {
	Config *BceClientConfiguration
	Signer auth.Signer // the sign algorithm
}

// BuildHttpRequest - the helper method for the client to build http request
//
// PARAMS:
//   - request: the input request object to be built
func (c *BceClient) buildHttpRequest(request *BceRequest) {
	// Construct the http request instance for the special fields
	request.BuildHttpRequest()

	// Set the client specific configurations
	if request.Endpoint() == "" {
		request.SetEndpoint(c.Config.Endpoint)
	}
	if request.Protocol() == "" {
		request.SetProtocol(DEFAULT_PROTOCOL)
	}
	if len(c.Config.ProxyUrl) != 0 {
		request.SetProxyUrl(c.Config.ProxyUrl)
	}
	request.SetTimeout(c.Config.ConnectionTimeoutInMillis / 1000)

	// Set the BCE request headers
	request.SetHeader(http.HOST, request.Host())
	request.SetHeader(http.USER_AGENT, c.Config.UserAgent)
	request.SetHeader(http.BCE_DATE, util.FormatISO8601Date(util.NowUTCSeconds()))

	//set default content-type if null
	if request.Header(http.CONTENT_TYPE) == "" {
		request.SetHeader(http.CONTENT_TYPE, DEFAULT_CONTENT_TYPE)
	}

	// Generate the auth string if needed
	if c.Config.Credentials != nil {
		c.Signer.Sign(&request.Request, c.Config.Credentials, c.Config.SignOption)
	}
}

// SendRequest - the client performs sending the http request with retry policy and receive the
// response from the BCE services.
//
// PARAMS:
//   - req: the request object to be sent to the BCE service
//   - resp: the response object to receive the content from BCE service
//
// RETURNS:
//   - error: nil if ok otherwise the specific error
func (c *BceClient) SendRequest(req *BceRequest, resp *BceResponse) error {
	// Return client error if it is not nil
	if req.ClientError() != nil {
		return req.ClientError()
	}

	// Build the http request and prepare to send
	c.buildHttpRequest(req)
	log.Infof("send http request: %v", req)

	// Send request with the given retry policy
	retries := 0
	if req.Body() != nil {
		defer req.Body().Close() // Manually close the ReadCloser body for retry
	}
	for {
		// The request body should be temporarily saved if retry to send the http request
		var retryBuf bytes.Buffer
		var teeReader io.Reader
		if c.Config.Retry.ShouldRetry(nil, 0) && req.Body() != nil {
			teeReader = io.TeeReader(req.Body(), &retryBuf)
			req.Request.SetBody(ioutil.NopCloser(teeReader))
		}
		httpResp, err := http.Execute(&req.Request)

		if err != nil {
			if c.Config.Retry.ShouldRetry(err, retries) {
				delay_in_mills := c.Config.Retry.GetDelayBeforeNextRetryInMillis(err, retries)
				time.Sleep(delay_in_mills)
			} else {
				return &BceClientError{
					fmt.Sprintf("execute http request failed! Retried %d times, error: %v",
						retries, err)}
			}
			retries++
			log.Warnf("send request failed: %v, retry for %d time(s)", err, retries)
			if req.Body() != nil {
				ioutil.ReadAll(teeReader)
				req.Request.SetBody(ioutil.NopCloser(&retryBuf))
			}
			continue
		}
		resp.SetHttpResponse(httpResp)
		resp.ParseResponse()

		log.Infof("receive http response: status: %s, debugId: %s, requestId: %s, elapsed: %v",
			resp.StatusText(), resp.DebugId(), resp.RequestId(), resp.ElapsedTime())

		if resp.ElapsedTime().Milliseconds() > DEFAULT_WARN_LOG_TIMEOUT_IN_MILLS {
			log.Warnf("request time more than 5 second, debugId: %s, requestId: %s, elapsed: %v",
				resp.DebugId(), resp.RequestId(), resp.ElapsedTime())
		}
		for k, v := range resp.Headers() {
			log.Debugf("%s=%s", k, v)
		}
		if resp.IsFail() {
			err := resp.ServiceError()
			if c.Config.Retry.ShouldRetry(err, retries) {
				delay_in_mills := c.Config.Retry.GetDelayBeforeNextRetryInMillis(err, retries)
				time.Sleep(delay_in_mills)
			} else {
				return err
			}
			retries++
			log.Warnf("send request failed, retry for %d time(s)", retries)
			if req.Body() != nil {
				ioutil.ReadAll(teeReader)
				req.Request.SetBody(ioutil.NopCloser(&retryBuf))
			}
			continue
		}
		return nil
	}
}

// SendRequestFromBytes - the client performs sending the http request with retry policy and receive the
// response from the BCE services.
//
// PARAMS:
//   - req: the request object to be sent to the BCE service
//   - resp: the response object to receive the content from BCE service
//   - content: the content of body
//
// RETURNS:
//   - error: nil if ok otherwise the specific error
func (c *BceClient) SendRequestFromBytes(req *BceRequest, resp *BceResponse, content []byte) error {
	// Return client error if it is not nil
	if req.ClientError() != nil {
		return req.ClientError()
	}
	// Build the http request and prepare to send
	c.buildHttpRequest(req)
	log.Infof("send http request: %v", req)
	// Send request with the given retry policy
	retries := 0
	for {
		// The request body should be temporarily saved if retry to send the http request
		buf := bytes.NewBuffer(content)
		req.Request.SetBody(ioutil.NopCloser(buf))
		defer req.Request.Body().Close() // Manually close the ReadCloser body for retry
		httpResp, err := http.Execute(&req.Request)
		if err != nil {
			if c.Config.Retry.ShouldRetry(err, retries) {
				delay_in_mills := c.Config.Retry.GetDelayBeforeNextRetryInMillis(err, retries)
				time.Sleep(delay_in_mills)
			} else {
				return &BceClientError{
					fmt.Sprintf("execute http request failed! Retried %d times, error: %v",
						retries, err)}
			}
			retries++
			log.Warnf("send request failed: %v, retry for %d time(s)", err, retries)
			continue
		}
		resp.SetHttpResponse(httpResp)
		resp.ParseResponse()
		log.Infof("receive http response: status: %s, debugId: %s, requestId: %s, elapsed: %v",
			resp.StatusText(), resp.DebugId(), resp.RequestId(), resp.ElapsedTime())
		for k, v := range resp.Headers() {
			log.Debugf("%s=%s", k, v)
		}
		if resp.IsFail() {
			err := resp.ServiceError()
			if c.Config.Retry.ShouldRetry(err, retries) {
				delay_in_mills := c.Config.Retry.GetDelayBeforeNextRetryInMillis(err, retries)
				time.Sleep(delay_in_mills)
			} else {
				return err
			}
			retries++
			log.Warnf("send request failed, retry for %d time(s)", retries)
			continue
		}
		return nil
	}
}

func (c *BceClient) GetBceClientConfig() *BceClientConfiguration {
	return c.Config
}

func NewBceClientWithTimeout(conf *BceClientConfiguration, sign auth.Signer) *BceClient {
	clientConfig := &http.ClientConfig{
		RedirectDisabled:      conf.RedirectDisabled,
		DisableKeepAlives:     conf.DisableKeepAlives,
		NoVerifySSL:           conf.NoVerifySSL,
		DialTimeout:           conf.DialTimeout,
		KeepAlive:             conf.KeepAlive,
		ReadTimeout:           conf.ReadTimeout,
		WriteTimeout:          conf.WriteTimeOut,
		TLSHandshakeTimeout:   conf.TLSHandshakeTimeout,
		IdleConnectionTimeout: conf.IdleConnectionTimeout,
		ResponseHeaderTimeout: conf.ResponseHeaderTimeout,
		HTTPClientTimeout:     conf.HTTPClientTimeout,
		HTTPClient:            conf.HTTPClient,
	}

	http.InitClientWithTimeout(clientConfig)
	return &BceClient{conf, sign}
}

func NewBceClient(conf *BceClientConfiguration, sign auth.Signer) *BceClient {
	clientConfig := http.ClientConfig{
		RedirectDisabled:  conf.RedirectDisabled,
		DisableKeepAlives: conf.DisableKeepAlives,
	}
	http.InitClient(clientConfig)
	return &BceClient{conf, sign}
}

func NewBceClientWithAkSk(ak, sk, endPoint string) (*BceClient, error) {
	credentials, err := auth.NewBceCredentials(ak, sk)
	if err != nil {
		return nil, err
	}
	defaultSignOptions := &auth.SignOptions{
		HeadersToSign: auth.DEFAULT_HEADERS_TO_SIGN,
		ExpireSeconds: auth.DEFAULT_EXPIRE_SECONDS}
	defaultConf := &BceClientConfiguration{
		Endpoint:                  endPoint,
		Region:                    DEFAULT_REGION,
		UserAgent:                 DEFAULT_USER_AGENT,
		Credentials:               credentials,
		SignOption:                defaultSignOptions,
		Retry:                     DEFAULT_RETRY_POLICY,
		ConnectionTimeoutInMillis: DEFAULT_CONNECTION_TIMEOUT_IN_MILLIS,
		RedirectDisabled:          false}
	v1Signer := &auth.BceV1Signer{}

	return NewBceClient(defaultConf, v1Signer), nil
}
