package cloud_provider

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/sts"
	sts_tyes "github.com/aws/aws-sdk-go-v2/service/sts/types"
	"github.com/juju/errors"
	log "github.com/sirupsen/logrus"

	"github.com/tidbcloud/auto-deploy/internal/constants"
	"github.com/tidbcloud/auto-deploy/internal/types"
	"github.com/tidbcloud/auto-deploy/internal/utils"
)

type awsCloudProvider struct {
	credentials  *sts_tyes.Credentials
	oidcProvider types.OidcProvider
	region       string
	account      types.Account
}

func NewAWSCloudProvider(oidcProvider types.OidcProvider, acc types.Account, region string) (types.CloudProvider, error) {
	token, err := oidcProvider.GetIdToken()
	if err != nil {
		return nil, err
	}
	log.WithField("token", token).Debug("Get id token")

	if oidcProvider.IsFake() {
		log.Debug("Use fake oidc, get credential from env")
		credentials := &sts_tyes.Credentials{
			AccessKeyId:     utils.Ptr(os.Getenv("AWS_ACCESS_KEY_ID")),
			SecretAccessKey: utils.Ptr(os.Getenv("AWS_SECRET_ACCESS_KEY")),
			SessionToken:    utils.Ptr(os.Getenv("AWS_SESSION_TOKEN")),
			Expiration:      utils.Ptr(time.Now().Add(time.Hour)),
		}
		return &awsCloudProvider{
			oidcProvider: oidcProvider,
			region:       region,
			credentials:  credentials,
			account:      acc,
		}, nil
	}

	sessionName := "auto-deploy-cli@" + oidcProvider.GetRunID()
	credentials, err := auth(acc, token, region, sessionName)
	return &awsCloudProvider{
		oidcProvider: oidcProvider,
		region:       region,
		credentials:  credentials,
		account:      acc,
	}, err
}

func (a *awsCloudProvider) GetCloudProviderType() types.CloudProviderType {
	return types.AWS
}

func (a *awsCloudProvider) GetAuthConfigPath(homeDir string) string {
	return homeDir + "/.aws"
}

func (a *awsCloudProvider) GetAuthConfigSubPath() []string {
	return []string{"credentials", "config"}
}

func (a *awsCloudProvider) GetAuthConfigYaml(name string, accounts []types.Account) string {
	profiles, _ := a.getProfiles(accounts)
	return `
apiVersion: v1
kind: Secret
type: Opaque
metadata:
    name: ` + name + `
stringData:
    credentials: |
` + utils.GetContentWithIndent(8, a.getCredentials()) + `
    config: |
` + utils.GetContentWithIndent(8, profiles)

}

func (a *awsCloudProvider) getCredentials() string {

	// write two profile:
	//   one for default aws command
	//   one for creating dataplane roles
	return `
[` + a.account.AwsRole.AccountId + `]
region=` + a.region + `
aws_access_key_id=` + *a.credentials.AccessKeyId + `
aws_secret_access_key=` + *a.credentials.SecretAccessKey + `
aws_session_token=` + *a.credentials.SessionToken + `

[default]
region=` + a.region + `
aws_access_key_id=` + *a.credentials.AccessKeyId + `
aws_secret_access_key=` + *a.credentials.SecretAccessKey + `
aws_session_token=` + *a.credentials.SessionToken + `
`
}

func (a *awsCloudProvider) WriteToLocal(homeDir string, accounts []types.Account) error {
	// do noting for fake provider
	if a.oidcProvider.IsFake() {
		return nil
	}

	path := a.GetAuthConfigPath(homeDir)
	err := os.MkdirAll(path, os.ModePerm&0o777)
	if err != nil {
		return errors.Annotatef(err, "Failed to create config path `%s`", path)
	}
	credPath := filepath.Join(path, "credentials")
	err = os.WriteFile(credPath, []byte(a.getCredentials()), os.ModePerm&0o644)
	if err != nil {
		return errors.Annotatef(err, "Failed to write credentials `%s`", credPath)
	}
	log.Debug("Write AWS credentials")

	profiles, err := a.getProfiles(accounts)
	if err != nil {
		return err
	}
	configPath := filepath.Join(path, "config")
	err = os.WriteFile(configPath, []byte(profiles), os.ModePerm&0o644)
	if err != nil {
		return errors.Annotatef(err, "Failed to write config `%s`", configPath)
	}
	log.Debug("Write AWS config")
	return nil
}

func (a *awsCloudProvider) getProfiles(accounts []types.Account) (string, error) {

	var buf bytes.Buffer
	buf.WriteString("# dataplane account profiles\n")
	for _, acc := range accounts {
		if *acc.AwsRole == *a.account.AwsRole {
			continue
		} else {
			buf.WriteString(`
[profile ` + acc.AwsRole.AccountId + `]
region=` + a.region + `
source_profile=default
role_arn=` + acc.AwsRole.String() + `
`)
		}
	}
	return buf.String(), nil
}

func (a *awsCloudProvider) getProfileArg(acc types.Account) string {
	if *a.account.AwsRole == *acc.AwsRole {
		return "default"
	}
	log.WithField("profile", acc.AwsRole.AccountId).Debug("With profile argument")
	return acc.AwsRole.AccountId
}

func (a *awsCloudProvider) CreateK8s(k8s *types.K8s) error {
	oidcIssuer, err := utils.RunCommandAndGetOutput("./scripts/get-eks-info.sh",
		"oidc-issuer", k8s.Name, k8s.Region.Region, a.getProfileArg(k8s.Region.Account))
	if err == nil {
		log.WithField("name", k8s.Name).Info("Clsuter already exists")
		k8s.OidcIssuer = oidcIssuer
		a.getPublicIp(k8s)
		a.getEndpoint(k8s)
		return errors.AlreadyExistsf("Cluster %s", k8s.Name)
	}

	args, err := getCreateK8sArgs(k8s)
	if err != nil {
		return err
	}

	exitCode, err := utils.RunCommand("./scripts/create-eks.sh",
		utils.LogWithPrefix(log.InfoLevel, fmt.Sprintf("create-%s-output: ", k8s.Name)),
		args, a.getProfileArg(k8s.Region.Account))
	if err != nil {
		return errors.Annotatef(err, "k8sName=%s", k8s.Name)
	}
	if exitCode != 0 {
		return errors.Errorf("Failed to create k8s %s, exitCode=%d", k8s.Name, exitCode)
	}
	log.WithFields(log.Fields{
		"name":    k8s.Name,
		"version": k8s.Version,
		"region":  k8s.Region.Region,
	}).Info("Created K8s")

	oidcIssuer, err = utils.RunCommandAndGetOutput("./scripts/get-eks-info.sh",
		"oidc-issuer", k8s.Name, k8s.Region.Region, a.getProfileArg(k8s.Region.Account))
	if err != nil {
		return err
	}
	k8s.OidcIssuer = oidcIssuer

	a.getPublicIp(k8s)
	a.getEndpoint(k8s)
	log.WithFields(log.Fields{
		"oidc-isuer": oidcIssuer,
		"public-ip":  k8s.PublicIps,
		"endpoint":   k8s.EndPoint,
	}).Debug("Get kubeconfig")
	return nil
}

func (a *awsCloudProvider) getEndpoint(k8s *types.K8s) {
	endpoint, err := utils.RunCommandAndGetOutput("./scripts/get-eks-info.sh",
		"endpoint", k8s.Name, k8s.Region.Region, a.getProfileArg(k8s.Region.Account))
	if err == nil {
		k8s.EndPoint = endpoint
	}
	log.WithField("endpoint", endpoint).Debug("Get endpoint")
}

func (a *awsCloudProvider) getPublicIp(k8s *types.K8s) {
	publicIp, err := utils.RunCommandAndGetOutput("./scripts/get-eks-info.sh",
		"public-ip", k8s.Name, k8s.Region.Region, a.getProfileArg(k8s.Region.Account))
	if err == nil {
		k8s.PublicIps = strings.Split(publicIp, "\n")
	}
	log.WithField("public-ip", publicIp).Debug("Get public ip")
}

func (a *awsCloudProvider) DestroyK8s(k8s *types.K8s) error {
	args, err := getCreateK8sArgs(k8s)
	if err != nil {
		return err
	}

	exitCode, err := utils.RunCommand("./scripts/destroy-eks.sh",
		utils.LogWithPrefix(log.InfoLevel, fmt.Sprintf("destroy-%s-output: ", k8s.Name)),
		args, a.getProfileArg(k8s.Region.Account))
	if err != nil {
		return errors.Annotatef(err, "k8sName=%s", k8s.Name)
	}
	if exitCode != 0 {
		return errors.Errorf("Failed to create k8s %s, exitCode=%d", k8s.Name, exitCode)
	}
	log.WithFields(log.Fields{
		"name":   k8s.Name,
		"region": k8s.Region.Region,
	}).Info("Destroyed K8s")
	return nil
}

func (a *awsCloudProvider) GetKubeConfig(k8s *types.K8s) (string, error) {
	return a.getKubeConfigString(k8s)
}

func auth(acc types.Account, webIdentityToken, region, sessionName string) (*sts_tyes.Credentials, error) {
	ctx := context.TODO()
	cfg, err := config.LoadDefaultConfig(ctx)
	if err != nil {
		return nil, errors.Annotatef(err, "Failed to load default aws config")
	}
	log.WithField("config", cfg).Debug("Load default config")

	// get max duration from env
	var maxDuration *int32 = nil // default value
	strDuration := os.Getenv(constants.MAX_AWS_SESSION_DURATION_IN_SECONDS)
	if strDuration != "" {
		duration, err := strconv.Atoi(strDuration)
		if err != nil || duration < 0 {
			log.Errorf("Failed to parse the max duration: %s. Use default max duration", strDuration)
		} else {
			maxDuration = utils.Ptr(int32(duration))
		}
	}

	cfg.Region = region
	stsClient := sts.NewFromConfig(cfg)
	output, err := stsClient.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityInput{
		RoleArn:          utils.Ptr(acc.AwsRole.String()),
		WebIdentityToken: &webIdentityToken,
		RoleSessionName:  &sessionName,
		DurationSeconds:  maxDuration,
	})
	if err != nil {
		return nil, errors.Annotatef(err, "Failed to get session token")
	}
	log.WithField("role", acc.AwsRole.Role).Debug("Get session token from AWS")

	utils.MaskSensitiveGithubOutput(*output.Credentials.SecretAccessKey)
	utils.MaskSensitiveGithubOutput(*output.Credentials.SessionToken)
	return output.Credentials, nil
}

// Get kubeconfig file path
//
//lint:ignore U1000 may use later
func (a *awsCloudProvider) getKubeConfigPath(k8s *types.K8s) (string, error) {
	return utils.RunCommandAndGetOutput("./scripts/get-eks-info.sh",
		"kube-config", k8s.Name, k8s.Region.Region, a.getProfileArg(k8s.Region.Account))
}

// Generate kubeconfig file
//
//lint:ignore U1000 may use later
func (a *awsCloudProvider) getKubeConfigString(k8s *types.K8s) (string, error) {
	logger := log.WithFields(log.Fields{
		"k8s":     k8s.Name,
		"region":  k8s.Region.Region,
		"account": k8s.Region.Account,
	})

	ca, err := utils.RunCommandAndGetOutput("./scripts/get-eks-info.sh",
		"ca", k8s.Name, k8s.Region.Region, a.getProfileArg(k8s.Region.Account))
	if err != nil {
		return "", err
	}
	utils.MaskSensitiveGithubOutput(ca)
	logger.WithField("ca", ca).Debug("Get ca")
	profile := "default"
	if *a.account.AwsRole != *k8s.Region.Account.AwsRole {
		profile = k8s.Region.Account.AwsRole.AccountId
	}

	kubeconfig := `
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: ` + ca + `
    server: ` + k8s.EndPoint + `
  name: ` + k8s.Name + `
contexts:
- context:
    cluster: ` + k8s.Name + `
    user: user
  name: ` + k8s.Name + `
current-context: ` + k8s.Name + `
kind: Config
preferences: {}
users:
- name: user
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - token
      - -i
      - ` + k8s.Name + `
      command: aws-iam-authenticator
      env:
      - name: AWS_STS_REGIONAL_ENDPOINTS
        value: regional
      - name: AWS_DEFAULT_REGION
        value: ` + k8s.Region.Region + `
      - name: AWS_PROFILE
        value: "` + profile + `"
      provideClusterInfo: false
`
	logger.WithField("kubeconfig", kubeconfig).Debug("Get kubeconfig with profile")
	return kubeconfig, nil
}
