package global_client

import (
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

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

const DEFAULT_TIMEOUT = time.Second * 10
const PAGE_SZIE = 100
const DEPLOY_TYPE_DEDICATED = "DEPLOY_TYPE_DEDICATED"
const DEPLOY_TYPE_NEXTGEN = "DEPLOY_TYPE_NEXTGEN"

const (
	GLOBAL_URL_REGIONS    = "/inner/mgmt/v1/regions"
	GLOBAL_URL_REGION_ID  = "/inner/mgmt/v1/regions/%s"
	GLOBAL_URL_SKU_GRROUP = "/inner/billing/sku_groups/%d"
	GLOBAL_URL_SKUS       = "/inner/billing/units"
	GLOBAL_URL_NEW_SKU    = "/inner/billing/units/_create_by_group"
	GLOBAL_URL_SKU        = "/inner/billing/units/%s"
	GLOBAL_URL_MGMT_SPU   = "/inner/mgmt/v1/spus/%s"

	GLOBAL_URL_SHARED_POOL_CONFIG_CENTER        = "/inner/config-center/cluster-types/SHARED_POOL/regional-servers"
	GLOBAL_URL_SHARED_POOL_CONFIG_CENTER_ID     = "/inner/config-center/cluster-types/SHARED_POOL/regional-servers/%s"
	GLOBAL_URL_LOGICAL_CLUSTER_CONFIG_CENTER    = "/inner/config-center/cluster-types/LOGICAL_CLUSTER/regional-servers"
	GLOBAL_URL_LOGICAL_CLUSTER_CONFIG_CENTER_ID = "/inner/config-center/cluster-types/LOGICAL_CLUSTER/regional-servers/%s"
)

const (
	INACTIVE_STATE = "INACTIVE"
	ACTIVE_STATE   = "ACTIVE"
)

type RegionList struct {
	Regions []Region `json:"regions"`
}

type Region struct {
	DeployType         string                   `json:"deployType"`
	Region             RegionDetail             `json:"region"`
	ProviderIdentities []RegionProviderIdentity `json:"regionProviderIdentities"`
}

type LogLocation struct {
	Type       string `json:"type"`
	BucketUri  string `json:"bucketUri"`
	RoleArn    string `json:"roleArn"`
	ExternalId string `json:"externalId"`
	AccountId  string `json:"accountId"`
}

type RegionProviderIdentity struct {
	Identity string `json:"identity"`
}

type OPSAuditLogSetting struct {
	Enabled  bool        `json:"enabled"`
	Location LogLocation `json:"logLocation"`
}

type RegionDetail struct {
	Id                string              `json:"id"`
	CloudProviderId   string              `json:"cloudProviderId"`
	DisplayName       string              `json:"displayName"`
	OpenedTenantIds   []string            `json:"openedTenantIds"`
	SortPriority      int                 `json:"sortPriority"`
	Area              string              `json:"area"`
	CountryCode       string              `json:"countryCode"`
	State             string              `json:"state"`
	Public            bool                `json:"isPublic"`
	DefaultBackupTime string              `json:"defaultBackupTime"`
	ServiceAddress    string              `json:"serviceAddress"`
	LogicalName       string              `json:"logicalName,omitempty"`
	ServicePlan       string              `json:"servicePlan,omitempty"`
	SharedBucketUrl   string              `json:"sharedBucketUrl,omitempty"`
	AuditLogSetting   *OPSAuditLogSetting `json:"ops_audit_log_setting,omitempty"`
}

type AddSkuRequest struct {
	GroupId     int               `json:"group_id"`
	Provider    string            `json:"provider"`
	Region      string            `json:"region"`
	GroupLabels map[string]string `json:"group_labels"`
}

type SkuResponse struct {
	SkuKey          string                  `json:"unit"`
	GroupId         int                     `json:"group_id"`
	Provider        types.CloudProviderType `json:"provider"`
	Region          string                  `json:"region"`
	Description     string                  `json:"description"`
	Category        string                  `json:"category"`
	EditionCode     string                  `json:"edition_code"`
	CommodityCode   string                  `json:"commodity_code"`
	PriceType       string                  `json:"price_type"`
	Price           string                  `json:"price"`
	PriceExpression map[string]string       `json:"price_expression"`
	SkuType         string                  `json:"sku_type"`
	MeasureUnit     string                  `json:"measure_unit"`
	SkuTemplateId   int                     `json:"sku_template_id"`
	Version         int                     `json:"version"`
}

type SkuGroup struct {
	GroupId         int               `json:"group_id" yaml:"group_id"`
	SkuKeyPattern   string            `json:"unit_key_pattern" yaml:"unit_key_pattern"`
	Name            string            `json:"name" yaml:"name"`
	Category        string            `json:"category" yaml:"category"`
	PriceFormula    string            `json:"price_formula" yaml:"price_formula"`
	GroupLabels     []string          `json:"group_labels" yaml:"group_labels"`
	PriceType       string            `json:"price_type" yaml:"price_type"`
	PriceExpression map[string]string `json:"price_expression" yaml:"price_expression"`
	MeasureUnit     string            `json:"measure_unit" yaml:"measure_unit"`
}

type Spu struct {
	Id            string       `json:"id"`
	Key           string       `json:"key"`
	ComponentType string       `json:"componentType"`
	DisplayName   string       `json:"displayName"`
	Name          string       `json:"name"`
	CpuCore       int          `json:"cpuCore"`
	MemorySizeGi  int          `json:"memorySizeGi"`
	Mappings      []SpuMapping `json:"mappings"`
	Default       bool         `json:"default"`
}

type SpuMapping struct {
	CloudProvider   string   `json:"cloudProvider"`
	PhysicalRegions []string `json:"physicalRegions"`
	OrganizationIds []string `json:"organizationIds"`
	TierUnitKey     string   `json:"skuKey"`
}

type RegionalConfigCenter struct {
	LogicalRegion string `json:"logicalRegion"`
	ServerAddress string `json:"serverAddress"`
	IsByoc        bool   `json:"isByoc"`
}

func (client *GlobalClient) CreateOrUpdateRegion(deployType, region, logicalName, serviceAddress, servicePlan, sharedBucketUrl string, auditLogSetting *OPSAuditLogSetting, sortPriority int, dataplaneAccountIds, openedTenantIds []string, public *bool, cloudProviderType types.CloudProviderType) (*Region, error) {
	if deployType == DEPLOY_TYPE_DEDICATED {
		servicePlan = "" // dedicated region does not have service plan
	}

	oldRegion, err := client.GetRegionByIdViaList(deployType, logicalName, cloudProviderType)
	if err == nil {
		// update region
		updated := false
		if oldRegion.Region.ServiceAddress != serviceAddress {
			oldRegion.Region.ServiceAddress = serviceAddress
			updated = true
		}
		if oldRegion.Region.State != ACTIVE_STATE {
			oldRegion.Region.State = ACTIVE_STATE
			updated = true
		}
		if len(openedTenantIds) > 0 && !slices.Equal(openedTenantIds, oldRegion.Region.OpenedTenantIds) {
			oldRegion.Region.OpenedTenantIds = openedTenantIds
			updated = true
		}
		if sortPriority > 0 && sortPriority != oldRegion.Region.SortPriority {
			oldRegion.Region.SortPriority = sortPriority
			updated = true
		}
		if public != nil && oldRegion.Region.Public != *public {
			oldRegion.Region.Public = *public
			updated = true
		}
		if serviceAddress != "" && oldRegion.Region.ServiceAddress != serviceAddress {
			oldRegion.Region.ServiceAddress = serviceAddress
			updated = true
		}
		if servicePlan != "" && oldRegion.Region.ServicePlan != servicePlan {
			oldRegion.Region.ServicePlan = servicePlan
			updated = true
		}
		if sharedBucketUrl != "" && oldRegion.Region.SharedBucketUrl != sharedBucketUrl {
			oldRegion.Region.SharedBucketUrl = sharedBucketUrl
			updated = true
		}
		if auditLogSetting != nil && (oldRegion.Region.AuditLogSetting.Enabled != auditLogSetting.Enabled || oldRegion.Region.AuditLogSetting.Location.BucketUri != auditLogSetting.Location.BucketUri) {
			oldRegion.Region.AuditLogSetting = auditLogSetting
			updated = true
		}

		if updated {
			updatedRegion := Region{}
			err = client.PutResult(fmt.Sprintf(GLOBAL_URL_REGION_ID, logicalName), map[string]string{}, oldRegion, &updatedRegion)
			if err != nil {
				err = errors.Annotatef(err, "Failed to update region")
			}
			return &updatedRegion, err
		}
		return oldRegion, nil
	}
	if !errors.Is(err, errors.NotFound) {
		return nil, errors.Annotatef(err, "Failed to get region")
	}
	return client.CreateRegion(deployType, region, logicalName, serviceAddress, servicePlan, sharedBucketUrl, auditLogSetting, sortPriority, dataplaneAccountIds, openedTenantIds, public != nil && *public, cloudProviderType)
}

func (client *GlobalClient) SetRegionState(deployType, logicalName, state, serviceAddress string, cloudProviderType types.CloudProviderType) error {
	oldRegion, err := client.GetRegionByIdViaList(deployType, logicalName, cloudProviderType)
	if err != nil {
		// if not found, ignore
		if errors.Is(err, errors.NotFound) {
			return nil
		}
		return err
	}

	// disable region
	updated := false
	if oldRegion.Region.State != state {
		oldRegion.Region.State = state
		updated = true
	}

	if serviceAddress != "" && serviceAddress != oldRegion.Region.ServiceAddress {
		oldRegion.Region.ServiceAddress = serviceAddress
		updated = true
	}

	if updated {
		updatedRegion := Region{}
		err = client.PutResult(fmt.Sprintf(GLOBAL_URL_REGION_ID, logicalName), map[string]string{}, oldRegion, &updatedRegion)
		if err != nil {
			err = errors.Annotatef(err, "Failed to update region")
		}
		return err
	}
	return nil
}

func (client *GlobalClient) UpdateRegionInfo(deployType string, cloudProviderType types.CloudProviderType, logicalName, state string, public *bool, openTenantsId []string) error {
	oldRegion, err := client.GetRegionByIdViaList(deployType, logicalName, cloudProviderType)
	if err != nil {
		// if not found, ignore
		if errors.Is(err, errors.NotFound) {
			return nil
		}
		return err
	}

	// update region
	updated := false
	if state != "" && state != oldRegion.Region.State {
		oldRegion.Region.State = state
		updated = true
	}

	if public != nil && *public != oldRegion.Region.Public {
		oldRegion.Region.Public = *public
		updated = true
	}

	if openTenantsId != nil && !slices.Equal(openTenantsId, oldRegion.Region.OpenedTenantIds) {
		oldRegion.Region.OpenedTenantIds = openTenantsId
		updated = true
	}

	if updated {
		updatedRegion := Region{}
		err = client.PutResult(fmt.Sprintf(GLOBAL_URL_REGION_ID, logicalName), map[string]string{}, oldRegion, &updatedRegion)
		if err != nil {
			err = errors.Annotatef(err, "Failed to update region")
		}
		return err
	}
	return nil
}

// Get region by id via list all region
// if region is not found, return a not found error
func (client *GlobalClient) GetRegionByIdViaList(deployType, logicalName string, cloudProviderType types.CloudProviderType) (*Region, error) {
	queryParams := map[string]string{
		"deployType": deployType,
		"pageSize":   strconv.Itoa(PAGE_SZIE),
	}

	for i := 1; ; i++ {
		result := RegionList{}
		queryParams["page"] = strconv.Itoa(i)
		err := client.GetResult(GLOBAL_URL_REGIONS, queryParams, &result)
		if err != nil {
			return nil, err
		}

		for _, r := range result.Regions {
			if r.Region.LogicalName == logicalName {
				return &r, nil
			}
		}
		if len(result.Regions) < PAGE_SZIE {
			break
		}
	}
	return nil, errors.NotFoundf("Region %s on %s", logicalName, client.instance)
}

// Get region by region id
// if region is not found, return a bad request error
func (client *GlobalClient) GetRegionById(deployType, logicalName string, cloudProviderType types.CloudProviderType) (*Region, error) {
	result := Region{}
	err := client.GetResult(fmt.Sprintf(GLOBAL_URL_REGION_ID, logicalName), map[string]string{
		"deployType":      deployType,
		"cloudProviderId": strings.ToUpper(string(cloudProviderType)),
	}, &result)

	return &result, err
}

// Delete region by region id
// if region is not found, return a bad request error
func (client *GlobalClient) DeleteRegionById(deployType, logicalName string, cloudProviderType types.CloudProviderType) error {
	body, statusCode, err := client.Delete(fmt.Sprintf(GLOBAL_URL_REGION_ID, logicalName), map[string]string{
		"deployType":      deployType,
		"cloudProviderId": strings.ToUpper(string(cloudProviderType)),
	})
	if err != nil {
		return err
	}
	if statusCode != 200 {
		return fmt.Errorf("delete region: %s", body)
	}
	return nil
}

func (client *GlobalClient) CreateRegion(deployType, region, logicalName, serviceAddress, servicePlan, sharedBucketUrl string, auditLogSetting *OPSAuditLogSetting, sortPriority int, dataplaneAccountIds, openedTenantIds []string, public bool, cloudProviderType types.CloudProviderType) (*Region, error) {
	var area, defaultBackupTime, country, displayName string
	var err error
	switch cloudProviderType {
	case types.AWS:
		regionName, err := utils.GetAwsRegionName(region)
		if err != nil {
			return nil, err
		}
		area, err = utils.GetAwsRegionArea(region)
		if err != nil {
			return nil, err
		}
		defaultBackupTime, err = utils.GetAwsRegionBackupTime(region)
		if err != nil {
			return nil, err
		}
		displayName = fmt.Sprintf("%s (%s)", regionName, region)
		country, err = utils.GetAwsRegionCountryCode(region)
		if err != nil {
			return nil, err
		}
	case types.Azure:
		area, err = utils.GetAzureRegionArea(region)
		if err != nil {
			return nil, err
		}
		defaultBackupTime, err = utils.GetAzureRegionBackupTime(region)
		if err != nil {
			return nil, err
		}
		displayName, err = utils.GetAzureRegionName(region)
		if err != nil {
			return nil, err
		}
		country, err = utils.GetAzureRegionCountryCode(region)
		if err != nil {
			return nil, err
		}
	case types.AliCloud:
		area, err = utils.GetAliCloudRegionArea(region)
		if err != nil {
			return nil, err
		}
		defaultBackupTime, err = utils.GetAliCloudRegionBackupTime(region)
		if err != nil {
			return nil, err
		}
		displayName, err = utils.GetAliCloudRegionName(region)
		if err != nil {
			return nil, err
		}
		country, err = utils.GetAliCloudRegionCountryCode(region)
		if err != nil {
			return nil, err
		}
	default:
		return nil, errors.NotImplementedf("Cloud provider type %s", cloudProviderType)
	}

	providerIdentities := []RegionProviderIdentity{}
	for _, daccount := range dataplaneAccountIds {
		providerIdentities = append(providerIdentities, RegionProviderIdentity{
			Identity: daccount,
		})
	}

	req := Region{
		DeployType: deployType,
		Region: RegionDetail{
			Id:                GetRegionId(region, cloudProviderType),
			CloudProviderId:   strings.ToUpper(string(cloudProviderType)),
			DisplayName:       displayName,
			CountryCode:       country,
			OpenedTenantIds:   openedTenantIds,
			SortPriority:      sortPriority,
			State:             ACTIVE_STATE,
			Area:              area,
			Public:            public,
			DefaultBackupTime: defaultBackupTime,
			ServiceAddress:    serviceAddress,
			LogicalName:       logicalName,
			ServicePlan:       servicePlan,
			SharedBucketUrl:   sharedBucketUrl,
			AuditLogSetting: auditLogSetting,
		},
		ProviderIdentities: providerIdentities,
	}
	result := Region{}
	err = client.PostResult(GLOBAL_URL_REGIONS, map[string]string{}, &req, &result)
	return &result, err
}

func GetRegionId(region string, cloudProviderType types.CloudProviderType) string {
	id := string(cloudProviderType) + "-" + region
	return strings.Replace(strings.ToUpper(id), "-", "_", -1)
}

func (client *GlobalClient) NewSku(req AddSkuRequest) (*SkuResponse, error) {
	resp := SkuResponse{}
	err := client.PostResult(GLOBAL_URL_NEW_SKU, map[string]string{}, req, &resp)
	return &resp, err
}

func (client *GlobalClient) DeleteSku(key string) error {
	_, _, err := client.Delete(fmt.Sprintf(GLOBAL_URL_SKU, key), map[string]string{})
	return err
}

func (client *GlobalClient) GetSku(key string) (*SkuResponse, error) {
	resp := SkuResponse{}
	err := client.GetResult(fmt.Sprintf(GLOBAL_URL_SKU, key), map[string]string{}, &resp)
	return &resp, err
}

func (client *GlobalClient) GetSkuGroupById(id int) (*SkuGroup, error) {
	resp := SkuGroup{}
	err := client.GetResult(fmt.Sprintf(GLOBAL_URL_SKU_GRROUP, id), map[string]string{}, &resp)
	return &resp, err
}

func (client *GlobalClient) GetMgmtSpuById(id string) (*Spu, error) {
	resp := Spu{}
	err := client.GetResult(fmt.Sprintf(GLOBAL_URL_MGMT_SPU, id), map[string]string{}, &resp)
	return &resp, err
}

func (client *GlobalClient) PatchMgmtSpu(spu Spu) (*Spu, error) {
	resp := Spu{}
	err := client.PatchResult(fmt.Sprintf(GLOBAL_URL_MGMT_SPU, spu.Id), map[string]string{}, &spu, &resp)
	return &resp, err
}

func (client *GlobalClient) NewRegionalConfigCenter(logicalRegion, serverAddress string, isByoc bool) error {
	req := RegionalConfigCenter{
		LogicalRegion: logicalRegion,
		ServerAddress: serverAddress,
		IsByoc:        isByoc,
	}
	resp := RegionalConfigCenter{}
	err := client.PostResult(GLOBAL_URL_LOGICAL_CLUSTER_CONFIG_CENTER, map[string]string{}, &req, &resp)
	if err != nil {
		return errors.Annotatef(err, "Failed to register LOGICAL_CLUSTER config center (name=%s, serviceAddress=%s)", logicalRegion, serverAddress)
	}
	err = client.PostResult(GLOBAL_URL_SHARED_POOL_CONFIG_CENTER, map[string]string{}, &req, &resp)
	if err != nil {
		return errors.Annotatef(err, "Failed to register SHARED_POOL config center (name=%s, serviceAddress=%s)", logicalRegion, serverAddress)
	}
	return nil
}

func (client *GlobalClient) DeleteRegionalConfigCenter(logicalRegion string) error {
	_, _, err := client.Delete(fmt.Sprintf(GLOBAL_URL_LOGICAL_CLUSTER_CONFIG_CENTER_ID, logicalRegion), map[string]string{})
	if err != nil {
		return errors.Annotatef(err, "Failed to unregister LOGICAL_CLUSTER config center (name=%s)", logicalRegion)
	}
	_, _, err = client.Delete(fmt.Sprintf(GLOBAL_URL_SHARED_POOL_CONFIG_CENTER_ID, logicalRegion), map[string]string{})
	if err != nil {
		return errors.Annotatef(err, "Failed to unregister SHARED_POOL config center (name=%s)", logicalRegion)
	}
	return nil
}
