package region

import (
	"context"
	"fmt"

	"github.com/pingcap/log"
	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/emptypb"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	clusterrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func (s *service) GetRegionByLogicalName(
	ctx context.Context,
	req *globalngv1.GetRegionByLogicalNameRequest,
) (*entity.Region, bizerr.BizError) {
	if req.RegionLogicalName == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region logical name is required")
	}

	return s.regionRepo.GetByLogicalName(ctx, req.RegionLogicalName)
}

func (s *service) ListRegions(
	ctx context.Context,
	req *globalngv1.ListRegionsRequest,
) ([]*entity.Region, bizerr.BizError) {
	filter := regionrepo.Filter{
		ServicePlan:   req.ServicePlan,
		CloudProvider: req.CloudProviderId,
		TenantId:      req.OrganizationId,
	}
	if req.State != globalcommonv1.Region_STATE_UNSPECIFIED {
		filter.State = req.State.String()
	}
	regions, err := s.regionRepo.ListRegions(ctx, filter)
	if err != nil {
		return nil, err
	}

	return regions, nil
}

func (s *service) RegisterRegion(
	ctx context.Context,
	req *globalngv1.RegisterRegionRequest,
) (*entity.Region, bizerr.BizError) {
	if req.GetRegion() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region is required")
	}
	r := &entity.Region{
		Region: req.GetRegion(),
	}
	servicePlans := normalizeServicePlans(r.Region)
	if len(servicePlans) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "service plan is required")
	}
	r.ServicePlan = servicePlans[0]

	// check if cloud provider exists
	cp, err := s.regionRepo.GetCloudProviderById(ctx, r.GetCloudProviderId(), r.GetServicePlan())
	if err != nil {
		return nil, err
	}

	if err := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		// check if region already exists
		rs, err := s.regionRepo.ListRegions(ctx, regionrepo.Filter{
			RegionId:    r.GetId(),
			ServicePlan: servicePlans[0],
		})
		if err != nil {
			return err
		}

		// set logical name if not set
		// use region id as logical name
		if r.LogicalName == "" {
			r.LogicalName = r.GetId().String()
		}

		_, exists := lo.Find(rs, func(each *entity.Region) bool {
			return each.GetLogicalName() == r.GetLogicalName()
		})

		if exists {
			return bizerr.New(bizerr.ErrCodeRecordExist, "region logical %s already exists", r.GetLogicalName())
		}

		// check if region is in the cloud provider
		if cp.State != globalcommonv1.CloudProvider_ACTIVE {
			return bizerr.New(bizerr.ErrCodeParams, "inactive cloud provider %s", cp.GetId())
		}

		if cp.Id != r.CloudProviderId {
			return bizerr.New(bizerr.ErrCodeParams, "region %s is not in cloud provider %s", r.GetId(), cp.GetId())
		}

		// save region
		err = s.regionRepo.SaveRegion(ctx, r)
		if err != nil {
			return err
		}

		log.Debug("region register success", zap.String("region", r.GetLogicalName()))
		// save region ops auditlog setting for byoc
		if req.GetRegion().GetOpsAuditLogSetting() != nil &&
			lo.Contains(servicePlans, globalcommonv1.ServicePlan_BYOC) {
			opsAuditLogSetting := req.GetRegion().GetOpsAuditLogSetting()

			// fulfill with default value
			if opsAuditLogSetting.GetLogRotation() == nil {
				opsAuditLogSetting.LogRotation = &globalcommonv1.RegionOpsAuditLogSetting_LogRotation{
					FileSizeLimitInMb:     10,
					RotationIntervalInMin: 1440,
				}
			}

			_, err = s.UpdateRegionOpsAuditLogSetting(ctx, &globalngv1.UpdateRegionOpsAuditLogSettingRequest{
				Region: &globalngv1.RegionIdentifier{
					RegionIdentifier: &globalngv1.RegionIdentifier_RegionLogicalName{
						RegionLogicalName: r.GetLogicalName(),
					},
				},
				OpsAuditLogSetting: opsAuditLogSetting,
			})
			if err != nil {
				return err
			}
		}

		if len(req.GetRegionProviderIdentities()) == 0 {
			return bizerr.New(bizerr.ErrCodeParams, "region provider identities is required")
		} else {
			// Validate that at most one identity is marked as default
			defaultCount := 0
			for _, identity := range req.GetRegionProviderIdentities() {
				if identity.GetIsDefault() {
					defaultCount++
				}
			}
			if defaultCount > 1 {
				return bizerr.New(bizerr.ErrCodeParams, "at most one provider identity can be marked as default per region")
			}

			for _, identity := range req.GetRegionProviderIdentities() {
				newIdentityId := s.idGeneratorAdapter.MustNew()
				identity.Id = newIdentityId
				err = s.regionProviderIdentityRepo.Save(ctx, &entity.RegionProviderIdentity{
					RegionProviderIdentity: identity,
					RegionId:               r.RegionID,
				})
				if err != nil {
					tilog.Error(ctx, "failed to save provider identity",
						zap.Uint64("region_id", r.RegionID),
						zap.String("region_logical_name", r.GetLogicalName()),
						zap.Uint64("provider_identity_id", newIdentityId),
						zap.String("provider_identity", identity.GetIdentity()),
						zap.Error(err),
					)
					return bizerr.New(bizerr.GeneralInternalErrorCode, "failed to save provider identity")
				}

				// If is_default is true, create/update DefaultProviderIdentity with TenantId=0
				if identity.GetIsDefault() {
					defaultIdentity := &entity.DefaultProviderIdentity{
						ProviderId:  r.ProviderID,
						ServicePlan: r.ServicePlan.String(),
						RegionId:    r.RegionID,
						TenantId:    0, // Default for all tenants
						Identity:    identity.GetIdentity(),
					}
					err = s.regionProviderIdentityRepo.SaveDefaultProviderIdentity(ctx, defaultIdentity)
					if err != nil {
						return bizerr.WrapError(err, "failed to save default provider identity")
					}
				}
			}
		}
		return nil
	}); err != nil {
		return nil, bizerr.WrapError(err, "register region failed")
	}

	return r, nil
}

func (s *service) UpdateRegion(
	ctx context.Context,
	req *globalngv1.UpdateRegionRequest,
) (*entity.Region, bizerr.BizError) {
	if req.GetRegion() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region is required")
	}

	if req.GetRegion().GetLogicalName() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region logical name is required")
	}

	r := &entity.Region{
		Region: req.GetRegion(),
	}
	servicePlans := normalizeServicePlans(r.Region)
	if len(servicePlans) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "service plan is required")
	}
	r.RegionWhiteLists = normalizeRegionWhiteLists(r.Region, servicePlans)
	r.ServicePlan = servicePlans[0]

	if err := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		// check if cloud provider exists
		cp, err := s.regionRepo.GetCloudProviderById(ctx, req.GetRegion().GetCloudProviderId(), servicePlans[0])
		if err != nil {
			return err
		}

		// check if region exists
		re, err := s.regionRepo.GetByLogicalName(ctx, req.GetRegion().GetLogicalName())
		if err != nil {
			return err
		}

		// check if region is in the cloud provider
		if re.CloudProviderId != cp.GetId() {
			return bizerr.New(bizerr.ErrCodeParams, "region %s is not in cloud provider %s", re.GetId(), cp.GetId())
		}

		// save region
		err = s.regionRepo.SaveRegion(ctx, r)
		if err != nil {
			return err
		}

		if req.DefaultProviderIdentities != nil {
			existingDefaults, err := s.regionProviderIdentityRepo.ListDefaultProviderIdentitiesByRegion(
				ctx,
				re.ProviderID,
				re.RegionID,
			)
			if err != nil {
				return err
			}

			existingTenantSeen := make(map[uint64]struct{}, len(existingDefaults))
			for _, item := range existingDefaults {
				if _, exists := existingTenantSeen[item.TenantId]; exists {
					continue
				}
				existingTenantSeen[item.TenantId] = struct{}{}
				err = s.regionProviderIdentityRepo.DeleteDefaultProviderIdentityByRegionAndTenant(
					ctx,
					re.ProviderID,
					re.RegionID,
					item.TenantId,
				)
				if err != nil {
					return err
				}
			}

			allowedPlans := make(map[globalcommonv1.ServicePlan]struct{}, len(servicePlans))
			for _, plan := range servicePlans {
				allowedPlans[plan] = struct{}{}
			}

			tenantPlanSeen := make(map[string]struct{}, len(req.DefaultProviderIdentities))
			for _, item := range req.DefaultProviderIdentities {
				if item.GetIdentity() == "" {
					return bizerr.New(bizerr.ErrCodeParams, "default provider identity is required")
				}
				if item.GetServicePlan() == globalcommonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED {
					return bizerr.New(bizerr.ErrCodeParams, "service_plan is required in default provider identities")
				}
				if _, ok := allowedPlans[item.GetServicePlan()]; !ok {
					return bizerr.New(bizerr.ErrCodeParams,
						"service_plan %s in default provider identities is not supported by region",
						item.GetServicePlan().String())
				}
				key := fmt.Sprintf("%d#%s", item.GetTenantId(), item.GetServicePlan().String())
				if _, exists := tenantPlanSeen[key]; exists {
					return bizerr.New(bizerr.ErrCodeParams,
						"duplicate tenant_id %d and service_plan %s in default provider identities",
						item.GetTenantId(), item.GetServicePlan().String())
				}
				tenantPlanSeen[key] = struct{}{}

				_, err = s.regionProviderIdentityRepo.GetProviderIdentityByIdentity(ctx, re.RegionID, item.GetIdentity())
				if err != nil {
					return bizerr.WrapError(err, "provider identity %s not found in region", item.GetIdentity())
				}

				err = s.regionProviderIdentityRepo.SaveDefaultProviderIdentity(ctx, &entity.DefaultProviderIdentity{
					ProviderId:  re.ProviderID,
					ServicePlan: item.GetServicePlan().String(),
					RegionId:    re.RegionID,
					TenantId:    item.GetTenantId(),
					Identity:    item.GetIdentity(),
				})
				if err != nil {
					return err
				}
			}
		}

		return nil
	}); err != nil {
		return nil, bizerr.WrapError(err, "register region failed")
	}

	return r, nil
}

func normalizeServicePlans(region *globalcommonv1.Region) []globalcommonv1.ServicePlan {
	if region == nil {
		return nil
	}
	servicePlans := lo.Map(region.GetRegionWhiteLists(), func(wl *globalcommonv1.RegionWhiteList, _ int) globalcommonv1.ServicePlan {
		return wl.GetServicePlan()
	})
	servicePlans = lo.Filter(servicePlans, func(plan globalcommonv1.ServicePlan, _ int) bool {
		return plan != globalcommonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED
	})
	if len(servicePlans) > 0 {
		return lo.Uniq(servicePlans)
	}
	if region.GetServicePlan() != globalcommonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED {
		servicePlans = append(servicePlans, region.GetServicePlan())
	}
	return lo.Uniq(servicePlans)
}

func normalizeRegionWhiteLists(
	region *globalcommonv1.Region,
	servicePlans []globalcommonv1.ServicePlan,
) []*globalcommonv1.RegionWhiteList {
	if region == nil {
		return nil
	}
	if len(region.GetRegionWhiteLists()) > 0 {
		out := make([]*globalcommonv1.RegionWhiteList, 0, len(servicePlans))
		for _, plan := range servicePlans {
			for _, wl := range region.GetRegionWhiteLists() {
				if wl.GetServicePlan() != plan {
					continue
				}
				out = append(out, &globalcommonv1.RegionWhiteList{
					ServicePlan:     wl.GetServicePlan(),
					IsPublic:        wl.GetIsPublic(),
					OpenedTenantIds: lo.Uniq(append([]uint64(nil), wl.GetOpenedTenantIds()...)),
				})
				break
			}
		}
		if len(out) > 0 {
			return out
		}
	}

	if len(servicePlans) == 0 {
		return nil
	}
	out := make([]*globalcommonv1.RegionWhiteList, 0, len(servicePlans))
	for _, plan := range servicePlans {
		out = append(out, &globalcommonv1.RegionWhiteList{
			ServicePlan:     plan,
			IsPublic:        region.GetIsPublic(),
			OpenedTenantIds: lo.Uniq(append([]uint64(nil), region.GetOpenedTenantIds()...)),
		})
	}
	return out
}

func (s *service) DeleteRegion(
	ctx context.Context,
	req *globalngv1.DeleteRegionRequest,
) (*emptypb.Empty, bizerr.BizError) {
	// check if region exists
	r, err := s.regionRepo.GetByLogicalName(ctx, req.GetRegionLogicalName())
	if err != nil {
		return nil, err
	}

	if req.GetCloudProviderId() != 0 && r.CloudProviderId != req.GetCloudProviderId() {
		tilog.Warn(
			ctx,
			"region provider not match with request cloud provider",
			zap.String("region", req.GetRegionLogicalName()),
			zap.String("request provicedr", req.GetCloudProviderId().String()),
		)
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"region %s is not in cloud provider %s",
			req.GetRegionLogicalName(),
			req.GetCloudProviderId().String(),
		)
	}

	if r.IsPublic {
		return nil, bizerr.New(bizerr.ErrCodeParams, "public region %s can not be deleted", req.GetRegionLogicalName())
	}

	// check if region is used
	clusters, err := s.clusterRepo.Query(ctx, &clusterrepo.Filter{
		CloudProviderNames: []entity.CloudProviderName{r.CloudProviderId},
		RegionNames:        []entity.RegionName{r.GetId()},
		LogicalRegionNames: []string{r.GetLogicalName()},
		ShowDeleted:        false,
	})
	if err != nil {
		return nil, err
	}

	for _, c := range clusters {
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"region %s is used by cluster %d",
			req.GetRegionLogicalName(),
			c.ClusterId,
		)
	}

	// delete region
	err = s.regionRepo.DeleteRegionByLogicalName(ctx, r.GetLogicalName())
	if err != nil {
		return nil, err
	}

	return &emptypb.Empty{}, nil
}

func (s *service) GetRegionByClusterId(
	ctx context.Context,
	req *globalngv1.GetRegionByClusterIdRequest,
) (*entity.Region, bizerr.BizError) {
	if req.GetClusterId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster id is required")
	}

	cluster, err := s.clusterRepo.Get(ctx, req.GetClusterId(), true)
	if err != nil {
		return nil, err
	}
	if cluster == nil {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "cluster not found")
	}

	return s.regionRepo.Get(ctx, cluster.RegionId)
}

func (s *service) SelectRegionByOrgId(
	ctx context.Context,
	req *globalngv1.SelectRegionByOrgIdRequest,
) (*entity.Region, bizerr.BizError) {
	if req.GetOrganizationId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "organization id is required")
	}

	r, err := s.regionRepo.SelectRegion(ctx, regionrepo.Filter{
		CloudProvider: req.GetCloudProviderId(),
		RegionId:      req.GetRegionId(),
		TenantId:      req.GetOrganizationId(),
		ServicePlan:   req.GetServicePlan(),
	})
	if err != nil {
		return nil, err
	}
	return r, nil
}

func (s *service) getRegionByIdentifier(
	ctx context.Context,
	identifier *globalngv1.RegionIdentifier,
) (*entity.Region, bizerr.BizError) {
	if identifier == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region identifier is nil")
	}

	var cloudRegion *entity.Region
	var err bizerr.BizError

	if identifier.GetRegionLogicalName() != "" {
		cloudRegion, err = s.regionRepo.GetByLogicalName(ctx, identifier.GetRegionLogicalName())
		if err != nil {
			return nil, err
		}
	} else if identifier.GetOrgRegion() != nil {
		cloudRegion, err = s.regionRepo.SelectRegion(ctx, regionrepo.Filter{
			CloudProvider: identifier.GetOrgRegion().GetCloudProviderId(),
			RegionId:      identifier.GetOrgRegion().GetRegionId(),
			TenantId:      identifier.GetOrgRegion().GetOrganizationId(),
			ServicePlan:   identifier.GetOrgRegion().GetServicePlan(),
		})
		if err != nil {
			return nil, err
		}
	}

	if cloudRegion == nil {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "region not found, %v", identifier)
	}
	return cloudRegion, nil
}
