// 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 (
	"fmt"
	"net/url"
	"sort"
	"strings"
	"time"
)

func (obsClient ObsClient) doAuthTemporary(method, bucketName, objectKey string, params map[string]string,
	headers map[string][]string, expires int64) (requestURL string, err error) {
	isAkSkEmpty := obsClient.conf.securityProvider == nil || obsClient.conf.securityProvider.ak == "" || obsClient.conf.securityProvider.sk == ""
	if isAkSkEmpty == false && obsClient.conf.securityProvider.securityToken != "" {
		if obsClient.conf.signature == SignatureObs {
			params[HEADER_STS_TOKEN_OBS] = obsClient.conf.securityProvider.securityToken
		} else {
			params[HEADER_STS_TOKEN_AMZ] = obsClient.conf.securityProvider.securityToken
		}
	}
	requestURL, canonicalizedURL := obsClient.conf.formatUrls(bucketName, objectKey, params, true)
	parsedRequestURL, err := url.Parse(requestURL)
	if err != nil {
		return "", err
	}
	encodeHeaders(headers)
	hostName := parsedRequestURL.Host

	isV4 := obsClient.conf.signature == SignatureV4
	prepareHostAndDate(headers, hostName, isV4)

	if isAkSkEmpty {
		doLog(LEVEL_WARN, "No ak/sk provided, skip to construct authorization")
	} else {
		if isV4 {
			date, parseDateErr := time.Parse(RFC1123_FORMAT, headers[HEADER_DATE_CAMEL][0])
			if parseDateErr != nil {
				doLog(LEVEL_WARN, "Failed to parse date with reason: %v", parseDateErr)
				return "", parseDateErr
			}
			delete(headers, HEADER_DATE_CAMEL)
			shortDate := date.Format(SHORT_DATE_FORMAT)
			longDate := date.Format(LONG_DATE_FORMAT)
			if len(headers[HEADER_HOST_CAMEL]) != 0 {
				index := strings.LastIndex(headers[HEADER_HOST_CAMEL][0], ":")
				if index != -1 {
					port := headers[HEADER_HOST_CAMEL][0][index+1:]
					if port == "80" || port == "443" {
						headers[HEADER_HOST_CAMEL] = []string{headers[HEADER_HOST_CAMEL][0][:index]}
					}
				}

			}

			signedHeaders, _headers := getSignedHeaders(headers)

			credential, scope := getCredential(obsClient.conf.securityProvider.ak, obsClient.conf.region, shortDate)
			params[PARAM_ALGORITHM_AMZ_CAMEL] = V4_HASH_PREFIX
			params[PARAM_CREDENTIAL_AMZ_CAMEL] = credential
			params[PARAM_DATE_AMZ_CAMEL] = longDate
			params[PARAM_EXPIRES_AMZ_CAMEL] = Int64ToString(expires)
			params[PARAM_SIGNEDHEADERS_AMZ_CAMEL] = strings.Join(signedHeaders, ";")

			requestURL, canonicalizedURL = obsClient.conf.formatUrls(bucketName, objectKey, params, true)
			parsedRequestURL, _err := url.Parse(requestURL)
			if _err != nil {
				return "", _err
			}

			stringToSign := getV4StringToSign(method, canonicalizedURL, parsedRequestURL.RawQuery, scope, longDate, UNSIGNED_PAYLOAD, signedHeaders, _headers)
			signature := getSignature(stringToSign, obsClient.conf.securityProvider.sk, obsClient.conf.region, shortDate)

			requestURL += fmt.Sprintf("&%s=%s", PARAM_SIGNATURE_AMZ_CAMEL, UrlEncode(signature, false))

		} else {
			originDate := headers[HEADER_DATE_CAMEL][0]
			date, parseDateErr := time.Parse(RFC1123_FORMAT, originDate)
			if parseDateErr != nil {
				doLog(LEVEL_WARN, "Failed to parse date with reason: %v", parseDateErr)
				return "", parseDateErr
			}
			expires += date.Unix()
			headers[HEADER_DATE_CAMEL] = []string{Int64ToString(expires)}

			stringToSign := getV2StringToSign(method, canonicalizedURL, headers, obsClient.conf.signature == SignatureObs)
			signature := UrlEncode(Base64Encode(HmacSha1([]byte(obsClient.conf.securityProvider.sk), []byte(stringToSign))), false)
			if strings.Index(requestURL, "?") < 0 {
				requestURL += "?"
			} else {
				requestURL += "&"
			}
			delete(headers, HEADER_DATE_CAMEL)

			if obsClient.conf.signature != SignatureObs {
				requestURL += "AWS"
			}
			requestURL += fmt.Sprintf("AccessKeyId=%s&Expires=%d&Signature=%s", UrlEncode(obsClient.conf.securityProvider.ak, false), expires, signature)
		}
	}

	return
}

func (obsClient ObsClient) doAuth(method, bucketName, objectKey string, params map[string]string,
	headers map[string][]string, hostName string) (requestURL string, err error) {
	isAkSkEmpty := obsClient.conf.securityProvider == nil || obsClient.conf.securityProvider.ak == "" || obsClient.conf.securityProvider.sk == ""
	if isAkSkEmpty == false && obsClient.conf.securityProvider.securityToken != "" {
		if obsClient.conf.signature == SignatureObs {
			headers[HEADER_STS_TOKEN_OBS] = []string{obsClient.conf.securityProvider.securityToken}
		} else {
			headers[HEADER_STS_TOKEN_AMZ] = []string{obsClient.conf.securityProvider.securityToken}
		}
	}
	isObs := obsClient.conf.signature == SignatureObs
	requestURL, canonicalizedURL := obsClient.conf.formatUrls(bucketName, objectKey, params, true)
	parsedRequestURL, err := url.Parse(requestURL)
	if err != nil {
		return "", err
	}
	encodeHeaders(headers)

	if hostName == "" {
		hostName = parsedRequestURL.Host
	}

	isV4 := obsClient.conf.signature == SignatureV4
	prepareHostAndDate(headers, hostName, isV4)

	if isAkSkEmpty {
		doLog(LEVEL_WARN, "No ak/sk provided, skip to construct authorization")
	} else {
		ak := obsClient.conf.securityProvider.ak
		sk := obsClient.conf.securityProvider.sk
		var authorization string
		if isV4 {
			headers[HEADER_CONTENT_SHA256_AMZ] = []string{UNSIGNED_PAYLOAD}
			ret := v4Auth(ak, sk, obsClient.conf.region, method, canonicalizedURL, parsedRequestURL.RawQuery, headers)
			authorization = fmt.Sprintf("%s Credential=%s,SignedHeaders=%s,Signature=%s", V4_HASH_PREFIX, ret["Credential"], ret["SignedHeaders"], ret["Signature"])
		} else {
			ret := v2Auth(ak, sk, method, canonicalizedURL, headers, isObs)
			hashPrefix := V2_HASH_PREFIX
			if isObs {
				hashPrefix = OBS_HASH_PREFIX
			}
			authorization = fmt.Sprintf("%s %s:%s", hashPrefix, ak, ret["Signature"])
		}
		headers[HEADER_AUTH_CAMEL] = []string{authorization}
	}
	return
}

func prepareHostAndDate(headers map[string][]string, hostName string, isV4 bool) {
	headers[HEADER_HOST_CAMEL] = []string{hostName}
	if date, ok := headers[HEADER_DATE_AMZ]; ok {
		flag := false
		if len(date) == 1 {
			if isV4 {
				if t, err := time.Parse(LONG_DATE_FORMAT, date[0]); err == nil {
					headers[HEADER_DATE_CAMEL] = []string{FormatUtcToRfc1123(t)}
					flag = true
				}
			} else {
				if strings.HasSuffix(date[0], "GMT") {
					headers[HEADER_DATE_CAMEL] = []string{date[0]}
					flag = true
				}
			}
		}
		if !flag {
			delete(headers, HEADER_DATE_AMZ)
		}
	}
	if _, ok := headers[HEADER_DATE_CAMEL]; !ok {
		headers[HEADER_DATE_CAMEL] = []string{FormatUtcToRfc1123(time.Now().UTC())}
	}
}

func encodeHeaders(headers map[string][]string) {
	for key, values := range headers {
		for index, value := range values {
			values[index] = UrlEncode(value, true)
		}
		headers[key] = values
	}
}

func attachHeaders(headers map[string][]string, isObs bool) string {
	length := len(headers)
	_headers := make(map[string][]string, length)
	keys := make([]string, 0, length)

	for key, value := range headers {
		_key := strings.ToLower(strings.TrimSpace(key))
		if _key != "" {
			prefixheader := HEADER_PREFIX
			if isObs {
				prefixheader = HEADER_PREFIX_OBS
			}
			if _key == "content-md5" || _key == "content-type" || _key == "date" || strings.HasPrefix(_key, prefixheader) {
				keys = append(keys, _key)
				_headers[_key] = value
			}
		} else {
			delete(headers, key)
		}
	}

	for _, interestedHeader := range interestedHeaders {
		if _, ok := _headers[interestedHeader]; !ok {
			_headers[interestedHeader] = []string{""}
			keys = append(keys, interestedHeader)
		}
	}
	dateCamelHeader := PARAM_DATE_AMZ_CAMEL
	dataHeader := HEADER_DATE_AMZ
	if isObs {
		dateCamelHeader = PARAM_DATE_OBS_CAMEL
		dataHeader = HEADER_DATE_OBS
	}
	if _, ok := _headers[HEADER_DATE_CAMEL]; ok {
		if _, ok := _headers[dataHeader]; ok {
			_headers[HEADER_DATE_CAMEL] = []string{""}
		} else if _, ok := headers[dateCamelHeader]; ok {
			_headers[HEADER_DATE_CAMEL] = []string{""}
		}
	} else if _, ok := _headers[strings.ToLower(HEADER_DATE_CAMEL)]; ok {
		if _, ok := _headers[dataHeader]; ok {
			_headers[HEADER_DATE_CAMEL] = []string{""}
		} else if _, ok := headers[dateCamelHeader]; ok {
			_headers[HEADER_DATE_CAMEL] = []string{""}
		}
	}

	sort.Strings(keys)

	stringToSign := make([]string, 0, len(keys))
	for _, key := range keys {
		var value string
		prefixHeader := HEADER_PREFIX
		prefixMetaHeader := HEADER_PREFIX_META
		if isObs {
			prefixHeader = HEADER_PREFIX_OBS
			prefixMetaHeader = HEADER_PREFIX_META_OBS
		}
		if strings.HasPrefix(key, prefixHeader) {
			if strings.HasPrefix(key, prefixMetaHeader) {
				for index, v := range _headers[key] {
					value += strings.TrimSpace(v)
					if index != len(_headers[key])-1 {
						value += ","
					}
				}
			} else {
				value = strings.Join(_headers[key], ",")
			}
			value = fmt.Sprintf("%s:%s", key, value)
		} else {
			value = strings.Join(_headers[key], ",")
		}
		stringToSign = append(stringToSign, value)
	}
	return strings.Join(stringToSign, "\n")
}

func getV2StringToSign(method, canonicalizedURL string, headers map[string][]string, isObs bool) string {
	stringToSign := strings.Join([]string{method, "\n", attachHeaders(headers, isObs), "\n", canonicalizedURL}, "")

	var isSecurityToken bool
	var securityToken []string
	if isObs {
		securityToken, isSecurityToken = headers[HEADER_STS_TOKEN_OBS]
	} else {
		securityToken, isSecurityToken = headers[HEADER_STS_TOKEN_AMZ]
	}
	var query []string
	if !isSecurityToken {
		parmas := strings.Split(canonicalizedURL, "?")
		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 = []string{value[len(HEADER_STS_TOKEN_AMZ)+1:]}
						isSecurityToken = true
					}
				}
			}
		}
	}
	logStringToSign := stringToSign
	if isSecurityToken && len(securityToken) > 0 {
		logStringToSign = strings.Replace(logStringToSign, securityToken[0], "******", -1)
	}
	doLog(LEVEL_DEBUG, "The v2 auth stringToSign:\n%s", logStringToSign)
	return stringToSign
}

func v2Auth(ak, sk, method, canonicalizedURL string, headers map[string][]string, isObs bool) map[string]string {
	stringToSign := getV2StringToSign(method, canonicalizedURL, headers, isObs)
	return map[string]string{"Signature": Base64Encode(HmacSha1([]byte(sk), []byte(stringToSign)))}
}

func getScope(region, shortDate string) string {
	return fmt.Sprintf("%s/%s/%s/%s", shortDate, region, V4_SERVICE_NAME, V4_SERVICE_SUFFIX)
}

func getCredential(ak, region, shortDate string) (string, string) {
	scope := getScope(region, shortDate)
	return fmt.Sprintf("%s/%s", ak, scope), scope
}

func getV4StringToSign(method, canonicalizedURL, queryURL, scope, longDate, payload string, signedHeaders []string, headers map[string][]string) string {
	canonicalRequest := make([]string, 0, 10+len(signedHeaders)*4)
	canonicalRequest = append(canonicalRequest, method)
	canonicalRequest = append(canonicalRequest, "\n")
	canonicalRequest = append(canonicalRequest, canonicalizedURL)
	canonicalRequest = append(canonicalRequest, "\n")
	canonicalRequest = append(canonicalRequest, queryURL)
	canonicalRequest = append(canonicalRequest, "\n")

	for _, signedHeader := range signedHeaders {
		values, _ := headers[signedHeader]
		for _, value := range values {
			canonicalRequest = append(canonicalRequest, signedHeader)
			canonicalRequest = append(canonicalRequest, ":")
			canonicalRequest = append(canonicalRequest, value)
			canonicalRequest = append(canonicalRequest, "\n")
		}
	}
	canonicalRequest = append(canonicalRequest, "\n")
	canonicalRequest = append(canonicalRequest, strings.Join(signedHeaders, ";"))
	canonicalRequest = append(canonicalRequest, "\n")
	canonicalRequest = append(canonicalRequest, payload)

	_canonicalRequest := strings.Join(canonicalRequest, "")

	var isSecurityToken bool
	var securityToken []string
	if securityToken, isSecurityToken = headers[HEADER_STS_TOKEN_OBS]; !isSecurityToken {
		securityToken, isSecurityToken = headers[HEADER_STS_TOKEN_AMZ]
	}
	var query []string
	if !isSecurityToken {
		query = strings.Split(queryURL, "&")
		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 = []string{value[len(HEADER_STS_TOKEN_AMZ)+1:]}
					isSecurityToken = true
				}
			}
		}
	}
	logCanonicalRequest := _canonicalRequest
	if isSecurityToken && len(securityToken) > 0 {
		logCanonicalRequest = strings.Replace(logCanonicalRequest, securityToken[0], "******", -1)
	}
	doLog(LEVEL_DEBUG, "The v4 auth canonicalRequest:\n%s", logCanonicalRequest)

	stringToSign := make([]string, 0, 7)
	stringToSign = append(stringToSign, V4_HASH_PREFIX)
	stringToSign = append(stringToSign, "\n")
	stringToSign = append(stringToSign, longDate)
	stringToSign = append(stringToSign, "\n")
	stringToSign = append(stringToSign, scope)
	stringToSign = append(stringToSign, "\n")
	stringToSign = append(stringToSign, HexSha256([]byte(_canonicalRequest)))

	_stringToSign := strings.Join(stringToSign, "")

	doLog(LEVEL_DEBUG, "The v4 auth stringToSign:\n%s", _stringToSign)
	return _stringToSign
}

func getSignedHeaders(headers map[string][]string) ([]string, map[string][]string) {
	length := len(headers)
	_headers := make(map[string][]string, length)
	signedHeaders := make([]string, 0, length)
	for key, value := range headers {
		_key := strings.ToLower(strings.TrimSpace(key))
		if _key != "" {
			signedHeaders = append(signedHeaders, _key)
			_headers[_key] = value
		} else {
			delete(headers, key)
		}
	}
	sort.Strings(signedHeaders)
	return signedHeaders, _headers
}

func getSignature(stringToSign, sk, region, shortDate string) string {
	key := HmacSha256([]byte(V4_HASH_PRE+sk), []byte(shortDate))
	key = HmacSha256(key, []byte(region))
	key = HmacSha256(key, []byte(V4_SERVICE_NAME))
	key = HmacSha256(key, []byte(V4_SERVICE_SUFFIX))
	return Hex(HmacSha256(key, []byte(stringToSign)))
}

// V4Auth is a wrapper for v4Auth
func V4Auth(ak, sk, region, method, canonicalizedURL, queryURL string, headers map[string][]string) map[string]string {
	return v4Auth(ak, sk, region, method, canonicalizedURL, queryURL, headers)
}

func v4Auth(ak, sk, region, method, canonicalizedURL, queryURL string, headers map[string][]string) map[string]string {
	var t time.Time
	if val, ok := headers[HEADER_DATE_AMZ]; ok {
		var err error
		t, err = time.Parse(LONG_DATE_FORMAT, val[0])
		if err != nil {
			t = time.Now().UTC()
		}
	} else if val, ok := headers[PARAM_DATE_AMZ_CAMEL]; ok {
		var err error
		t, err = time.Parse(LONG_DATE_FORMAT, val[0])
		if err != nil {
			t = time.Now().UTC()
		}
	} else if val, ok := headers[HEADER_DATE_CAMEL]; ok {
		var err error
		t, err = time.Parse(RFC1123_FORMAT, val[0])
		if err != nil {
			t = time.Now().UTC()
		}
	} else if val, ok := headers[strings.ToLower(HEADER_DATE_CAMEL)]; ok {
		var err error
		t, err = time.Parse(RFC1123_FORMAT, val[0])
		if err != nil {
			t = time.Now().UTC()
		}
	} else {
		t = time.Now().UTC()
	}
	shortDate := t.Format(SHORT_DATE_FORMAT)
	longDate := t.Format(LONG_DATE_FORMAT)

	signedHeaders, _headers := getSignedHeaders(headers)

	credential, scope := getCredential(ak, region, shortDate)

	payload := UNSIGNED_PAYLOAD
	if val, ok := headers[HEADER_CONTENT_SHA256_AMZ]; ok {
		payload = val[0]
	}
	stringToSign := getV4StringToSign(method, canonicalizedURL, queryURL, scope, longDate, payload, signedHeaders, _headers)

	signature := getSignature(stringToSign, sk, region, shortDate)

	ret := make(map[string]string, 3)
	ret["Credential"] = credential
	ret["SignedHeaders"] = strings.Join(signedHeaders, ";")
	ret["Signature"] = signature
	return ret
}
