package provider

import (
	"cmp"
	"context"
	"errors"
	"fmt"
	"slices"
	"strings"

	"github.com/samber/lo"
	accountsdk "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/account"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/serverlessglobal"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/apimiddleware/bizerror"
	appcommon "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

type Server interface {
	ListRegions(
		ctx context.Context,
		req *openapiserverlessv1beta1.ListRegionsRequest,
	) (*openapiserverlessv1beta1.ListRegionsResponse, error)
	ListCloudProviders(
		ctx context.Context,
		req *openapiserverlessv1beta1.ListCloudProvidersRequest,
	) (*openapiserverlessv1beta1.ListCloudProvidersResponse, error)
}

func NewServer(serverlessCli serverlessglobal.ServerlessGlobal, accountCli account.Client) Server {
	return &serverImpl{
		serverlessCli: serverlessCli,
		accounCli:     accountCli,
	}
}

type serverImpl struct {
	serverlessCli serverlessglobal.ServerlessGlobal
	accounCli     account.Client
}

func (r serverImpl) ListRegions(
	ctx context.Context,
	req *openapiserverlessv1beta1.ListRegionsRequest,
) (*openapiserverlessv1beta1.ListRegionsResponse, error) {
	// Extract request meta.
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	/* priority: marketplace org > alicloud whitelist org > org country */
	var orgMeta *accountsdk.Org
	thirdProviders, err := r.accounCli.GetTenantThirdPartyAccountProviders(ctx, reqMeta.OrgID)
	if err != nil {
		return nil, bizerror.NewInternal(
			errors.New("failed to get third party account providers"),
			"failed to get third party account providers",
		)
	}
	if len(thirdProviders) == 0 {
		// it's not a marketplace org, try checking if it's a alicloud whitelist org
		isAlicloudOrg, err := r.accounCli.IsHitFeatureFlag(
			ctx,
			&accountsdk.HitFeatureFlagReq{OrgId: reqMeta.OrgID, FlagName: "enable_alibaba_cloud_orgs"},
		)
		if err != nil {
			tilog.Error(ctx, "failed to check alibaba cloud org feature flag", zap.Error(err))
		}
		if !isAlicloudOrg {
			// if not a alicloud whitelist org, try checking org country
			orgMeta, _ = r.accounCli.GetOrgByID(ctx, reqMeta.OrgID)
		}
	}

	// First, list all cloud providers.
	resp, err := r.serverlessCli.ListCloudProviders(ctx, &globalserverlessv1.CloudProviderServiceListRequest{
		OrganizationId: reqMeta.OrgID,
		ServicePlan:    appcommon.CvtGlobalServicePlan(req.GetServicePlan()),
	})
	if err != nil {
		return nil, err
	}

	if resp.Total == 0 {
		return nil, bizerror.NewNotFound(errors.New("no cloud provider found"), "no cloud provider found")
	}

	regionMap := make(map[string]*openapicommonv1beta1.Region)
	// Then, list all regions for each cloud provider.
	for _, p := range resp.GetCloudProviders() {
		for _, r := range p.GetRegions() {
			// check region status
			if r.State != globalcommonv1.Region_ACTIVE {
				tilog.Info(
					ctx,
					"region is not active",
					zap.String("region", r.Id.String()),
					zap.Uint64("org_id", reqMeta.OrgID),
				)
				continue
			}

			// check region allow list if this region is not pubic
			if !r.IsPublic && len(r.GetOpenedTenantIds()) > 0 && !lo.Contains(r.GetOpenedTenantIds(), reqMeta.OrgID) {
				tilog.Info(
					ctx,
					"region not pubilc",
					zap.String("region", r.Id.String()),
					zap.Uint64("org_id", reqMeta.OrgID),
				)
				continue
			}

			cloudProvider, err := enum.FromString[openapicommonv1beta1.Region_CloudProvider](p.Id.String())
			if err != nil {
				return nil, bizerror.NewInternal(err, fmt.Sprintf("provider %s is not supported", p.Id.String()))
			}
			tilog.Info(
				ctx,
				"region",
				zap.Any("thirdProviders", thirdProviders),
				zap.Any("cloudProvider", cloudProvider),
			)

			// check third party account provider
			if len(thirdProviders) > 0 &&
				!lo.Contains(thirdProviders, openapicommonv1beta1.Region_CloudProvider(cloudProvider)) {
				tilog.Warn(
					ctx,
					"region not allowed by third party account provider",
					zap.String("region", r.Id.String()),
					zap.Uint64("org_id", reqMeta.OrgID),
					zap.Any("third providers", thirdProviders),
				)
				continue
			}
			if orgMeta.GetCountry() == "JP" &&
				openapicommonv1beta1.Region_CloudProvider(cloudProvider) == openapicommonv1beta1.Region_alicloud {
				// disable alicloud region in JP org
				tilog.Warn(
					ctx,
					"alicloud region not allowed by org country",
					zap.Uint64("org_id", reqMeta.OrgID),
					zap.Any("org_country", orgMeta.Country))
				continue
			}
			openapiId := fmt.Sprintf("%s-%s", strings.ToLower(p.GetId().String()), r.Id.String())
			servicePlan := appcommon.CvtOpenAPIServicePlan(r.ServicePlan)
			engineType := cvtOpenAPIEngineType(r.GetEngineType())
			if _, ok := regionMap[openapiId]; ok {
				regionMap[openapiId].ServicePlans = append(
					regionMap[openapiId].ServicePlans,
					servicePlan,
				)
				regionMap[openapiId].ServicePlans = lo.Uniq(regionMap[openapiId].ServicePlans)
				appendSupportedEngineType(regionMap[openapiId], servicePlan, engineType)
			} else {
				regionMap[openapiId] = &openapicommonv1beta1.Region{
					Name:          fmt.Sprintf("regions/%s", openapiId),
					DisplayName:   r.DisplayName,
					CloudProvider: openapicommonv1beta1.Region_CloudProvider(cloudProvider),
					Provider:      lo.ToPtr(openapicommonv1beta1.Region_CloudProvider(cloudProvider).String()),
					RegionId:      openapiId,
					ServicePlans:  []openapicommonv1beta1.ServicePlan{servicePlan},
					SupportedServicePlans: []*openapicommonv1beta1.ServicePlanInfo{{
						ServicePlan:          servicePlan,
						DisplayName:          servicePlanDisplayName(servicePlan),
						SupportedEngineTypes: []openapicommonv1beta1.EngineType{engineType},
					}},
				}
			}
		}
	}

	for _, region := range regionMap {
		setSupportedServicePlans(region)
	}

	regions := lo.Values(regionMap)
	// Sort regions by region.cloud_provider first, then by region.name.
	slices.SortFunc(regions, func(a, b *openapicommonv1beta1.Region) int {
		if c := cmp.Compare(a.CloudProvider, b.CloudProvider); c != 0 {
			return c
		}
		return cmp.Compare(a.Name, b.Name)
	})
	return &openapiserverlessv1beta1.ListRegionsResponse{Regions: regions}, nil
}

func appendSupportedEngineType(
	region *openapicommonv1beta1.Region,
	plan openapicommonv1beta1.ServicePlan,
	engineType openapicommonv1beta1.EngineType,
) {
	if region == nil {
		return
	}
	for _, info := range region.GetSupportedServicePlans() {
		if info == nil || info.GetServicePlan() != plan {
			continue
		}
		info.SupportedEngineTypes = lo.Uniq(append(info.GetSupportedEngineTypes(), engineType))
		return
	}
	region.SupportedServicePlans = append(region.SupportedServicePlans, &openapicommonv1beta1.ServicePlanInfo{
		ServicePlan:          plan,
		DisplayName:          servicePlanDisplayName(plan),
		SupportedEngineTypes: []openapicommonv1beta1.EngineType{engineType},
	})
}

func setSupportedServicePlans(region *openapicommonv1beta1.Region) {
	if region == nil {
		return
	}
	planInfoMap := make(map[openapicommonv1beta1.ServicePlan]*openapicommonv1beta1.ServicePlanInfo)
	for _, info := range region.GetSupportedServicePlans() {
		if info == nil {
			continue
		}
		planInfoMap[info.GetServicePlan()] = &openapicommonv1beta1.ServicePlanInfo{
			ServicePlan:          info.GetServicePlan(),
			DisplayName:          servicePlanDisplayName(info.GetServicePlan()),
			SupportedEngineTypes: lo.Uniq(info.GetSupportedEngineTypes()),
		}
	}
	region.SupportedServicePlans = lo.Map(region.GetServicePlans(), func(
		plan openapicommonv1beta1.ServicePlan,
		_ int,
	) *openapicommonv1beta1.ServicePlanInfo {
		info, ok := planInfoMap[plan]
		if !ok {
			return &openapicommonv1beta1.ServicePlanInfo{
				ServicePlan:          plan,
				DisplayName:          servicePlanDisplayName(plan),
				SupportedEngineTypes: defaultSupportedEngineTypes(plan),
			}
		}
		return info
	})
}

func cvtOpenAPIEngineType(engineType globalcommonv1.Region_EngineType) openapicommonv1beta1.EngineType {
	switch engineType {
	case globalcommonv1.Region_POSTGRESQL_COMPATIBLE:
		return openapicommonv1beta1.EngineType_POSTGRESQL_COMPATIBLE
	case globalcommonv1.Region_ENGINE_TYPE_UNSPECIFIED, globalcommonv1.Region_MYSQL_COMPATIBLE:
		return openapicommonv1beta1.EngineType_MYSQL_COMPATIBLE
	default:
		return openapicommonv1beta1.EngineType_ENGINE_TYPE_UNSPECIFIED
	}
}

func defaultSupportedEngineTypes(plan openapicommonv1beta1.ServicePlan) []openapicommonv1beta1.EngineType {
	switch plan {
	case openapicommonv1beta1.ServicePlan_Starter, openapicommonv1beta1.ServicePlan_Essential:
		return []openapicommonv1beta1.EngineType{openapicommonv1beta1.EngineType_MYSQL_COMPATIBLE}
	default:
		return nil
	}
}

func servicePlanDisplayName(val openapicommonv1beta1.ServicePlan) string {
	entry := val.Descriptor().Values().ByNumber(val.Number())
	if entry == nil {
		return ""
	}

	options := entry.Options()
	if proto.HasExtension(options, openapicommonv1beta1.E_DisplayName) {
		ext := proto.GetExtension(options, openapicommonv1beta1.E_DisplayName)
		if str, ok := ext.(string); ok {
			return str
		}
	}

	return string(entry.Name())
}

func (r serverImpl) ListCloudProviders(
	ctx context.Context,
	req *openapiserverlessv1beta1.ListCloudProvidersRequest,
) (*openapiserverlessv1beta1.ListCloudProvidersResponse, error) {
	resp, err := r.ListRegions(ctx, &openapiserverlessv1beta1.ListRegionsRequest{})
	if err != nil {
		return nil, err
	}
	ps := make([]openapicommonv1beta1.Region_CloudProvider, 0, len(resp.Regions))
	for _, region := range resp.Regions {
		ps = append(ps, region.CloudProvider)
	}
	ps = lo.Uniq(ps)

	return &openapiserverlessv1beta1.ListCloudProvidersResponse{CloudProviders: ps}, nil
}
