// 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 (
	"encoding/json"
	"io/ioutil"
	"math/rand"
	"net"
	"net/http"
	"os"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

const (
	accessKeyEnv     = "OBS_ACCESS_KEY_ID"
	securityKeyEnv   = "OBS_SECRET_ACCESS_KEY"
	securityTokenEnv = "OBS_SECURITY_TOKEN"
	ecsRequestURL    = "http://169.254.169.254/openstack/latest/securitykey"
)

type securityHolder struct {
	ak            string
	sk            string
	securityToken string
}

var emptySecurityHolder = securityHolder{}

type securityProvider interface {
	getSecurity() securityHolder
}

type BasicSecurityProvider struct {
	val atomic.Value
}

func (bsp *BasicSecurityProvider) getSecurity() securityHolder {
	if sh, ok := bsp.val.Load().(securityHolder); ok {
		return sh
	}
	return emptySecurityHolder
}

func (bsp *BasicSecurityProvider) refresh(ak, sk, securityToken string) {
	bsp.val.Store(securityHolder{ak: strings.TrimSpace(ak), sk: strings.TrimSpace(sk), securityToken: strings.TrimSpace(securityToken)})
}

func NewBasicSecurityProvider(ak, sk, securityToken string) *BasicSecurityProvider {
	bsp := &BasicSecurityProvider{}
	bsp.refresh(ak, sk, securityToken)
	return bsp
}

type EnvSecurityProvider struct {
	sh     securityHolder
	suffix string
	once   sync.Once
}

func (esp *EnvSecurityProvider) getSecurity() securityHolder {
	//ensure run only once
	esp.once.Do(func() {
		esp.sh = securityHolder{
			ak:            strings.TrimSpace(os.Getenv(accessKeyEnv + esp.suffix)),
			sk:            strings.TrimSpace(os.Getenv(securityKeyEnv + esp.suffix)),
			securityToken: strings.TrimSpace(os.Getenv(securityTokenEnv + esp.suffix)),
		}
	})

	return esp.sh
}

func NewEnvSecurityProvider(suffix string) *EnvSecurityProvider {
	if suffix != "" {
		suffix = "_" + suffix
	}
	esp := &EnvSecurityProvider{
		suffix: suffix,
	}
	return esp
}

type TemporarySecurityHolder struct {
	securityHolder
	expireDate time.Time
}

var emptyTemporarySecurityHolder = TemporarySecurityHolder{}

type EcsSecurityProvider struct {
	val        atomic.Value
	lock       sync.Mutex
	httpClient *http.Client
	prefetch   int32
	retryCount int
}

func (ecsSp *EcsSecurityProvider) loadTemporarySecurityHolder() (TemporarySecurityHolder, bool) {
	if sh := ecsSp.val.Load(); sh == nil {
		return emptyTemporarySecurityHolder, false
	} else if _sh, ok := sh.(TemporarySecurityHolder); !ok {
		return emptyTemporarySecurityHolder, false
	} else {
		return _sh, true
	}
}

func (ecsSp *EcsSecurityProvider) getAndSetSecurityWithOutLock() securityHolder {
	_sh := TemporarySecurityHolder{}
	_sh.expireDate = time.Now().Add(time.Minute * 5)
	retryCount := 0
	for {
		if req, err := http.NewRequest("GET", ecsRequestURL, nil); err == nil {
			start := GetCurrentTimestamp()
			res, err := ecsSp.httpClient.Do(req)
			if err == nil {
				if data, _err := ioutil.ReadAll(res.Body); _err == nil {
					temp := &struct {
						Credential struct {
							AK            string    `json:"access,omitempty"`
							SK            string    `json:"secret,omitempty"`
							SecurityToken string    `json:"securitytoken,omitempty"`
							ExpireDate    time.Time `json:"expires_at,omitempty"`
						} `json:"credential"`
					}{}

					doLog(LEVEL_DEBUG, "Get the json data from ecs succeed")

					if jsonErr := json.Unmarshal(data, temp); jsonErr == nil {
						_sh.ak = temp.Credential.AK
						_sh.sk = temp.Credential.SK
						_sh.securityToken = temp.Credential.SecurityToken
						_sh.expireDate = temp.Credential.ExpireDate.Add(time.Minute * -1)

						doLog(LEVEL_INFO, "Get security from ecs succeed, AK:xxxx, SK:xxxx, SecurityToken:xxxx, ExprireDate %s", _sh.expireDate)

						doLog(LEVEL_INFO, "Get security from ecs succeed, cost %d ms", (GetCurrentTimestamp() - start))
						break
					} else {
						err = jsonErr
					}
				} else {
					err = _err
				}
			}

			doLog(LEVEL_WARN, "Try to get security from ecs failed, cost %d ms, err %s", (GetCurrentTimestamp() - start), err.Error())
		}

		if retryCount >= ecsSp.retryCount {
			doLog(LEVEL_WARN, "Try to get security from ecs failed and exceed the max retry count")
			break
		}
		sleepTime := float64(retryCount+2) * rand.Float64()
		if sleepTime > 10 {
			sleepTime = 10
		}
		time.Sleep(time.Duration(sleepTime * float64(time.Second)))
		retryCount++
	}

	ecsSp.val.Store(_sh)
	return _sh.securityHolder
}

func (ecsSp *EcsSecurityProvider) getAndSetSecurity() securityHolder {
	ecsSp.lock.Lock()
	defer ecsSp.lock.Unlock()
	tsh, succeed := ecsSp.loadTemporarySecurityHolder()
	if !succeed || time.Now().After(tsh.expireDate) {
		return ecsSp.getAndSetSecurityWithOutLock()
	}
	return tsh.securityHolder
}

func (ecsSp *EcsSecurityProvider) getSecurity() securityHolder {
	if tsh, succeed := ecsSp.loadTemporarySecurityHolder(); succeed {
		if time.Now().Before(tsh.expireDate) {
			//not expire
			if time.Now().Add(time.Minute*5).After(tsh.expireDate) && atomic.CompareAndSwapInt32(&ecsSp.prefetch, 0, 1) {
				//do prefetch
				sh := ecsSp.getAndSetSecurityWithOutLock()
				atomic.CompareAndSwapInt32(&ecsSp.prefetch, 1, 0)
				return sh
			}
			return tsh.securityHolder
		}
		return ecsSp.getAndSetSecurity()
	}

	return ecsSp.getAndSetSecurity()
}

func getInternalTransport() *http.Transport {

	timeout := 10
	transport := &http.Transport{
		Dial: func(network, addr string) (net.Conn, error) {
			start := GetCurrentTimestamp()
			conn, err := (&net.Dialer{
				Timeout:  time.Second * time.Duration(timeout),
				Resolver: net.DefaultResolver,
			}).Dial(network, addr)

			if isInfoLogEnabled() {
				doLog(LEVEL_INFO, "Do http dial cost %d ms", (GetCurrentTimestamp() - start))
			}
			if err != nil {
				return nil, err
			}
			return getConnDelegate(conn, timeout, timeout*10), nil
		},
		MaxIdleConns:          10,
		MaxIdleConnsPerHost:   10,
		ResponseHeaderTimeout: time.Second * time.Duration(timeout),
		IdleConnTimeout:       time.Second * time.Duration(DEFAULT_IDLE_CONN_TIMEOUT),
		DisableCompression:    true,
	}

	return transport
}

func NewEcsSecurityProvider(retryCount int) *EcsSecurityProvider {
	ecsSp := &EcsSecurityProvider{
		retryCount: retryCount,
	}
	ecsSp.httpClient = &http.Client{Transport: getInternalTransport(), CheckRedirect: checkRedirectFunc}
	return ecsSp
}
