package appconfig

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"hyapp/pkg/giftlimits"
)

const (
	giftComboConfigGroup = "room-gift-combo"
	giftComboConfigKey   = "default"
)

const getGiftComboConfigSQL = `
	SELECT app_code, COALESCE(value, ''), is_deleted, updated_at_ms
	FROM admin_app_configs
	WHERE app_code IN ('', ?) AND ` + "`group`" + ` = ? AND ` + "`key`" + ` = ?
	ORDER BY app_code ASC`

// GiftComboConfig 是礼物面板下发给 Flutter 的运行时策略。
// 这里仅控制客户端如何封批和施加背压；账务数量、幂等和容量上限仍由 owner service 强制执行，
// 因而旧客户端、篡改客户端或缓存了旧配置的客户端都不能绕过服务端保护。
type GiftComboConfig struct {
	Enabled              bool  `json:"enabled"`
	APIVersion           int32 `json:"api_version"`
	WindowMS             int32 `json:"window_ms"`
	MaxGiftCountPerBatch int32 `json:"max_gift_count_per_batch"`
	MaxGiftUnitsPerBatch int32 `json:"max_gift_units_per_batch"`
	MaxInFlight          int32 `json:"max_in_flight"`
	MaxPendingBatches    int32 `json:"max_pending_batches"`
	RetryMaxAttempts     int32 `json:"retry_max_attempts"`
	RetryBaseDelayMS     int32 `json:"retry_base_delay_ms"`
	RolloutBasisPoints   int32 `json:"rollout_basis_points"`
	UpdatedAtMS          int64 `json:"updated_at_ms"`
}

type storedGiftComboConfig struct {
	appCode     string
	value       string
	isDeleted   bool
	updatedAtMS int64
}

// DefaultGiftComboConfig keeps the feature usable when no admin row exists. A production kill switch
// is an explicit scoped row with enabled=false, so a transient config read failure is never silently
// converted into a different batching policy by this method.
func DefaultGiftComboConfig() GiftComboConfig {
	return GiftComboConfig{
		Enabled:              true,
		APIVersion:           2,
		WindowMS:             300,
		MaxGiftCountPerBatch: 999,
		MaxGiftUnitsPerBatch: 5_000,
		MaxInFlight:          2,
		MaxPendingBatches:    20,
		RetryMaxAttempts:     5,
		RetryBaseDelayMS:     250,
		// 缺少后台配置时必须保持 0% 灰度。发布代码不能等价于全量打开财务主链路；
		// 运维确认 V2、Saga 和 MQ 指标后，再按 App 显式提高该值。
		RolloutBasisPoints: 0,
	}
}

// GiftComboConfig returns the app-scoped remote policy. A scoped tombstone intentionally suppresses
// the global baseline and falls back to code defaults, matching the admin_app_configs overlay contract.
func (r *MySQLReader) GiftComboConfig(ctx context.Context, appCode string) (GiftComboConfig, error) {
	if r == nil || r.db == nil {
		return GiftComboConfig{}, errors.New("app config reader is not configured")
	}

	appCode = normalizeAppCode(appCode)
	rows, err := r.db.QueryContext(ctx, getGiftComboConfigSQL, appCode, giftComboConfigGroup, giftComboConfigKey)
	if err != nil {
		return GiftComboConfig{}, err
	}
	defer rows.Close()

	var baseline *storedGiftComboConfig
	var scoped *storedGiftComboConfig
	for rows.Next() {
		var item storedGiftComboConfig
		if err := rows.Scan(&item.appCode, &item.value, &item.isDeleted, &item.updatedAtMS); err != nil {
			return GiftComboConfig{}, err
		}
		if strings.EqualFold(strings.TrimSpace(item.appCode), appCode) {
			copy := item
			scoped = &copy
			continue
		}
		copy := item
		baseline = &copy
	}
	if err := rows.Err(); err != nil {
		return GiftComboConfig{}, err
	}

	selected := baseline
	if scoped != nil {
		if scoped.isDeleted {
			return DefaultGiftComboConfig(), nil
		}
		selected = scoped
	}
	if selected == nil || selected.isDeleted || strings.TrimSpace(selected.value) == "" {
		return DefaultGiftComboConfig(), nil
	}
	config, err := ParseGiftComboConfig(selected.value)
	if err != nil {
		return GiftComboConfig{}, fmt.Errorf("parse gift combo config for app %s: %w", appCode, err)
	}
	config.UpdatedAtMS = selected.updatedAtMS
	return config, nil
}

// ParseGiftComboConfig overlays a stored JSON object on safe defaults. This allows newly introduced
// fields to acquire defaults while old admin rows remain valid during rolling upgrades.
func ParseGiftComboConfig(raw string) (GiftComboConfig, error) {
	config := DefaultGiftComboConfig()
	if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &config); err != nil {
		return GiftComboConfig{}, err
	}
	if config.APIVersion == 1 {
		// 早期灰度配置曾把 1 当作客户端降级开关。读取时一次性提升为当前 V2 契约，避免
		// 已有 admin_app_configs 行让礼物面板降级失败；新的配置写入仍只能提交 2。
		config.APIVersion = 2
	}
	return NormalizeGiftComboConfig(config)
}

// NormalizeGiftComboConfig rejects policies that could disable client backpressure or exceed the V2
// contract. Validation is deliberately shared with admin writes, but callers still enforce server caps.
func NormalizeGiftComboConfig(config GiftComboConfig) (GiftComboConfig, error) {
	if config.APIVersion == 0 {
		config.APIVersion = 2
	}
	if config.APIVersion != 2 {
		// api_version 是当前客户端契约标记，不是远程降级开关。旧 Flutter 自己固定调用 V1；
		// 新 Flutter 必须始终使用具备完整幂等结果重放语义的 V2。
		return GiftComboConfig{}, fmt.Errorf("api_version must be 2")
	}
	if config.WindowMS < 100 || config.WindowMS > 1_000 {
		return GiftComboConfig{}, fmt.Errorf("window_ms must be between 100 and 1000")
	}
	if config.MaxGiftCountPerBatch < 1 || config.MaxGiftCountPerBatch > 999 {
		return GiftComboConfig{}, fmt.Errorf("max_gift_count_per_batch must be between 1 and 999")
	}
	if config.MaxGiftUnitsPerBatch < 1 || int64(config.MaxGiftUnitsPerBatch) > giftlimits.MaxTotalGiftUnits {
		return GiftComboConfig{}, fmt.Errorf("max_gift_units_per_batch must be between 1 and %d", giftlimits.MaxTotalGiftUnits)
	}
	if config.MaxInFlight < 1 || config.MaxInFlight > 2 {
		return GiftComboConfig{}, fmt.Errorf("max_in_flight must be between 1 and 2")
	}
	if config.MaxPendingBatches < 1 || config.MaxPendingBatches > 50 {
		return GiftComboConfig{}, fmt.Errorf("max_pending_batches must be between 1 and 50")
	}
	if config.MaxPendingBatches < config.MaxInFlight {
		// Flutter allocates in-flight work from the same bounded batch queue. A smaller queue cannot
		// represent the advertised concurrency and would make the client silently rewrite policy.
		return GiftComboConfig{}, fmt.Errorf("max_pending_batches must be greater than or equal to max_in_flight")
	}
	if config.RetryMaxAttempts < 1 || config.RetryMaxAttempts > 10 {
		return GiftComboConfig{}, fmt.Errorf("retry_max_attempts must be between 1 and 10")
	}
	if config.RetryBaseDelayMS < 100 || config.RetryBaseDelayMS > 5_000 {
		return GiftComboConfig{}, fmt.Errorf("retry_base_delay_ms must be between 100 and 5000")
	}
	if config.RolloutBasisPoints < 0 || config.RolloutBasisPoints > 10_000 {
		return GiftComboConfig{}, fmt.Errorf("rollout_basis_points must be between 0 and 10000")
	}
	return config, nil
}

var _ interface {
	GiftComboConfig(context.Context, string) (GiftComboConfig, error)
} = (*MySQLReader)(nil)
