package resourcepool

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"maps"
	"strconv"
	"strings"
	"time"

	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"

	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"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	resourcepoolrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/resourcepoolcapacity"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

const (
	metadataBackfillCursorVersion = 1
	maxMetadataBackfillPageSize   = 500
)

type MetadataBackfillRequest struct {
	PageSize       int64
	Cursor         string
	OrganizationID uint64
	DryRun         bool
}

type MetadataBackfillReport struct {
	DryRun          bool  `json:"dry_run"`
	Scanned         int64 `json:"scanned"`
	TotalSize       int64 `json:"total_size"`
	Candidates      int64 `json:"candidates"`
	UpdatedRecords  int64 `json:"updated_records"`
	UpdatedFields   int64 `json:"updated_fields"`
	AlreadyComplete int64 `json:"already_complete"`

	DuplicateNames             int64    `json:"duplicate_names"`
	CIDROverlaps               int64    `json:"cidr_overlaps"`
	InvalidCIDRs               int64    `json:"invalid_cidrs"`
	InvalidCIDRResourcePoolIDs []uint64 `json:"invalid_cidr_resource_pool_ids,omitempty"`
	TopologyDrivenPools        int64    `json:"topology_driven_pools"`
	UnspecifiedCapacityMode    int64    `json:"unspecified_capacity_mode"`
	InvalidCapacityShapes      int64    `json:"invalid_capacity_shapes"`
	LegacyWriteComponents      int64    `json:"legacy_write_components"`
	RegionalFailures           int64    `json:"regional_failures"`
	UnsupportedPhases          int64    `json:"unsupported_phases"`

	NextCursor      string `json:"next_cursor,omitempty"`
	ScanComplete    bool   `json:"scan_complete"`
	ReadyForRollout bool   `json:"ready_for_rollout"`

	invalidCIDRResourcePoolIDs map[uint64]struct{}
}

func (r *MetadataBackfillReport) BlockingRecords() int64 {
	if r == nil {
		return 0
	}
	return r.DuplicateNames +
		r.CIDROverlaps +
		r.InvalidCIDRs +
		r.TopologyDrivenPools +
		r.UnspecifiedCapacityMode +
		r.InvalidCapacityShapes +
		r.RegionalFailures +
		r.UnsupportedPhases
}

type metadataBackfillCursor struct {
	Version   int       `json:"version"`
	CreatedAt time.Time `json:"created_at"`
	ID        uint64    `json:"id"`
}

func (s *serviceImpl) BackfillResourcePoolMetadata(
	ctx context.Context,
	request MetadataBackfillRequest,
) (*MetadataBackfillReport, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.BackfillResourcePoolMetadata")
	defer span.End()

	if request.PageSize <= 0 || request.PageSize > maxMetadataBackfillPageSize {
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"page size must be between 1 and %d",
			maxMetadataBackfillPageSize,
		)
	}
	cursor, hasCursor, err := parseMetadataBackfillCursor(request.Cursor)
	if err != nil {
		return nil, err
	}

	// Scan all owner types so shared pools receive common metadata too; name and
	// CIDR conflict checks below still apply only to their respective namespaces.
	filter := &resourcepoolrepo.Filter{
		PageSize: request.PageSize + 1,
	}
	if hasCursor {
		filter.Cursor = &cursor
	}
	if request.OrganizationID != 0 {
		filter.OwnerIds = []uint64{request.OrganizationID}
	}
	pools, total, queryErr := s.resourcePoolRepo.Query(ctx, filter)
	if queryErr != nil {
		return nil, bizerr.WrapError(queryErr, "query resource pools for metadata backfill")
	}

	report := &MetadataBackfillReport{
		DryRun:    request.DryRun,
		TotalSize: total,
	}
	if int64(len(pools)) > request.PageSize {
		pools = pools[:request.PageSize]
		last := pools[len(pools)-1]
		nextCursor, cursorErr := buildMetadataBackfillCursor(last)
		if cursorErr != nil {
			return nil, cursorErr
		}
		report.NextCursor = nextCursor
	}

	for _, pool := range pools {
		report.Scanned++
		if itemErr := s.backfillResourcePoolMetadata(ctx, pool, request.DryRun, report); itemErr != nil {
			return nil, itemErr
		}
	}
	report.ScanComplete = report.NextCursor == ""
	report.ReadyForRollout = request.Cursor == "" &&
		report.ScanComplete &&
		report.Candidates == 0 &&
		report.BlockingRecords() == 0
	return report, nil
}

func (s *serviceImpl) backfillResourcePoolMetadata(
	ctx context.Context,
	pool *entity.ResourcePool,
	dryRun bool,
	report *MetadataBackfillReport,
) bizerr.BizError {
	if pool == nil || pool.GetId() == 0 {
		return bizerr.New(bizerr.ErrCodeUnknown, "resource pool backfill item is incomplete")
	}

	patch := resourcepoolrepo.MetadataBackfillPatch{ResourcePoolID: pool.GetId()}
	if pool.NormalizedName == "" {
		normalizedName := entity.NormalizeResourcePoolName(pool.GetName())
		if normalizedName != "" {
			patch.NormalizedName = &normalizedName
		}
	}
	if pool.CanonicalCIDR == nil && strings.TrimSpace(pool.GetCidr()) != "" {
		start, end, err := entity.CanonicalIPv4Range(pool.GetCidr())
		if err != nil {
			// Non-BYOC CIDR is metadata only and must not block backfill.
			if usesCustomerCIDRNamespace(pool.ResourcePool) {
				report.addInvalidCIDR(pool.GetId())
			}
		} else {
			patch.CIDRStartIPv4 = start
			patch.CIDREndIPv4 = end
		}
	}
	if pool.GetOwnerType() == globalngv1.ResourcePool_ORGANIZATION &&
		pool.GetServicePlan() == globalcommonv1.ServicePlan_BYOC &&
		pool.GetVcpuLimitMode() == globalngv1.ResourcePool_INHERITED && pool.GetVcpusLimit() > 0 {
		// vCPU limit mode is part of the BYOC capacity policy, not common
		// metadata for other service plans.
		mode := globalngv1.ResourcePool_LIMITED
		patch.VcpuLimitMode = &mode
	}

	cloudRegion, regionErr := s.regionRepo.SelectRegion(ctx, regionrepo.Filter{
		CloudProvider: pool.GetProviderId(),
		RegionId:      pool.GetRegionId(),
		TenantId:      resourcePoolTenantId(pool.ResourcePool),
		ServicePlan:   pool.GetServicePlan(),
	})
	if regionErr != nil || cloudRegion == nil {
		report.RegionalFailures++
	} else {
		sharedPool, getErr := s.regionalCli.GetSharedPool(
			ctx,
			cloudRegion,
			&clusterngv1beta.GetSharedPoolRequest{SharedPoolId: strconv.FormatUint(pool.GetId(), 10)},
		)
		if getErr != nil || sharedPool == nil {
			report.RegionalFailures++
		} else {
			if pool.GetLifecycleState() == globalngv1.ResourcePool_LIFECYCLE_STATE_UNSPECIFIED {
				state, stateErr := backfillLifecycleFromRegionalPhase(sharedPool.GetPhase())
				if stateErr != nil {
					report.UnsupportedPhases++
				} else {
					patch.LifecycleState = &state
				}
			}
			if pool.GetServicePlan() == globalcommonv1.ServicePlan_BYOC {
				auditSharedPoolCapacity(sharedPool, report)
				switch sharedPool.GetCapacityMode() {
				case clusterngv1beta.CapacityMode_RCU_DRIVEN:
					if pool.GetCloudResourceTags() == nil {
						patch.BackfillTags = true
						patch.CloudResourceTags = maps.Clone(sharedPool.GetCloudResourceTags())
						if patch.CloudResourceTags == nil {
							patch.CloudResourceTags = map[string]string{}
						}
					}
				case clusterngv1beta.CapacityMode_TOPOLOGY_DRIVEN:
					report.TopologyDrivenPools++
				default:
					report.UnspecifiedCapacityMode++
				}
			}
		}
	}

	if dryRun {
		return s.previewResourcePoolMetadataBackfill(ctx, pool, &patch, report)
	}
	return s.applyResourcePoolMetadataBackfill(ctx, pool, &patch, report)
}

func backfillLifecycleFromRegionalPhase(
	phase clusterngv1beta.SharedPool_Phase,
) (globalngv1.ResourcePool_LifecycleState, bizerr.BizError) {
	switch phase {
	case clusterngv1beta.SharedPool_CREATING:
		return globalngv1.ResourcePool_CREATING, nil
	case clusterngv1beta.SharedPool_AVAILABLE:
		return globalngv1.ResourcePool_ACTIVE, nil
	case clusterngv1beta.SharedPool_MODIFYING:
		return globalngv1.ResourcePool_MODIFYING, nil
	default:
		return globalngv1.ResourcePool_LIFECYCLE_STATE_UNSPECIFIED,
			bizerr.New(bizerr.ErrCodeUnknown, "unsupported regional resource pool phase")
	}
}

func auditSharedPoolCapacity(
	sharedPool *clusterngv1beta.SharedPool,
	report *MetadataBackfillReport,
) {
	if _, err := resourcepoolcapacity.Project(sharedPool.GetComponentGroups()); err != nil {
		report.InvalidCapacityShapes++
	}
	for _, component := range sharedPool.GetComponentGroups() {
		if component.GetComponentType() == clusterngv1beta.ComponentType_TIFLASH_WRITE &&
			component.GetNodeQuota().GetNodeCount() > 0 {
			report.LegacyWriteComponents++
		}
	}
}

func (s *serviceImpl) previewResourcePoolMetadataBackfill(
	ctx context.Context,
	pool *entity.ResourcePool,
	patch *resourcepoolrepo.MetadataBackfillPatch,
	report *MetadataBackfillReport,
) bizerr.BizError {
	if err := s.removeConflictingBackfillFields(ctx, pool, patch, report); err != nil {
		return err
	}
	if metadataBackfillPatchEmpty(patch) {
		report.AlreadyComplete++
		return nil
	}
	report.Candidates++
	return nil
}

func (s *serviceImpl) applyResourcePoolMetadataBackfill(
	ctx context.Context,
	pool *entity.ResourcePool,
	patch *resourcepoolrepo.MetadataBackfillPatch,
	report *MetadataBackfillReport,
) bizerr.BizError {
	transactionErr := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		if usesOrganizationNameNamespace(pool.ResourcePool) {
			if err := s.resourcePoolRepo.LockOrganizationNamespace(ctx, pool.GetOwnerId()); err != nil {
				return err
			}
		}
		locked, err := s.resourcePoolRepo.LoadForUpdate(ctx, pool.GetId(), false)
		if err != nil {
			return err
		}
		if locked == nil ||
			locked.GetOwnerType() != pool.GetOwnerType() ||
			locked.GetOwnerId() != pool.GetOwnerId() {
			return bizerr.New(bizerr.ErrCodeRecordNotFound, "resource pool is no longer eligible for backfill")
		}
		if err := s.removeConflictingBackfillFields(ctx, locked, patch, report); err != nil {
			return err
		}
		if metadataBackfillPatchEmpty(patch) {
			report.AlreadyComplete++
			return nil
		}
		report.Candidates++
		result, err := s.resourcePoolRepo.BackfillMetadata(ctx, patch)
		if err != nil {
			return err
		}
		updatedFields := metadataBackfillUpdatedFields(result)
		if updatedFields == 0 {
			report.AlreadyComplete++
			return nil
		}
		report.UpdatedRecords++
		report.UpdatedFields += updatedFields
		return nil
	})
	if transactionErr == nil {
		return nil
	}
	var businessErr bizerr.BizError
	if errors.As(transactionErr, &businessErr) {
		return businessErr
	}
	return bizerr.WrapGormError(transactionErr, "backfill resource pool metadata")
}

func (s *serviceImpl) removeConflictingBackfillFields(
	ctx context.Context,
	pool *entity.ResourcePool,
	patch *resourcepoolrepo.MetadataBackfillPatch,
	report *MetadataBackfillReport,
) bizerr.BizError {
	if patch.NormalizedName != nil && usesOrganizationNameNamespace(pool.ResourcePool) {
		conflict, err := s.resourcePoolRepo.HasLiveNormalizedName(
			ctx,
			pool.GetOwnerId(),
			*patch.NormalizedName,
			pool.GetId(),
		)
		if err != nil {
			return err
		}
		if conflict {
			report.DuplicateNames++
			patch.NormalizedName = nil
		}
	}
	if len(patch.CIDRStartIPv4) == 4 && len(patch.CIDREndIPv4) == 4 &&
		usesCustomerCIDRNamespace(pool.ResourcePool) {
		conflict, invalidResourcePoolIDs, err := s.resourcePoolRepo.InspectLiveCIDROverlapForBackfill(
			ctx,
			pool.GetOwnerId(),
			pool.GetProviderId(),
			pool.GetRegionId(),
			patch.CIDRStartIPv4,
			patch.CIDREndIPv4,
			pool.GetId(),
		)
		if err != nil {
			return err
		}
		for _, resourcePoolID := range invalidResourcePoolIDs {
			report.addInvalidCIDR(resourcePoolID)
		}
		if conflict {
			report.CIDROverlaps++
		}
		if conflict || len(invalidResourcePoolIDs) > 0 {
			patch.CIDRStartIPv4 = nil
			patch.CIDREndIPv4 = nil
		}
	}
	return nil
}

func (r *MetadataBackfillReport) addInvalidCIDR(resourcePoolID uint64) {
	if r.invalidCIDRResourcePoolIDs == nil {
		r.invalidCIDRResourcePoolIDs = make(map[uint64]struct{})
	}
	if _, exists := r.invalidCIDRResourcePoolIDs[resourcePoolID]; exists {
		return
	}
	r.invalidCIDRResourcePoolIDs[resourcePoolID] = struct{}{}
	r.InvalidCIDRResourcePoolIDs = append(r.InvalidCIDRResourcePoolIDs, resourcePoolID)
	r.InvalidCIDRs++
}

func metadataBackfillPatchEmpty(patch *resourcepoolrepo.MetadataBackfillPatch) bool {
	return patch.NormalizedName == nil &&
		len(patch.CIDRStartIPv4) == 0 &&
		patch.LifecycleState == nil &&
		patch.VcpuLimitMode == nil &&
		!patch.BackfillTags
}

func metadataBackfillUpdatedFields(result resourcepoolrepo.MetadataBackfillResult) int64 {
	var count int64
	for _, updated := range []bool{
		result.NormalizedName,
		result.CIDR,
		result.LifecycleState,
		result.VcpuLimitMode,
		result.CloudResourceTags,
	} {
		if updated {
			count++
		}
	}
	return count
}

func parseMetadataBackfillCursor(
	value string,
) (resourcepoolrepo.Cursor, bool, bizerr.BizError) {
	if value == "" {
		return resourcepoolrepo.Cursor{}, false, nil
	}
	raw, err := base64.RawURLEncoding.DecodeString(value)
	if err != nil {
		return resourcepoolrepo.Cursor{}, false, bizerr.WrapErrorWithCode(
			err,
			bizerr.ErrCodeParams,
			"invalid metadata backfill cursor",
		)
	}
	var cursor metadataBackfillCursor
	if err := json.Unmarshal(raw, &cursor); err != nil {
		return resourcepoolrepo.Cursor{}, false, bizerr.WrapErrorWithCode(
			err,
			bizerr.ErrCodeParams,
			"invalid metadata backfill cursor",
		)
	}
	if cursor.Version != metadataBackfillCursorVersion || cursor.ID == 0 || cursor.CreatedAt.IsZero() {
		return resourcepoolrepo.Cursor{}, false, bizerr.New(
			bizerr.ErrCodeParams,
			"invalid metadata backfill cursor",
		)
	}
	return resourcepoolrepo.Cursor{CreatedAt: cursor.CreatedAt, ID: cursor.ID}, true, nil
}

func buildMetadataBackfillCursor(pool *entity.ResourcePool) (string, bizerr.BizError) {
	if pool == nil || pool.GetId() == 0 || pool.GetCreateTime() == nil ||
		!pool.GetCreateTime().IsValid() {
		return "", bizerr.New(bizerr.ErrCodeUnknown, "cannot build metadata backfill cursor")
	}
	raw, err := json.Marshal(metadataBackfillCursor{
		Version:   metadataBackfillCursorVersion,
		CreatedAt: pool.GetCreateTime().AsTime(),
		ID:        pool.GetId(),
	})
	if err != nil {
		return "", bizerr.WrapError(err, "marshal metadata backfill cursor")
	}
	return base64.RawURLEncoding.EncodeToString(raw), nil
}
