// Copyright 2019 Huawei Technologies Co.,Ltd.
// 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.

package obs

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

func prepareHeaders(headers map[string][]string, meta bool, isObs bool) map[string][]string {
	_headers := make(map[string][]string, len(headers))
	if headers != nil {
		for key, value := range headers {
			key = strings.TrimSpace(key)
			if key == "" {
				continue
			}
			_key := strings.ToLower(key)
			if _, ok := allowedRequestHTTPHeaderMetadataNames[_key]; !ok && !strings.HasPrefix(key, HEADER_PREFIX) && !strings.HasPrefix(key, HEADER_PREFIX_OBS) {
				if !meta {
					continue
				}
				if !isObs {
					_key = HEADER_PREFIX_META + _key
				} else {
					_key = HEADER_PREFIX_META_OBS + _key
				}
			} else {
				_key = key
			}
			_headers[_key] = value
		}
	}
	return _headers
}

func (obsClient ObsClient) doActionWithoutBucket(action, method string, input ISerializable, output IBaseModel, extensions []extensionOptions) error {
	return obsClient.doAction(action, method, "", "", input, output, true, true, extensions)
}

func (obsClient ObsClient) doActionWithBucketV2(action, method, bucketName string, input ISerializable, output IBaseModel, extensions []extensionOptions) error {
	if strings.TrimSpace(bucketName) == "" && !obsClient.conf.cname {
		return errors.New("Bucket is empty")
	}
	return obsClient.doAction(action, method, bucketName, "", input, output, false, true, extensions)
}

func (obsClient ObsClient) doActionWithBucket(action, method, bucketName string, input ISerializable, output IBaseModel, extensions []extensionOptions) error {
	if strings.TrimSpace(bucketName) == "" && !obsClient.conf.cname {
		return errors.New("Bucket is empty")
	}
	return obsClient.doAction(action, method, bucketName, "", input, output, true, true, extensions)
}

func (obsClient ObsClient) doActionWithBucketAndKey(action, method, bucketName, objectKey string, input ISerializable, output IBaseModel, extensions []extensionOptions) error {
	return obsClient._doActionWithBucketAndKey(action, method, bucketName, objectKey, input, output, true, extensions)
}

func (obsClient ObsClient) doActionWithBucketAndKeyV2(action, method, bucketName, objectKey string, input ISerializable, output IBaseModel, extensions []extensionOptions) error {
	if strings.TrimSpace(bucketName) == "" && !obsClient.conf.cname {
		return errors.New("Bucket is empty")
	}
	if strings.TrimSpace(objectKey) == "" {
		return errors.New("Key is empty")
	}
	return obsClient.doAction(action, method, bucketName, objectKey, input, output, false, true, extensions)
}

func (obsClient ObsClient) doActionWithBucketAndKeyUnRepeatable(action, method, bucketName, objectKey string, input ISerializable, output IBaseModel, extensions []extensionOptions) error {
	return obsClient._doActionWithBucketAndKey(action, method, bucketName, objectKey, input, output, false, extensions)
}

func (obsClient ObsClient) _doActionWithBucketAndKey(action, method, bucketName, objectKey string, input ISerializable, output IBaseModel, repeatable bool, extensions []extensionOptions) error {
	if strings.TrimSpace(bucketName) == "" && !obsClient.conf.cname {
		return errors.New("Bucket is empty")
	}
	if strings.TrimSpace(objectKey) == "" {
		return errors.New("Key is empty")
	}
	return obsClient.doAction(action, method, bucketName, objectKey, input, output, true, repeatable, extensions)
}

func (obsClient ObsClient) doAction(action, method, bucketName, objectKey string, input ISerializable, output IBaseModel, xmlResult bool, repeatable bool, extensions []extensionOptions) error {

	var resp *http.Response
	var respError error
	doLog(LEVEL_INFO, "Enter method %s...", action)
	start := GetCurrentTimestamp()

	params, headers, data, err := input.trans(obsClient.conf.signature == SignatureObs)
	if err != nil {
		return err
	}

	if params == nil {
		params = make(map[string]string)
	}

	if headers == nil {
		headers = make(map[string][]string)
	}

	for _, extension := range extensions {
		if extensionHeader, ok := extension.(extensionHeaders); ok {
			_err := extensionHeader(headers, obsClient.conf.signature == SignatureObs)
			if _err != nil {
				doLog(LEVEL_INFO, fmt.Sprintf("set header with error: %v", _err))
			}
		} else {
			doLog(LEVEL_INFO, "Unsupported extensionOptions")
		}
	}

	switch method {
	case HTTP_GET:
		resp, respError = obsClient.doHTTPGet(bucketName, objectKey, params, headers, data, repeatable)
	case HTTP_POST:
		resp, respError = obsClient.doHTTPPost(bucketName, objectKey, params, headers, data, repeatable)
	case HTTP_PUT:
		resp, respError = obsClient.doHTTPPut(bucketName, objectKey, params, headers, data, repeatable)
	case HTTP_DELETE:
		resp, respError = obsClient.doHTTPDelete(bucketName, objectKey, params, headers, data, repeatable)
	case HTTP_HEAD:
		resp, respError = obsClient.doHTTPHead(bucketName, objectKey, params, headers, data, repeatable)
	case HTTP_OPTIONS:
		resp, respError = obsClient.doHTTPOptions(bucketName, objectKey, params, headers, data, repeatable)
	default:
		respError = errors.New("Unexpect http method error")
	}
	if respError == nil && output != nil {
		respError = ParseResponseToBaseModel(resp, output, xmlResult, obsClient.conf.signature == SignatureObs)
		if respError != nil {
			doLog(LEVEL_WARN, "Parse response to BaseModel with error: %v", respError)
		}
	} else {
		doLog(LEVEL_WARN, "Do http request with error: %v", respError)
	}

	if isDebugLogEnabled() {
		doLog(LEVEL_DEBUG, "End method %s, obsclient cost %d ms", action, (GetCurrentTimestamp() - start))
	}

	return respError
}

func (obsClient ObsClient) doHTTPGet(bucketName, objectKey string, params map[string]string,
	headers map[string][]string, data interface{}, repeatable bool) (*http.Response, error) {
	return obsClient.doHTTP(HTTP_GET, bucketName, objectKey, params, prepareHeaders(headers, false, obsClient.conf.signature == SignatureObs), data, repeatable)
}

func (obsClient ObsClient) doHTTPHead(bucketName, objectKey string, params map[string]string,
	headers map[string][]string, data interface{}, repeatable bool) (*http.Response, error) {
	return obsClient.doHTTP(HTTP_HEAD, bucketName, objectKey, params, prepareHeaders(headers, false, obsClient.conf.signature == SignatureObs), data, repeatable)
}

func (obsClient ObsClient) doHTTPOptions(bucketName, objectKey string, params map[string]string,
	headers map[string][]string, data interface{}, repeatable bool) (*http.Response, error) {
	return obsClient.doHTTP(HTTP_OPTIONS, bucketName, objectKey, params, prepareHeaders(headers, false, obsClient.conf.signature == SignatureObs), data, repeatable)
}

func (obsClient ObsClient) doHTTPDelete(bucketName, objectKey string, params map[string]string,
	headers map[string][]string, data interface{}, repeatable bool) (*http.Response, error) {
	return obsClient.doHTTP(HTTP_DELETE, bucketName, objectKey, params, prepareHeaders(headers, false, obsClient.conf.signature == SignatureObs), data, repeatable)
}

func (obsClient ObsClient) doHTTPPut(bucketName, objectKey string, params map[string]string,
	headers map[string][]string, data interface{}, repeatable bool) (*http.Response, error) {
	return obsClient.doHTTP(HTTP_PUT, bucketName, objectKey, params, prepareHeaders(headers, true, obsClient.conf.signature == SignatureObs), data, repeatable)
}

func (obsClient ObsClient) doHTTPPost(bucketName, objectKey string, params map[string]string,
	headers map[string][]string, data interface{}, repeatable bool) (*http.Response, error) {
	return obsClient.doHTTP(HTTP_POST, bucketName, objectKey, params, prepareHeaders(headers, true, obsClient.conf.signature == SignatureObs), data, repeatable)
}

func prepareAgentHeader(clientUserAgent string) string {
	userAgent := USER_AGENT
	if clientUserAgent != "" {
		userAgent = clientUserAgent
	}
	return userAgent
}

func (obsClient ObsClient) getSignedURLResponse(action string, output IBaseModel, xmlResult bool, resp *http.Response, err error, start int64) (respError error) {
	var msg interface{}
	if err != nil {
		respError = err
		resp = nil
	} else {
		doLog(LEVEL_DEBUG, "Response headers: %v", resp.Header)
		if resp.StatusCode >= 300 {
			respError = ParseResponseToObsError(resp, obsClient.conf.signature == SignatureObs)
			msg = resp.Status
			resp = nil
		} else {
			if output != nil {
				respError = ParseResponseToBaseModel(resp, output, xmlResult, obsClient.conf.signature == SignatureObs)
			}
			if respError != nil {
				doLog(LEVEL_WARN, "Parse response to BaseModel with error: %v", respError)
			}
		}
	}

	if msg != nil {
		doLog(LEVEL_ERROR, "Failed to send request with reason:%v", msg)
	}

	if isDebugLogEnabled() {
		doLog(LEVEL_DEBUG, "End method %s, obsclient cost %d ms", action, (GetCurrentTimestamp() - start))
	}
	return
}

func (obsClient ObsClient) doHTTPWithSignedURL(action, method string, signedURL string, actualSignedRequestHeaders http.Header, data io.Reader, output IBaseModel, xmlResult bool) (respError error) {
	req, err := http.NewRequest(method, signedURL, data)
	if err != nil {
		return err
	}
	if obsClient.conf.ctx != nil {
		req = req.WithContext(obsClient.conf.ctx)
	}
	var resp *http.Response

	var isSecurityToken bool
	var securityToken string
	var query []string
	parmas := strings.Split(signedURL, "?")
	if len(parmas) > 1 {
		query = strings.Split(parmas[1], "&")
		for _, value := range query {
			if strings.HasPrefix(value, HEADER_STS_TOKEN_AMZ+"=") || strings.HasPrefix(value, HEADER_STS_TOKEN_OBS+"=") {
				if value[len(HEADER_STS_TOKEN_AMZ)+1:] != "" {
					securityToken = value[len(HEADER_STS_TOKEN_AMZ)+1:]
					isSecurityToken = true
				}
			}
		}
	}
	logSignedURL := signedURL
	if isSecurityToken {
		logSignedURL = strings.Replace(logSignedURL, securityToken, "******", -1)
	}
	doLog(LEVEL_INFO, "Do %s with signedUrl %s...", action, logSignedURL)

	req.Header = actualSignedRequestHeaders
	if value, ok := req.Header[HEADER_HOST_CAMEL]; ok {
		req.Host = value[0]
		delete(req.Header, HEADER_HOST_CAMEL)
	} else if value, ok := req.Header[HEADER_HOST]; ok {
		req.Host = value[0]
		delete(req.Header, HEADER_HOST)
	}

	if value, ok := req.Header[HEADER_CONTENT_LENGTH_CAMEL]; ok {
		req.ContentLength = StringToInt64(value[0], -1)
		delete(req.Header, HEADER_CONTENT_LENGTH_CAMEL)
	} else if value, ok := req.Header[HEADER_CONTENT_LENGTH]; ok {
		req.ContentLength = StringToInt64(value[0], -1)
		delete(req.Header, HEADER_CONTENT_LENGTH)
	}

	userAgent := prepareAgentHeader(obsClient.conf.userAgent)
	req.Header[HEADER_USER_AGENT_CAMEL] = []string{userAgent}
	start := GetCurrentTimestamp()
	resp, err = obsClient.httpClient.Do(req)
	if isInfoLogEnabled() {
		doLog(LEVEL_INFO, "Do http request cost %d ms", (GetCurrentTimestamp() - start))
	}

	respError = obsClient.getSignedURLResponse(action, output, xmlResult, resp, err, start)

	return
}

func prepareData(headers map[string][]string, data interface{}) (io.Reader, error) {
	var _data io.Reader
	if data != nil {
		if dataStr, ok := data.(string); ok {
			doLog(LEVEL_DEBUG, "Do http request with string: %s", dataStr)
			headers["Content-Length"] = []string{IntToString(len(dataStr))}
			_data = strings.NewReader(dataStr)
		} else if dataByte, ok := data.([]byte); ok {
			doLog(LEVEL_DEBUG, "Do http request with byte array")
			headers["Content-Length"] = []string{IntToString(len(dataByte))}
			_data = bytes.NewReader(dataByte)
		} else if dataReader, ok := data.(io.Reader); ok {
			_data = dataReader
		} else {
			doLog(LEVEL_WARN, "Data is not a valid io.Reader")
			return nil, errors.New("Data is not a valid io.Reader")
		}
	}
	return _data, nil
}

func (obsClient ObsClient) getRequest(redirectURL, requestURL string, redirectFlag bool, _data io.Reader, method,
	bucketName, objectKey string, params map[string]string, headers map[string][]string) (*http.Request, error) {
	if redirectURL != "" {
		if !redirectFlag {
			parsedRedirectURL, err := url.Parse(redirectURL)
			if err != nil {
				return nil, err
			}
			requestURL, err = obsClient.doAuth(method, bucketName, objectKey, params, headers, parsedRedirectURL.Host)
			if err != nil {
				return nil, err
			}
			if parsedRequestURL, err := url.Parse(requestURL); err != nil {
				return nil, err
			} else if parsedRequestURL.RawQuery != "" && parsedRedirectURL.RawQuery == "" {
				redirectURL += "?" + parsedRequestURL.RawQuery
			}
		}
		requestURL = redirectURL
	} else {
		var err error
		requestURL, err = obsClient.doAuth(method, bucketName, objectKey, params, headers, "")
		if err != nil {
			return nil, err
		}
	}

	req, err := http.NewRequest(method, requestURL, _data)
	if obsClient.conf.ctx != nil {
		req = req.WithContext(obsClient.conf.ctx)
	}
	if err != nil {
		return nil, err
	}
	doLog(LEVEL_DEBUG, "Do request with url [%s] and method [%s]", requestURL, method)
	return req, nil
}

func logHeaders(headers map[string][]string, signature SignatureType) {
	if isDebugLogEnabled() {
		auth := headers[HEADER_AUTH_CAMEL]
		delete(headers, HEADER_AUTH_CAMEL)

		var isSecurityToken bool
		var securityToken []string
		if securityToken, isSecurityToken = headers[HEADER_STS_TOKEN_AMZ]; isSecurityToken {
			headers[HEADER_STS_TOKEN_AMZ] = []string{"******"}
		} else if securityToken, isSecurityToken = headers[HEADER_STS_TOKEN_OBS]; isSecurityToken {
			headers[HEADER_STS_TOKEN_OBS] = []string{"******"}
		}
		doLog(LEVEL_DEBUG, "Request headers: %v", headers)
		headers[HEADER_AUTH_CAMEL] = auth
		if isSecurityToken {
			if signature == SignatureObs {
				headers[HEADER_STS_TOKEN_OBS] = securityToken
			} else {
				headers[HEADER_STS_TOKEN_AMZ] = securityToken
			}
		}
	}
}

func prepareReq(headers map[string][]string, req, lastRequest *http.Request, clientUserAgent string) *http.Request {
	for key, value := range headers {
		if key == HEADER_HOST_CAMEL {
			req.Host = value[0]
			delete(headers, key)
		} else if key == HEADER_CONTENT_LENGTH_CAMEL {
			req.ContentLength = StringToInt64(value[0], -1)
			delete(headers, key)
		} else {
			req.Header[key] = value
		}
	}

	lastRequest = req

	userAgent := prepareAgentHeader(clientUserAgent)
	req.Header[HEADER_USER_AGENT_CAMEL] = []string{userAgent}

	if lastRequest != nil {
		req.Host = lastRequest.Host
		req.ContentLength = lastRequest.ContentLength
	}
	return lastRequest
}

func canNotRetry(repeatable bool, statusCode int) bool {
	if !repeatable || (statusCode >= 400 && statusCode < 500) || statusCode == 304 {
		return true
	}
	return false
}

func isRedirectErr(location string, redirectCount, maxRedirectCount int) bool {
	if location != "" && redirectCount < maxRedirectCount {
		return true
	}
	return false
}

func setRedirectFlag(statusCode int, method string) (redirectFlag bool) {
	if statusCode == 302 && method == HTTP_GET {
		redirectFlag = true
	} else {
		redirectFlag = false
	}
	return
}

func prepareRetry(resp *http.Response, headers map[string][]string, _data io.Reader, msg interface{}) (io.Reader, *http.Response, error) {
	if resp != nil {
		_err := resp.Body.Close()
		checkAndLogErr(_err, LEVEL_WARN, "Failed to close resp body")
		resp = nil
	}
	if _, ok := headers[HEADER_AUTH_CAMEL]; ok {
		delete(headers, HEADER_AUTH_CAMEL)
	}
	doLog(LEVEL_WARN, "Failed to send request with reason:%v, will try again", msg)
	if r, ok := _data.(*strings.Reader); ok {
		_, err := r.Seek(0, 0)
		if err != nil {
			return nil, nil, err
		}
	} else if r, ok := _data.(*bytes.Reader); ok {
		_, err := r.Seek(0, 0)
		if err != nil {
			return nil, nil, err
		}
	} else if r, ok := _data.(*fileReaderWrapper); ok {
		fd, err := os.Open(r.filePath)
		if err != nil {
			return nil, nil, err
		}
		fileReaderWrapper := &fileReaderWrapper{filePath: r.filePath}
		fileReaderWrapper.mark = r.mark
		fileReaderWrapper.reader = fd
		fileReaderWrapper.totalCount = r.totalCount
		_data = fileReaderWrapper
		_, err = fd.Seek(r.mark, 0)
		if err != nil {
			errMsg := fd.Close()
			checkAndLogErr(errMsg, LEVEL_WARN, "Failed to close with reason: %v", errMsg)
			return nil, nil, err
		}
	} else if r, ok := _data.(*readerWrapper); ok {
		_, err := r.seek(0, 0)
		if err != nil {
			return nil, nil, err
		}
	}
	return _data, resp, nil
}

func (obsClient ObsClient) doHTTP(method, bucketName, objectKey string, params map[string]string,
	headers map[string][]string, data interface{}, repeatable bool) (resp *http.Response, respError error) {

	bucketName = strings.TrimSpace(bucketName)

	method = strings.ToUpper(method)

	var redirectURL string
	var requestURL string
	maxRetryCount := obsClient.conf.maxRetryCount
	maxRedirectCount := obsClient.conf.maxRedirectCount

	_data, _err := prepareData(headers, data)
	if _err != nil {
		return nil, _err
	}

	var lastRequest *http.Request
	redirectFlag := false
	for i, redirectCount := 0, 0; i <= maxRetryCount; i++ {
		req, err := obsClient.getRequest(redirectURL, requestURL, redirectFlag, _data,
			method, bucketName, objectKey, params, headers)
		if err != nil {
			return nil, err
		}

		logHeaders(headers, obsClient.conf.signature)

		lastRequest = prepareReq(headers, req, lastRequest, obsClient.conf.userAgent)

		start := GetCurrentTimestamp()
		resp, err = obsClient.httpClient.Do(req)
		doLog(LEVEL_INFO, "Do http request cost %d ms", (GetCurrentTimestamp() - start))

		var msg interface{}
		if err != nil {
			msg = err
			respError = err
			resp = nil
			if !repeatable {
				break
			}
		} else {
			doLog(LEVEL_DEBUG, "Response headers: %v", resp.Header)
			if resp.StatusCode < 300 {
				respError = nil
				break
			} else if canNotRetry(repeatable, resp.StatusCode) {
				respError = ParseResponseToObsError(resp, obsClient.conf.signature == SignatureObs)
				resp = nil
				break
			} else if resp.StatusCode >= 300 && resp.StatusCode < 400 {
				location := resp.Header.Get(HEADER_LOCATION_CAMEL)
				if isRedirectErr(location, redirectCount, maxRedirectCount) {
					redirectURL = location
					doLog(LEVEL_WARN, "Redirect request to %s", redirectURL)
					msg = resp.Status
					maxRetryCount++
					redirectCount++
					redirectFlag = setRedirectFlag(resp.StatusCode, method)
				} else {
					respError = ParseResponseToObsError(resp, obsClient.conf.signature == SignatureObs)
					resp = nil
					break
				}
			} else {
				msg = resp.Status
			}
		}
		if i != maxRetryCount {
			_data, resp, err = prepareRetry(resp, headers, _data, msg)
			if err != nil {
				return nil, err
			}
			if r, ok := _data.(*fileReaderWrapper); ok {
				if _fd, _ok := r.reader.(*os.File); _ok {
					defer func() {
						errMsg := _fd.Close()
						checkAndLogErr(errMsg, LEVEL_WARN, "Failed to close with reason: %v", errMsg)
					}()
				}
			}
			time.Sleep(time.Duration(float64(i+2) * rand.Float64() * float64(time.Second)))
		} else {
			doLog(LEVEL_ERROR, "Failed to send request with reason:%v", msg)
			if resp != nil {
				respError = ParseResponseToObsError(resp, obsClient.conf.signature == SignatureObs)
				resp = nil
			}
		}
	}
	return
}

type connDelegate struct {
	conn          net.Conn
	socketTimeout time.Duration
	finalTimeout  time.Duration
}

func getConnDelegate(conn net.Conn, socketTimeout int, finalTimeout int) *connDelegate {
	return &connDelegate{
		conn:          conn,
		socketTimeout: time.Second * time.Duration(socketTimeout),
		finalTimeout:  time.Second * time.Duration(finalTimeout),
	}
}

func (delegate *connDelegate) Read(b []byte) (n int, err error) {
	setReadDeadlineErr := delegate.SetReadDeadline(time.Now().Add(delegate.socketTimeout))
	flag := isDebugLogEnabled()

	if setReadDeadlineErr != nil && flag {
		doLog(LEVEL_DEBUG, "Failed to set read deadline with reason: %v, but it's ok", setReadDeadlineErr)
	}

	n, err = delegate.conn.Read(b)
	setReadDeadlineErr = delegate.SetReadDeadline(time.Now().Add(delegate.finalTimeout))
	if setReadDeadlineErr != nil && flag {
		doLog(LEVEL_DEBUG, "Failed to set read deadline with reason: %v, but it's ok", setReadDeadlineErr)
	}
	return n, err
}

func (delegate *connDelegate) Write(b []byte) (n int, err error) {
	setWriteDeadlineErr := delegate.SetWriteDeadline(time.Now().Add(delegate.socketTimeout))
	flag := isDebugLogEnabled()
	if setWriteDeadlineErr != nil && flag {
		doLog(LEVEL_DEBUG, "Failed to set write deadline with reason: %v, but it's ok", setWriteDeadlineErr)
	}

	n, err = delegate.conn.Write(b)
	finalTimeout := time.Now().Add(delegate.finalTimeout)
	setWriteDeadlineErr = delegate.SetWriteDeadline(finalTimeout)
	if setWriteDeadlineErr != nil && flag {
		doLog(LEVEL_DEBUG, "Failed to set write deadline with reason: %v, but it's ok", setWriteDeadlineErr)
	}
	setReadDeadlineErr := delegate.SetReadDeadline(finalTimeout)
	if setReadDeadlineErr != nil && flag {
		doLog(LEVEL_DEBUG, "Failed to set read deadline with reason: %v, but it's ok", setReadDeadlineErr)
	}
	return n, err
}

func (delegate *connDelegate) Close() error {
	return delegate.conn.Close()
}

func (delegate *connDelegate) LocalAddr() net.Addr {
	return delegate.conn.LocalAddr()
}

func (delegate *connDelegate) RemoteAddr() net.Addr {
	return delegate.conn.RemoteAddr()
}

func (delegate *connDelegate) SetDeadline(t time.Time) error {
	return delegate.conn.SetDeadline(t)
}

func (delegate *connDelegate) SetReadDeadline(t time.Time) error {
	return delegate.conn.SetReadDeadline(t)
}

func (delegate *connDelegate) SetWriteDeadline(t time.Time) error {
	return delegate.conn.SetWriteDeadline(t)
}
