package billing_svc

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/facade"
	"github.com/tidbcloud/serverless-service/internal/domain/repo"
	"github.com/tidbcloud/serverless-service/internal/domain/service"
	"github.com/tidbcloud/serverless-service/internal/domain/vo"
	"github.com/tidbcloud/serverless-service/internal/metric"
	"go.uber.org/zap"
)

// InstanceCapacityMeterService meters active instance counts per org per hour.
// It runs every minute, checks the checkpoint, and meters the current hour only if
// it hasn't been metered yet. On any org error it aborts and waits for the next trigger.
type InstanceCapacityMeterService struct {
	clusterRepo    repo.ClusterRepo
	branchRepo     repo.BranchRepo
	billingFacade  facade.BillingFacade
	checkpointRepo repo.InstanceCapacityCheckpointRepo
}

func NewInstanceCapacityMeterService(
	clusterRepo repo.ClusterRepo,
	branchRepo repo.BranchRepo,
	billingFacade facade.BillingFacade,
	checkpointRepo repo.InstanceCapacityCheckpointRepo,
) *InstanceCapacityMeterService {
	return &InstanceCapacityMeterService{
		clusterRepo:    clusterRepo,
		branchRepo:     branchRepo,
		billingFacade:  billingFacade,
		checkpointRepo: checkpointRepo,
	}
}

// Execute meters one hour per invocation: the next unmetered hour after the checkpoint,
// or the current hour if checkpoint is already up to date. On any org error it aborts
// without advancing the checkpoint so the next trigger will retry.
func (s *InstanceCapacityMeterService) Execute(ctx context.Context) error {
	now := time.Now().UTC()
	currentHour := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.UTC).Unix()

	checkpoint, err := s.checkpointRepo.Load(ctx)
	if err != nil {
		return fmt.Errorf("load checkpoint: %w", err)
	}
	metric.InstanceCapacityCheckpointGauge.Set(float64(checkpoint))
	if checkpoint == currentHour {
		return nil
	}

	// Determine which hour to meter this round.
	// No checkpoint (0) → current hour; checkpoint behind → next hour after checkpoint.
	var hourToMeter int64
	if checkpoint == 0 {
		hourToMeter = currentHour
	} else {
		hourToMeter = checkpoint + 3600
		if hourToMeter > currentHour {
			return nil
		}
		if hourToMeter < currentHour {
			log.WithContextL(ctx).Warn("instance capacity meter: backfilling a missed hour using current instance counts",
				zap.Int64("hour", hourToMeter),
				zap.Int64("checkpoint", checkpoint),
				zap.Int64("current_hour", currentHour))
		}
	}

	contracts := config.Global().BizConfig.InstanceCapacityContracts

	for _, contract := range contracts {
		if err := s.meterOneOrg(ctx, contract, hourToMeter); err != nil {
			log.WithContextL(ctx).Error("instance capacity meter: aborted due to error",
				zap.Uint64("org_id", contract.OrgID),
				zap.Int64("hour", hourToMeter),
				zap.Error(err))
			return err
		}
	}

	if err := s.checkpointRepo.Save(ctx, hourToMeter); err != nil {
		log.WithContextL(ctx).Error("instance capacity meter: failed to save checkpoint", zap.Error(err))
		return err
	}

	log.WithContextL(ctx).Info("instance capacity meter: checkpoint updated",
		zap.Int64("checkpoint", hourToMeter),
		zap.String("checkpoint_time", time.Unix(hourToMeter, 0).UTC().Format(time.RFC3339)))
	return nil
}

func (s *InstanceCapacityMeterService) meterOneOrg(
	ctx context.Context,
	contract config.InstanceCapacityContract,
	hourTimestamp int64,
) error {
	logger := log.WithContextL(ctx).With(zap.Uint64("org_id", contract.OrgID))

	clusterCount, err := s.clusterRepo.CountClusters(ctx, &repo.ClusterFilter{
		TenantIds: []uint64{contract.OrgID},
	})
	if err != nil {
		return fmt.Errorf("count clusters: %w", err)
	}

	var branchCount int64
	if !contract.ExcludeBranches {
		branchCount, err = s.branchRepo.CountBranchesByOrgID(ctx, contract.OrgID)
		if err != nil {
			return fmt.Errorf("count branches: %w", err)
		}
	}

	totalCount := clusterCount + branchCount
	if totalCount == 0 {
		logger.Info("no active instances, skip")
		return nil
	}

	freeTierLimit := constant.InstanceCapacityFreeLimit
	if contract.FreeTierOverride > 0 {
		freeTierLimit = contract.FreeTierOverride
	}
	tiers := service.SplitIntoTiers(totalCount, freeTierLimit)
	if len(tiers) == 0 {
		return nil
	}

	skuBuilder := &vo.SkuNameBuilder{}
	reports := make([]*facade.BillingReport, 0, len(tiers))
	for skuType, count := range tiers {
		if skuType == constant.SkuTypeInstanceCapacityFree {
			continue
		}
		costID := fmt.Sprintf("%d/instance-capacity/%s/%d", contract.OrgID, string(skuType), hourTimestamp)
		reports = append(reports, &facade.BillingReport{
			CostID:        costID,
			TenantID:      contract.OrgID,
			Sku:           skuBuilder.Build(skuType),
			Cost:          count,
			CostTimeStamp: hourTimestamp,
			Labels: map[string]string{
				constant.BillingLabelTenantID:   fmt.Sprintf("%d", contract.OrgID),
				constant.BillingLabelMetricName: "instance_capacity_count",
				constant.BillingLabelServerless: "true",
				constant.BillingLabelWorker:     "instanceCapacityMeter",
				"total_count":                   fmt.Sprintf("%d", totalCount),
				"cluster_count":                 fmt.Sprintf("%d", clusterCount),
				"branch_count":                  fmt.Sprintf("%d", branchCount),
			},
		})
	}

	if err := s.billingFacade.Measure(ctx, reports); err != nil {
		return fmt.Errorf("measure instance capacity: %w", err)
	}

	logger.Info("metered instance capacity",
		zap.Int64("total", totalCount),
		zap.Int64("clusters", clusterCount),
		zap.Int64("branches", branchCount),
		zap.Int("tiers", len(tiers)),
		zap.Any("reports", reports))
	return nil
}
