package shadowpool

import (
	"context"
	"errors"
	"strconv"

	"go.opentelemetry.io/otel"
	"gorm.io/gorm"

	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/apimiddleware/bizerror"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

const (
	defaultListPageSize = 50
	maxListPageSize     = 1000
)

var tracer = otel.Tracer("Domain.ShadowPool")

type Service interface {
	CreateShadowPool(ctx context.Context, req *tidbmgmtservicev1.CreateShadowPoolRequest) (*tidbmgmtservicev1.ShadowPool, error)
	GetShadowPool(ctx context.Context, req *tidbmgmtservicev1.GetShadowPoolRequest) (*tidbmgmtservicev1.ShadowPool, error)
	ListShadowPools(ctx context.Context, req *tidbmgmtservicev1.ListShadowPoolsRequest) (*tidbmgmtservicev1.ListShadowPoolsResponse, error)
	UpdateShadowPool(ctx context.Context, req *tidbmgmtservicev1.UpdateShadowPoolRequest) (*tidbmgmtservicev1.ShadowPool, error)
	DeleteShadowPool(ctx context.Context, req *tidbmgmtservicev1.DeleteShadowPoolRequest) (*tidbmgmtservicev1.DeleteShadowPoolResponse, error)
}

type service struct {
	shadowPoolRepo repository.ShadowPoolRepository
}

func NewService(shadowPoolRepo repository.ShadowPoolRepository) Service {
	return &service{shadowPoolRepo: shadowPoolRepo}
}

func (s *service) CreateShadowPool(ctx context.Context, req *tidbmgmtservicev1.CreateShadowPoolRequest) (*tidbmgmtservicev1.ShadowPool, error) {
	ctx, span := tracer.Start(ctx, "CreateShadowPool")
	defer span.End()

	pool := req.GetShadowPool()
	if pool == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool is required")
	}
	if err := validateCreateShadowPool(pool); err != nil {
		return nil, err
	}

	newPool := toEntityShadowPool(pool)
	if newPool.ID == 0 {
		newPool.ID = entity.GenClusterID()
	}
	if err := s.shadowPoolRepo.Create(ctx, newPool); err != nil {
		return nil, err
	}

	created, err := s.shadowPoolRepo.Get(ctx, newPool.ID)
	if err != nil {
		return nil, err
	}

	return fromEntityShadowPool(created), nil
}

func (s *service) GetShadowPool(ctx context.Context, req *tidbmgmtservicev1.GetShadowPoolRequest) (*tidbmgmtservicev1.ShadowPool, error) {
	ctx, span := tracer.Start(ctx, "GetShadowPool")
	defer span.End()

	if req.GetId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "id is required")
	}

	pool, err := s.shadowPoolRepo.Get(ctx, req.GetId())
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "shadow pool not found")
		}
		return nil, err
	}

	return fromEntityShadowPool(pool), nil
}

func (s *service) ListShadowPools(
	ctx context.Context,
	req *tidbmgmtservicev1.ListShadowPoolsRequest,
) (*tidbmgmtservicev1.ListShadowPoolsResponse, error) {
	ctx, span := tracer.Start(ctx, "ListShadowPools")
	defer span.End()

	filter := &repository.ShadowPoolFilter{
		IDs:            req.GetIds(),
		OrgIDs:         req.GetOrgIds(),
		ProjectIDs:     req.GetProjectIds(),
		ServicePlans:   req.GetServicePlans(),
		CloudProviders: req.GetCloudProviders(),
		Regions:        req.GetRegions(),
		OnlyEnabled:    req.GetOnlyEnabled(),
	}

	pools, err := s.shadowPoolRepo.List(ctx, filter)
	if err != nil {
		return nil, err
	}

	offset, err := parsePageToken(req.GetPageToken())
	if err != nil {
		return nil, err
	}

	pageSize := normalizePageSize(req.GetPageSize())
	if offset >= len(pools) {
		return &tidbmgmtservicev1.ListShadowPoolsResponse{ShadowPools: []*tidbmgmtservicev1.ShadowPool{}}, nil
	}

	end := min(offset+pageSize, len(pools))

	res := make([]*tidbmgmtservicev1.ShadowPool, 0, end-offset)
	for _, pool := range pools[offset:end] {
		res = append(res, fromEntityShadowPool(pool))
	}

	nextPageToken := ""
	if end < len(pools) {
		nextPageToken = strconv.Itoa(end)
	}

	return &tidbmgmtservicev1.ListShadowPoolsResponse{
		ShadowPools:   res,
		NextPageToken: nextPageToken,
	}, nil
}

func (s *service) UpdateShadowPool(ctx context.Context, req *tidbmgmtservicev1.UpdateShadowPoolRequest) (*tidbmgmtservicev1.ShadowPool, error) {
	ctx, span := tracer.Start(ctx, "UpdateShadowPool")
	defer span.End()

	update := req.GetShadowPool()
	if update == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool is required")
	}
	if update.GetId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool.id is required")
	}

	existing, err := s.shadowPoolRepo.Get(ctx, update.GetId())
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "shadow pool not found")
		}
		return nil, err
	}

	if err := validateImmutableFields(existing, update); err != nil {
		return nil, err
	}

	merged, err := mergeShadowPoolForUpdate(existing, update)
	if err != nil {
		return nil, err
	}

	if err := s.shadowPoolRepo.Update(ctx, merged); err != nil {
		return nil, err
	}

	latest, err := s.shadowPoolRepo.Get(ctx, merged.ID)
	if err != nil {
		return nil, err
	}

	return fromEntityShadowPool(latest), nil
}

func (s *service) DeleteShadowPool(
	ctx context.Context,
	req *tidbmgmtservicev1.DeleteShadowPoolRequest,
) (*tidbmgmtservicev1.DeleteShadowPoolResponse, error) {
	ctx, span := tracer.Start(ctx, "DeleteShadowPool")
	defer span.End()

	if req.GetId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "id is required")
	}

	if _, err := s.shadowPoolRepo.Get(ctx, req.GetId()); err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "shadow pool not found")
		}
		return nil, err
	}

	if err := s.shadowPoolRepo.Delete(ctx, req.GetId()); err != nil {
		return nil, err
	}

	return &tidbmgmtservicev1.DeleteShadowPoolResponse{}, nil
}

func validateCreateShadowPool(pool *tidbmgmtservicev1.ShadowPool) error {
	if pool.GetName() == "" {
		return bizerr.New(bizerr.ErrCodeParams, "shadow_pool.name is required")
	}
	if pool.GetOrganizationId() == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "shadow_pool.organization_id is required")
	}
	if pool.GetProjectId() == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "shadow_pool.project_id is required")
	}
	if pool.GetRegion() == "" {
		return bizerr.New(bizerr.ErrCodeParams, "shadow_pool.region is required")
	}
	if pool.GetCloudProvider() == "" {
		return bizerr.New(bizerr.ErrCodeParams, "shadow_pool.cloud_provider is required")
	}
	if pool.GetServicePlan() == "" {
		return bizerr.New(bizerr.ErrCodeParams, "shadow_pool.service_plan is required")
	}
	if pool.GetMaxCapacity() > 0 && pool.GetMinCapacity() > pool.GetMaxCapacity() {
		return bizerr.New(bizerr.ErrCodeParams, "shadow_pool.min_capacity cannot exceed shadow_pool.max_capacity")
	}

	return nil
}

func parsePageToken(token string) (int, error) {
	if token == "" {
		return 0, nil
	}
	offset, err := strconv.Atoi(token)
	if err != nil || offset < 0 {
		return 0, bizerr.New(bizerr.ErrCodeParams, "invalid page_token")
	}
	return offset, nil
}

func normalizePageSize(pageSize int32) int {
	if pageSize <= 0 {
		return defaultListPageSize
	}
	if pageSize > maxListPageSize {
		return maxListPageSize
	}
	return int(pageSize)
}

func validateImmutableFields(existing *entity.ShadowPool, update *tidbmgmtservicev1.ShadowPool) error {
	if update.GetOrganizationId() != 0 && update.GetOrganizationId() != existing.OrganizationID {
		return bizerr.New(bizerr.ErrCodeParams, "shadow_pool.organization_id is immutable")
	}
	if update.GetProjectId() != 0 && update.GetProjectId() != existing.ProjectID {
		return bizerr.New(bizerr.ErrCodeParams, "shadow_pool.project_id is immutable")
	}
	if update.GetRegion() != "" && update.GetRegion() != existing.Region {
		return bizerr.New(bizerr.ErrCodeParams, "shadow_pool.region is immutable")
	}
	return nil
}

func mergeShadowPoolForUpdate(existing *entity.ShadowPool, update *tidbmgmtservicev1.ShadowPool) (*entity.ShadowPool, error) {
	merged := *existing

	if update.GetName() != "" {
		merged.Name = update.GetName()
	}
	if update.GetCloudProvider() != "" {
		merged.CloudProvider = update.GetCloudProvider()
	}
	if update.GetServicePlan() != "" {
		merged.ServicePlan = update.GetServicePlan()
	}
	if update.GetPriority() != 0 {
		merged.Priority = update.GetPriority()
	}
	merged.Enabled = update.GetEnabled()
	if update.GetMinCapacity() > 0 {
		merged.MinCapacity = int(update.GetMinCapacity())
	}
	if update.GetMaxCapacity() > 0 {
		merged.MaxCapacity = int(update.GetMaxCapacity())
	}
	if update.GetAutoScalingConfig() != nil {
		merged.AutoScalingConfig = fromProtoAutoScalingConfig(update.GetAutoScalingConfig())
	}
	if update.GetInstanceDemand() != nil {
		merged.InstanceDemand = fromProtoInstanceDemand(update.GetInstanceDemand())
	}
	if update.GetInitSqls() != nil {
		merged.InitSQLs = update.GetInitSqls()
	}

	if merged.MaxCapacity > 0 && merged.MinCapacity > merged.MaxCapacity {
		return nil, bizerr.New(bizerr.ErrCodeParams, "shadow_pool.min_capacity cannot exceed shadow_pool.max_capacity")
	}

	return &merged, nil
}

func toEntityShadowPool(pool *tidbmgmtservicev1.ShadowPool) *entity.ShadowPool {
	return &entity.ShadowPool{
		ID:                pool.GetId(),
		Name:              pool.GetName(),
		OrganizationID:    pool.GetOrganizationId(),
		ProjectID:         pool.GetProjectId(),
		Region:            pool.GetRegion(),
		CloudProvider:     pool.GetCloudProvider(),
		ServicePlan:       pool.GetServicePlan(),
		Priority:          pool.GetPriority(),
		Enabled:           pool.GetEnabled(),
		MinCapacity:       int(pool.GetMinCapacity()),
		MaxCapacity:       int(pool.GetMaxCapacity()),
		AutoScalingConfig: fromProtoAutoScalingConfig(pool.GetAutoScalingConfig()),
		InstanceDemand:    fromProtoInstanceDemand(pool.GetInstanceDemand()),
		InitSQLs:          pool.GetInitSqls(),
	}
}

func fromEntityShadowPool(pool *entity.ShadowPool) *tidbmgmtservicev1.ShadowPool {
	if pool == nil {
		return nil
	}

	return &tidbmgmtservicev1.ShadowPool{
		Id:                pool.ID,
		Name:              pool.Name,
		OrganizationId:    pool.OrganizationID,
		ProjectId:         pool.ProjectID,
		Region:            pool.Region,
		CloudProvider:     pool.CloudProvider,
		ServicePlan:       pool.ServicePlan,
		Priority:          pool.Priority,
		Enabled:           pool.Enabled,
		MinCapacity:       int32(pool.MinCapacity),
		MaxCapacity:       int32(pool.MaxCapacity),
		AutoScalingConfig: toProtoAutoScalingConfig(pool.AutoScalingConfig),
		InstanceDemand:    toProtoInstanceDemand(pool.InstanceDemand),
		InitSqls:          pool.InitSQLs,
	}
}

func fromProtoAutoScalingConfig(cfg *tidbmgmtservicev1.AutoScalingConfig) entity.AutoScalingConfig {
	if cfg == nil {
		return entity.AutoScalingConfig{}
	}

	states := make([]entity.ShadowState, 0, len(cfg.GetFreeStates()))
	for _, state := range cfg.GetFreeStates() {
		if state == tidbmgmtservicev1.ShadowState_SHADOW_STATE_UNSPECIFIED {
			continue
		}
		states = append(states, entity.ShadowState(state))
	}

	return entity.AutoScalingConfig{
		MaxBatchSize:      int(cfg.GetMaxBatchSize()),
		TimeToFreeInHours: int(cfg.GetTimeToFreeInHours()),
		FreeStates:        states,
	}
}

func toProtoAutoScalingConfig(cfg entity.AutoScalingConfig) *tidbmgmtservicev1.AutoScalingConfig {
	states := make([]tidbmgmtservicev1.ShadowState, 0, len(cfg.FreeStates))
	for _, state := range cfg.FreeStates {
		states = append(states, tidbmgmtservicev1.ShadowState(state))
	}

	return &tidbmgmtservicev1.AutoScalingConfig{
		MaxBatchSize:      int32(cfg.MaxBatchSize),
		TimeToFreeInHours: int32(cfg.TimeToFreeInHours),
		FreeStates:        states,
	}
}

func fromProtoInstanceDemand(demand *tidbmgmtservicev1.InstanceDemand) entity.InstanceDemand {
	if demand == nil {
		return entity.InstanceDemand{}
	}

	return entity.InstanceDemand{
		MonthlySpendingLimit: demand.GetMonthlySpendingLimit(),
		DailySpendingLimit:   demand.GetDailySpendingLimit(),
		MinRcu:               demand.GetMinRcu(),
		MaxRcu:               demand.GetMaxRcu(),
	}
}

func toProtoInstanceDemand(demand entity.InstanceDemand) *tidbmgmtservicev1.InstanceDemand {
	return &tidbmgmtservicev1.InstanceDemand{
		MonthlySpendingLimit: demand.MonthlySpendingLimit,
		DailySpendingLimit:   demand.DailySpendingLimit,
		MinRcu:               demand.MinRcu,
		MaxRcu:               demand.MaxRcu,
	}
}
