// Copyright (c) 2009-present, Alibaba Cloud All rights reserved.
//
// 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 config

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/aliyun/aliyun_assist_client/agent/session/plugin/cli"

	"github.com/aliyun/alibaba-cloud-sdk-go/sdk"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses"
	jmespath "github.com/jmespath/go-jmespath"
)

type AuthenticateMode string

const (
	AK                = AuthenticateMode("AK")
	StsToken          = AuthenticateMode("StsToken")
	RamRoleArn        = AuthenticateMode("RamRoleArn")
	EcsRamRole        = AuthenticateMode("EcsRamRole")
	RsaKeyPair        = AuthenticateMode("RsaKeyPair")
	RamRoleArnWithEcs = AuthenticateMode("RamRoleArnWithRoleName")
	External          = AuthenticateMode("External")
	CredentialsURI    = AuthenticateMode("CredentialsURI")
)

type Profile struct {
	Name            string           `json:"name"`
	Mode            AuthenticateMode `json:"mode"`
	AccessKeyId     string           `json:"access_key_id"`
	AccessKeySecret string           `json:"access_key_secret"`
	StsToken        string           `json:"sts_token"`
	StsRegion       string           `json:"sts_region"`
	RamRoleName     string           `json:"ram_role_name"`
	RamRoleArn      string           `json:"ram_role_arn"`
	RoleSessionName string           `json:"ram_session_name"`
	PrivateKey      string           `json:"private_key"`
	KeyPairName     string           `json:"key_pair_name"`
	ExpiredSeconds  int              `json:"expired_seconds"`
	Verified        string           `json:"verified"`
	RegionId        string           `json:"region_id"`
	OutputFormat    string           `json:"output_format"`
	Language        string           `json:"language"`
	Site            string           `json:"site"`
	ReadTimeout     int              `json:"retry_timeout"`
	ConnectTimeout  int              `json:"connect_timeout"`
	RetryCount      int              `json:"retry_count"`
	ProcessCommand  string           `json:"process_command"`
	CredentialsURI  string           `json:"credentials_uri"`
	parent          *Configuration   //`json:"-"`
}

func NewProfile(name string) Profile {
	return Profile{
		Name:         name,
		Mode:         AK,
		OutputFormat: "json",
		Language:     "en",
	}
}

func (cp *Profile) Validate() error {

	if cp.RegionId == "" {
		return fmt.Errorf("region can't be empty")
	}

	if !IsRegion(cp.RegionId) {
		return fmt.Errorf("invalid region %s", cp.RegionId)
	}

	if cp.Mode == "" {
		return fmt.Errorf("profile %s is not configure yet, run `aliyun configure --profile %s` first", cp.Name, cp.Name)
	}

	switch cp.Mode {
	case AK:
		return cp.ValidateAK()
	case StsToken:
		err := cp.ValidateAK()
		if err != nil {
			return err
		}
		if cp.StsToken == "" {
			return fmt.Errorf("invalid sts_token")
		}
	case RamRoleArn:
		err := cp.ValidateAK()
		if err != nil {
			return err
		}
		if cp.RamRoleArn == "" {
			return fmt.Errorf("invalid ram_role_arn")
		}
		if cp.RoleSessionName == "" {
			return fmt.Errorf("invalid role_session_name")
		}
	case EcsRamRole, RamRoleArnWithEcs:
	case RsaKeyPair:
		if cp.PrivateKey == "" {
			return fmt.Errorf("invalid private_key")
		}
		if cp.KeyPairName == "" {
			return fmt.Errorf("invalid key_pair_name")
		}
	case External:
		if cp.ProcessCommand == "" {
			return fmt.Errorf("invalid process_command")
		}
	case CredentialsURI:
		if cp.CredentialsURI == "" {
			return fmt.Errorf("invalid credentials_uri")
		}

	default:
		return fmt.Errorf("invalid mode: %s", cp.Mode)
	}
	return nil
}

func (cp *Profile) GetParent() *Configuration {
	return cp.parent
}

func (cp *Profile) OverwriteWithFlags(ctx *cli.Context) {
	cp.Mode = AuthenticateMode(ModeFlag(ctx.Flags()).GetStringOrDefault(string(cp.Mode)))
	cp.AccessKeyId = AccessKeyIdFlag(ctx.Flags()).GetStringOrDefault(cp.AccessKeyId)
	cp.AccessKeySecret = AccessKeySecretFlag(ctx.Flags()).GetStringOrDefault(cp.AccessKeySecret)
	cp.StsToken = StsTokenFlag(ctx.Flags()).GetStringOrDefault(cp.StsToken)
	cp.StsRegion = StsRegionFlag(ctx.Flags()).GetStringOrDefault(cp.StsRegion)
	cp.RamRoleName = RamRoleNameFlag(ctx.Flags()).GetStringOrDefault(cp.RamRoleName)
	cp.RamRoleArn = RamRoleArnFlag(ctx.Flags()).GetStringOrDefault(cp.RamRoleArn)
	cp.RoleSessionName = RoleSessionNameFlag(ctx.Flags()).GetStringOrDefault(cp.RoleSessionName)
	cp.KeyPairName = KeyPairNameFlag(ctx.Flags()).GetStringOrDefault(cp.KeyPairName)
	cp.PrivateKey = PrivateKeyFlag(ctx.Flags()).GetStringOrDefault(cp.PrivateKey)
	cp.RegionId = RegionFlag(ctx.Flags()).GetStringOrDefault(cp.RegionId)
	cp.Language = LanguageFlag(ctx.Flags()).GetStringOrDefault(cp.Language)
	cp.ReadTimeout = ReadTimeoutFlag(ctx.Flags()).GetIntegerOrDefault(cp.ReadTimeout)
	cp.ConnectTimeout = ConnectTimeoutFlag(ctx.Flags()).GetIntegerOrDefault(cp.ConnectTimeout)
	cp.RetryCount = RetryCountFlag(ctx.Flags()).GetIntegerOrDefault(cp.RetryCount)
	cp.ExpiredSeconds = ExpiredSecondsFlag(ctx.Flags()).GetIntegerOrDefault(cp.ExpiredSeconds)
	cp.ProcessCommand = ProcessCommandFlag(ctx.Flags()).GetStringOrDefault(cp.ProcessCommand)

	if cp.AccessKeyId == "" {
		switch {
		case os.Getenv("ALIBABACLOUD_ACCESS_KEY_ID") != "":
			cp.AccessKeyId = os.Getenv("ALIBABACLOUD_ACCESS_KEY_ID")
		case os.Getenv("ALICLOUD_ACCESS_KEY_ID") != "":
			cp.AccessKeyId = os.Getenv("ALICLOUD_ACCESS_KEY_ID")
		case os.Getenv("ACCESS_KEY_ID") != "":
			cp.AccessKeyId = os.Getenv("ACCESS_KEY_ID")
		}
	}

	if cp.AccessKeySecret == "" {
		switch {
		case os.Getenv("ALIBABACLOUD_ACCESS_KEY_SECRET") != "":
			cp.AccessKeySecret = os.Getenv("ALIBABACLOUD_ACCESS_KEY_SECRET")
		case os.Getenv("ALICLOUD_ACCESS_KEY_SECRET") != "":
			cp.AccessKeySecret = os.Getenv("ALICLOUD_ACCESS_KEY_SECRET")
		case os.Getenv("ACCESS_KEY_SECRET") != "":
			cp.AccessKeySecret = os.Getenv("ACCESS_KEY_SECRET")
		}
	}

	if cp.StsToken == "" {
		cp.StsToken = os.Getenv("SECURITY_TOKEN")
	}

	if cp.CredentialsURI == "" {
		cp.CredentialsURI = os.Getenv("ALIBABA_CLOUD_CREDENTIALS_URI")
	}

	if cp.RegionId == "" {
		switch {
		case os.Getenv("ALIBABACLOUD_REGION_ID") != "":
			cp.RegionId = os.Getenv("ALIBABACLOUD_REGION_ID")
		case os.Getenv("ALICLOUD_REGION_ID") != "":
			cp.RegionId = os.Getenv("ALICLOUD_REGION_ID")
		case os.Getenv("REGION") != "":
			cp.RegionId = os.Getenv("REGION")
		}
	}
	AutoModeRecognition(cp)
}

func AutoModeRecognition(cp *Profile) {
	if cp.Mode != AuthenticateMode("") {
		return
	}
	if cp.AccessKeyId != "" && cp.AccessKeySecret != "" {
		cp.Mode = AK
		if cp.StsToken != "" {
			cp.Mode = StsToken
		} else if cp.RamRoleArn != "" {
			cp.Mode = RamRoleArn
		}
	} else if cp.PrivateKey != "" && cp.KeyPairName != "" {
		cp.Mode = RsaKeyPair
	} else if cp.RamRoleName != "" {
		cp.Mode = EcsRamRole
	} else if cp.ProcessCommand != "" {
		cp.Mode = External
	}
}

func (cp *Profile) ValidateAK() error {
	if len(cp.AccessKeyId) == 0 {
		return fmt.Errorf("invalid access_key_id: %s", cp.AccessKeyId)
	}
	if len(cp.AccessKeySecret) == 0 {
		return fmt.Errorf("invalid access_key_secret: %s", cp.AccessKeySecret)
	}
	return nil
}

func (cp *Profile) GetClient(ctx *cli.Context) (*sdk.Client, error) {
	config := sdk.NewConfig()
	// get UserAgent from env
	config.UserAgent = os.Getenv("ALIYUN_USER_AGENT")

	if cp.RetryCount > 0 {
		// when use --retry-count, enable auto retry
		config.WithAutoRetry(true)
		config.WithMaxRetryTime(cp.RetryCount)
	}
	var client *sdk.Client
	var err error
	switch cp.Mode {
	case AK:
		client, err = cp.GetClientByAK(config)
	case StsToken:
		client, err = cp.GetClientBySts(config)
	case RamRoleArn:
		client, err = cp.GetClientByRoleArn(config)
	case EcsRamRole:
		client, err = cp.GetClientByEcsRamRole(config)
	case RsaKeyPair:
		client, err = cp.GetClientByPrivateKey(config)
	case RamRoleArnWithEcs:
		client, err = cp.GetClientByRamRoleArnWithEcs(config)
	case External:
		return cp.GetClientByExternal(config, ctx)
	case CredentialsURI:
		return cp.GetClientByCredentialsURI(config, ctx)
	default:
		client, err = nil, fmt.Errorf("unexcepted certificate mode: %s", cp.Mode)
	}

	if client != nil {
		if cp.ReadTimeout > 0 {
			client.SetReadTimeout(time.Duration(cp.ReadTimeout) * time.Second)
		}
		if cp.ConnectTimeout > 0 {
			client.SetConnectTimeout(time.Duration(cp.ConnectTimeout) * time.Second)
		}
		if SkipSecureVerify(ctx.Flags()).IsAssigned() {
			client.SetHTTPSInsecure(true)
		}
	}
	return client, err
}

func (cp *Profile) GetClientByAK(config *sdk.Config) (*sdk.Client, error) {
	if cp.AccessKeyId == "" || cp.AccessKeySecret == "" {
		return nil, fmt.Errorf("AccessKeyId/AccessKeySecret is empty! run `aliyun configure` first")
	}
	if cp.RegionId == "" {
		return nil, fmt.Errorf("default RegionId is empty! run `aliyun configure` first")
	}
	cred := credentials.NewAccessKeyCredential(cp.AccessKeyId, cp.AccessKeySecret)
	client, err := sdk.NewClientWithOptions(cp.RegionId, config, cred)
	return client, err
}

func (cp *Profile) GetClientBySts(config *sdk.Config) (*sdk.Client, error) {
	cred := credentials.NewStsTokenCredential(cp.AccessKeyId, cp.AccessKeySecret, cp.StsToken)
	client, err := sdk.NewClientWithOptions(cp.RegionId, config, cred)
	return client, err
}

func (cp *Profile) GetClientByRoleArn(config *sdk.Config) (*sdk.Client, error) {
	cred := credentials.NewRamRoleArnCredential(cp.AccessKeyId, cp.AccessKeySecret, cp.RamRoleArn, cp.RoleSessionName, cp.ExpiredSeconds)
	cred.StsRegion = cp.StsRegion
	client, err := sdk.NewClientWithOptions(cp.RegionId, config, cred)
	return client, err
}

func (cp *Profile) GetClientByRamRoleArnWithEcs(config *sdk.Config) (*sdk.Client, error) {
	client, err := cp.GetClientByEcsRamRole(config)
	if err != nil {
		return nil, err
	}
	accessKeyID, accessKeySecret, StsToken, err := cp.GetSessionCredential(client)
	if err != nil {
		return nil, err
	}
	cred := credentials.NewStsTokenCredential(accessKeyID, accessKeySecret, StsToken)
	return sdk.NewClientWithOptions(cp.RegionId, config, cred)
}

func (cp *Profile) GetSessionCredential(client *sdk.Client) (string, string, string, error) {
	req := requests.NewCommonRequest()
	rep := responses.NewCommonResponse()
	req.Scheme = "HTTPS"
	req.Product = "Sts"
	req.RegionId = cp.RegionId
	req.Version = "2015-04-01"
	if cp.StsRegion != "" {
		req.Domain = fmt.Sprintf("sts.%s.aliyuncs.com", cp.StsRegion)
	} else {
		req.Domain = "sts.aliyuncs.com"
	}
	req.ApiName = "AssumeRole"
	req.QueryParams["RoleArn"] = cp.RamRoleArn
	req.QueryParams["RoleSessionName"] = cp.RoleSessionName
	req.QueryParams["DurationSeconds"] = strconv.Itoa(cp.ExpiredSeconds)
	req.TransToAcsRequest()
	err := client.DoAction(req, rep)
	if err != nil {
		return "", "", "", err
	}
	var v interface{}
	err = json.Unmarshal(rep.GetHttpContentBytes(), &v)
	if err != nil {
		return "", "", "", err
	}
	accessKeyID, _ := jmespath.Search("Credentials.AccessKeyId", v)
	accessKeySecret, _ := jmespath.Search("Credentials.AccessKeySecret", v)
	StsToken, _ := jmespath.Search("Credentials.SecurityToken", v)
	if accessKeyID == nil || accessKeySecret == nil || StsToken == nil {
		return "", "", "", errors.New("get session credential failed")
	}
	return accessKeyID.(string), accessKeySecret.(string), StsToken.(string), nil
}

func (cp *Profile) GetClientByEcsRamRole(config *sdk.Config) (*sdk.Client, error) {
	cred := credentials.NewEcsRamRoleCredential(cp.RamRoleName)
	client, err := sdk.NewClientWithOptions(cp.RegionId, config, cred)
	return client, err
}

func (cp *Profile) GetClientByPrivateKey(config *sdk.Config) (*sdk.Client, error) {
	cred := credentials.NewRsaKeyPairCredential(cp.PrivateKey, cp.KeyPairName, cp.ExpiredSeconds)
	client, err := sdk.NewClientWithOptions(cp.RegionId, config, cred)
	return client, err
}

func (cp *Profile) GetClientByExternal(config *sdk.Config, ctx *cli.Context) (*sdk.Client, error) {
	args := strings.Fields(cp.ProcessCommand)

	cmd := exec.Command(args[0], args[1:]...)
	if errors.Is(cmd.Err, exec.ErrDot) {
		cmd.Err = nil
	}
	buf, err := cmd.CombinedOutput()
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal(buf, cp)
	if err != nil {
		fmt.Println(cp.ProcessCommand)
		fmt.Println(string(buf))
		return nil, err
	}
	return cp.GetClient(ctx)
}

func (cp *Profile) GetClientByCredentialsURI(config *sdk.Config, ctx *cli.Context) (*sdk.Client, error) {
	uri := cp.CredentialsURI
	res, err := http.Get(uri)
	if err != nil {
		return nil, err
	}

	if res.StatusCode != 200 {
		return nil, fmt.Errorf("Get Credentials from %s failed, status code %d", uri, res.StatusCode)
	}

	body, err := io.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return nil, err
	}

	type Response struct {
		Code            string
		AccessKeyId     string
		AccessKeySecret string
		SecurityToken   string
		Expiration      string
	}
	var response Response
	err = json.Unmarshal(body, &response)
	if err != nil {
		return nil, fmt.Errorf("Unmarshal credentials failed, the body %s", string(body))
	}

	if response.Code != "Success" {
		return nil, fmt.Errorf("Get sts token err, Code is not Success")
	}

	cred := credentials.NewStsTokenCredential(response.AccessKeyId, response.AccessKeySecret, response.SecurityToken)
	return sdk.NewClientWithOptions(cp.RegionId, config, cred)
}

func IsRegion(region string) bool {
	if match, _ := regexp.MatchString("^[a-zA-Z0-9-]*$", region); !match {
		return false
	}
	return true
}
