package internal

import (
	"fmt"
	"os"
	"time"

	"github.com/juju/errors"
	log "github.com/sirupsen/logrus"
	"gopkg.in/yaml.v3"

	"github.com/tidbcloud/auto-deploy/internal/argocd"
	cloud_provider "github.com/tidbcloud/auto-deploy/internal/cloud-provider"
	"github.com/tidbcloud/auto-deploy/internal/constants"
	global_client "github.com/tidbcloud/auto-deploy/internal/global-client"
	"github.com/tidbcloud/auto-deploy/internal/job"
	"github.com/tidbcloud/auto-deploy/internal/meta"
	"github.com/tidbcloud/auto-deploy/internal/metrics"
	oidc_provider "github.com/tidbcloud/auto-deploy/internal/oidc-provider"
	"github.com/tidbcloud/auto-deploy/internal/types"
	"github.com/tidbcloud/auto-deploy/internal/utils"
	"github.com/tidbcloud/auto-deploy/internal/utils/s3"
)

const (
	BYOC_AUDIT_LOG_WRITE_ROLE_NAME = "tidbcloud-audit-log-write-only-role"
)

type DeployAwsRegionArgs struct {
	OidcProviderType        types.OidcProviderType
	InstanceName            string
	JobPath                 string
	RegionName              string
	RoleArn                 string
	Region                  string
	K8s                     string
	OutputBucket            string
	TerraformStateBucket    string
	TidbClusterAccounts     []string
	ImageRegistry           string
	JobOptions              string
	GlobalOptions           string
	ForceOverwriteResources bool
	MetaURI                 string
	DeployType              string
	ServicePlan             string
}

func DeployAwsRegion(args DeployAwsRegionArgs) error {
	metrics.AddCommonLabel(metrics.LABEL_CLOUD_PROVIDER, "aws")
	metrics.AddCommonLabel(metrics.LABEL_CLOUD_REGION, args.Region)
	metrics.AddCommonLabel(metrics.LABEL_REGION_NAME, args.RegionName)
	metrics.AddCommonLabel(metrics.LABEL_ENVIRONMENT, args.InstanceName)
	metrics.AddCommonLabel(metrics.LABEL_DEPLOY_TYPE, args.DeployType)
	logger := log.WithFields(log.Fields{
		"instance": args.InstanceName,
		"region":   args.Region,
		"name":     args.RegionName,
		"meta-uri": args.MetaURI,
	})

	logger.WithFields(log.Fields{
		"role-arn": args.RoleArn,
		"job-path": args.JobPath,
	}).Info("Deploy aws region")

	oidcProvider, err := oidc_provider.GetProvider(args.OidcProviderType)
	if err != nil {
		return err
	}
	metrics.AddCommonLabel(metrics.LABEL_DEPLOY_ID, oidcProvider.GetRunID())

	account, err := types.FromAwsArn(args.RoleArn)
	if err != nil {
		return err
	}
	cloudProvider, err := cloud_provider.NewAWSCloudProvider(oidcProvider, *account, args.Region)
	if err != nil {
		return err
	}
	logger.WithFields(log.Fields{
		"account":       account.AwsRole.AccountId,
		"oidc-provider": args.OidcProviderType,
	}).Info("Auth to cloud provider")

	homeDir, err := os.UserHomeDir()
	if err != nil {
		return errors.Annotatef(err, "Failed to get home dir")
	}

	tidbClusterAccounts := make([]types.Account, len(args.TidbClusterAccounts))
	if len(args.TidbClusterAccounts) == 0 {
		log.Info("Use control plane account as data plane account")
		tidbClusterAccounts = []types.Account{*account}
	} else {
		for i, role := range args.TidbClusterAccounts {
			acc, err := types.FromAwsArn(role)
			if err != nil {
				return err
			}
			tidbClusterAccounts[i] = *acc
			log.WithField("role-arn", acc.AwsRole.AccountId).Info("Add dataplane account")
		}
	}
	cloudProvider.WriteToLocal(homeDir, tidbClusterAccounts)
	logger.WithField("home-dir", homeDir).Info("Write credential files to host")

	oldRegion, err := meta.LoadRegionControl(args.MetaURI, args.InstanceName, args.RegionName)
	if err == nil {
		logger.Error("Region has already deployed, try to re-deploy")
	} else if !errors.Is(err, errors.NotFound) {
		return err
	}

	if args.ImageRegistry == "" {
		args.ImageRegistry = account.AwsRole.AccountId + ".dkr.ecr.us-west-2.amazonaws.com"
	}

	var globalOptions types.GlobalOptions
	if args.GlobalOptions != "" {
		err = yaml.Unmarshal([]byte(args.GlobalOptions), &globalOptions)
		if err != nil {
			return errors.Annotatef(err, "Failed to parse global options")
		}
	}

	if globalOptions.Env == "" {
		globalOptions.Env = args.InstanceName
	}
	if globalOptions.LogicalRegionName == "" {
		globalOptions.LogicalRegionName = args.RegionName
	}

	region := &types.Region{
		InstanceName:         args.InstanceName,
		Name:                 args.RegionName,
		Region:               args.Region,
		CloudProviderType:    cloudProvider.GetCloudProviderType(),
		CloudProvider:        cloudProvider,
		OidcProvider:         oidcProvider,
		Account:              *account,
		JobPath:              args.JobPath,
		OutputBucket:         getOutputBucket(args),
		TerraformStateBucket: getTerraformStateBucket(args),
		JobOutputs:           make(map[string]types.JobOutput),
		TidbClusterAccounts:  tidbClusterAccounts,
		ImageRegistry:        args.ImageRegistry,
		JobOptions:           make(map[string]any),
		LatestRunId:          oidcProvider.GetRunID(),
		GlobalOptions:        globalOptions,
	}

	k8s, err := utils.RenderK8sTemplate(args.K8s, region)
	if err != nil {
		return errors.Annotatef(err, "Failed to parse k8s: %s", args.K8s)
	}

	region.K8s = *k8s
	region.K8s.Region = region
	region.K8s.Name = getK8sName(region)
	region.K8s.CredentialSecretName = constants.AWS_SECRET_NAME
	region.K8s.CredentialSecret = cloudProvider.GetAuthConfigYaml(constants.AWS_SECRET_NAME, tidbClusterAccounts)

	if args.JobOptions != "" {
		jobOptions, err := utils.RenderJobOptionsTemplate(args.JobOptions, region)
		if err != nil {
			return errors.Annotatef(err, "Failed to render job additional options: %s", args.JobOptions)
		}

		region.JobOptions = jobOptions
	}

	if err = createK8s(region); err != nil {
		if !errors.Is(err, errors.AlreadyExists) || oldRegion == nil {
			return err
		}
		if oldRegion.GlobalOptions.LogicalRegionName != region.GlobalOptions.LogicalRegionName {
			return errors.Errorf("Can't set different logical region name")
		}

		// update oidc issuer and public ip
		oldRegion.K8s.PublicIps = region.K8s.PublicIps
		oldRegion.K8s.OidcIssuer = region.K8s.OidcIssuer
		oldRegion.K8s.EndPoint = region.K8s.EndPoint

		// set unsaved values
		oldRegion.K8s.CredentialSecretName = region.K8s.CredentialSecretName
		oldRegion.K8s.CredentialSecret = region.K8s.CredentialSecret
		oldRegion.K8s.Region = region

		region.K8s = oldRegion.K8s
		metrics.AddCommonLabel(metrics.LABEL_DEPOLY_METHOD, "re-deploy")
	} else {
		metrics.AddCommonLabel(metrics.LABEL_DEPOLY_METHOD, "new-deploy")
	}

	err = deployRegion(region, args.ForceOverwriteResources)
	err2 := meta.SaveRegionControl(args.MetaURI, *region)
	if err2 != nil {
		logger.Error("Failed to save region meta")
		region.Print()
	} else {
		logger.Info("Saved region meta")
	}

	if err == nil {
		authKey := os.Getenv(constants.GLOBAL_AUTH_KEY_ENV)
		client := global_client.NewGlobalClient(globalOptions.Env, authKey, global_client.DEFAULT_TIMEOUT, log.IsLevelEnabled(log.DebugLevel))
		serverAddress := ""
		sharedBucketUrl := ""
		var auditLogSetting *global_client.OPSAuditLogSetting
		switch args.DeployType {
		case global_client.DEPLOY_TYPE_DEDICATED:
			dnsName, _ := region.GetJobOption("regionalServer.dnsName")
			serverAddress = "https://" + dnsName.(string)
		case global_client.DEPLOY_TYPE_NEXTGEN:
			dnsName, _ := region.GetJobOption("dataplaneManager.dnsName")
			serverAddress = "https://" + dnsName.(string)
			err = client.NewRegionalConfigCenter(region.GlobalOptions.LogicalRegionName, serverAddress, args.ServicePlan == "BYOC")
			if err != nil {
				logger.WithError(err).Error("Failed to register regional config center")
			}
			if args.ServicePlan == "BYOC" {
				if output, ok := region.JobOutputs["sli-resources"]; ok {
					sharedBucketUrl, err = ReadJobOutput(region.CloudProviderType, output.Output, ".bucket_name")
					if err != nil {
						logger.WithError(err).Error("Failed read sli-resources output")
					}
				} else {
					logger.Error("sli-resources Job output not found")
				}

				daccount := ""
				if tidbClusterAccounts[0].AwsRole != nil {
					daccount = tidbClusterAccounts[0].AwsRole.AccountId
				}
				auditLogSetting = &global_client.OPSAuditLogSetting{
					Enabled: true,
					Location: global_client.LogLocation{
						Type:      "TYPE_STANDARD",
						RoleArn:   fmt.Sprintf("arn:aws:iam::%s:role/%s", daccount, BYOC_AUDIT_LOG_WRITE_ROLE_NAME),
						AccountId: daccount,
					},
				}
				if output, ok := region.JobOutputs["audit-log"]; ok {
					bucketName, err := ReadJobOutput(region.CloudProviderType, output.Output, ".bucket_name")
					if err != nil {
						logger.WithError(err).Error("Failed read audit-log output")
					} else {
						auditLogSetting.Location.BucketUri = "s3://" + bucketName
					}
				} else {
					logger.Error("audit-log Job output not found")
				}
			}
		default:
			return errors.Errorf("Unknown deploy type: %s", args.DeployType)
		}
		daccounts := []string{}
		for _, acc := range tidbClusterAccounts {
			if acc.AwsRole != nil {
				daccounts = append(daccounts, acc.AwsRole.AccountId)
			}
		}
		_, err = client.CreateOrUpdateRegion(args.DeployType, region.Region, region.GlobalOptions.LogicalRegionName, serverAddress, args.ServicePlan, sharedBucketUrl, auditLogSetting, globalOptions.SortPriority, daccounts, globalOptions.OpenedTenantIds, &globalOptions.Public, region.CloudProviderType)

	}
	return err
}

func deployRegion(region *types.Region, forceOverwriteResources bool) error {
	logger := log.WithFields(log.Fields{
		"instance": region.InstanceName,
		"region":   region.Region,
		"name":     region.Name,
	})
	// should NOT return, always need to save meta
	// save result
	err := argocd.RegisterK8s(&region.K8s, forceOverwriteResources)
	if err == nil {
		logger.WithField("k8s-name", region.K8s.Name).Info("Registered K8s")
		err = job.RunDeployJobs(region)
		if err != nil {
			logger.Error("Failed to run deploy jobs")
			metrics.AddMetric(metrics.METRIC_DEPLOY_STATUS, 1)
		} else {
			logger.Info("Finished to run deploy jobs")
			metrics.AddMetric(metrics.METRIC_DEPLOY_STATUS, 0)
		}
	} else {
		logger.WithField("k8s-name", region.K8s.Name).Error("Failed to registered K8s")
	}

	metrics.AddDeployTotalDurationMetric()
	metrics.WriteMetrics(fmt.Sprintf("./metrics/deploy-%s-%s.yaml", time.Now().Format("20060102-150405"), region.LatestRunId), metrics.MF_YAML)
	return err
}

func getK8sName(region *types.Region) string {
	return region.InstanceName + "-" + constants.REGION_K8S_PREFIX + "-" + region.Name
}

func getOutputBucket(args DeployAwsRegionArgs) string {
	if args.OutputBucket == "" {
		return args.InstanceName + "-" + constants.DEFAULT_OUTPUT_BUCKET
	} else {
		return args.OutputBucket
	}
}

func getTerraformStateBucket(args DeployAwsRegionArgs) string {
	if args.TerraformStateBucket == "" {
		return args.InstanceName + "-" + constants.DEFAULT_TERRAFORM_STATE_BUCKET
	} else {
		return args.TerraformStateBucket
	}
}

func createK8s(region *types.Region) error {
	log.WithField("name", region.K8s.Name).Info("Create k8s")
	satrtedAt := time.Now()
	err := region.CloudProvider.CreateK8s(&region.K8s)
	if err != nil {
		if errors.Is(err, errors.AlreadyExists) {
			return err
		}

		log.WithFields(log.Fields{
			"name":  region.K8s.Name,
			"error": err,
		}).Error("Failed to create k8s")
		err2 := region.CloudProvider.DestroyK8s(&region.K8s)
		if err2 == nil {
			log.WithField("name", region.K8s.Name).Info("Destroyed k8s")
			return err
		} else {
			log.WithFields(log.Fields{
				"name":  region.K8s.Name,
				"error": err2,
			}).Error("Failed to destroy k8s")
			return errors.Wrap(err, err2)
		}
	}

	log.WithField("name", region.K8s.Name).Info("Created k8s")
	metrics.AddMetric(metrics.METRIC_CREATE_K8S_DURATION, time.Since(satrtedAt).Seconds())
	return nil
}

func ReadJobOutput(cloudProvider types.CloudProviderType, inputPath string, jsonPath string) (string, error) {
	var bytes []byte
	var err error
	switch cloudProvider {
	case types.AWS:
		bytes, err = s3.ReadFile(inputPath)
	case types.Azure:
		bytes, err = utils.ReadAzureBlobFile(inputPath)
	case types.AliCloud:
		bytes, err = utils.ReadOssFile(inputPath)
	default:
		bytes, err = nil, errors.NotImplementedf("Not support GCP yet")
	}
	if err != nil {
		return "", err
	}

	return utils.GetStringByJsonPath(bytes, jsonPath)
}
