package mysql

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"errors"
	"math"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
)

func luckyPoolScopeForStrategy(strategyVersion string) (string, error) {
	switch strings.ToLower(strings.TrimSpace(strategyVersion)) {
	case domain.StrategyFixedV2:
		return luckyBasePoolScopeType, nil
	case domain.StrategyDynamicV3:
		return luckyDynamicPoolScopeType, nil
	default:
		return "", xerr.New(xerr.InvalidArgument, "unsupported lucky gift strategy version")
	}
}

func luckyStrategyForPoolScope(scopeType string) (string, bool) {
	switch scopeType {
	case luckyBasePoolScopeType:
		return domain.StrategyFixedV2, true
	case luckyDynamicPoolScopeType:
		return domain.StrategyDynamicV3, true
	default:
		return "", false
	}
}

// AdjustLuckyGiftPoolBalance 是人工资金调整的唯一持久化入口。同一个事务先锁 adjustment_id，
// 再锁 app+strategy+pool 资金行；这样并发重试只能重放首次结果，不能在两个事务中重复增减余额。
func (r *Repository) AdjustLuckyGiftPoolBalance(ctx context.Context, command domain.PoolAdjustmentCommand, nowMS int64) (domain.PoolAdjustmentResult, error) {
	if r == nil || r.db == nil {
		return domain.PoolAdjustmentResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	appCode := appcode.Normalize(command.AppCode)
	poolID := luckyPoolID(command.PoolID)
	scopeType, err := luckyPoolScopeForStrategy(command.StrategyVersion)
	if err != nil {
		return domain.PoolAdjustmentResult{}, err
	}

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.PoolAdjustmentResult{}, err
	}
	defer func() { _ = tx.Rollback() }()

	var tokenEntropy [16]byte
	if _, err := rand.Read(tokenEntropy[:]); err != nil {
		return domain.PoolAdjustmentResult{}, xerr.New(xerr.Unavailable, "cannot generate lucky gift pool adjustment insert token")
	}
	insertToken := hex.EncodeToString(tokenEntropy[:])
	// 唯一键的 ON DUPLICATE 分支只做 no-op；随后比较库内首次随机令牌来判断本事务是否首写，
	// 因而既不依赖 clientFoundRows，也避免多个 duplicate INSERT 错误事务互相持锁形成死锁。
	_, err = tx.ExecContext(ctx, `
		INSERT INTO lucky_pool_adjustments (
			app_code, adjustment_id, pool_id, strategy_version, scope_type, direction,
			amount_coins, reason, operator_admin_id, request_id, insert_token,
			balance_before, balance_after, pool_snapshot_json, status, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, 'pending', ?, ?)
		ON DUPLICATE KEY UPDATE adjustment_id = adjustment_id`,
		appCode, command.AdjustmentID, poolID, command.StrategyVersion, scopeType, command.Direction,
		command.AmountCoins, command.Reason, command.OperatorAdminID, command.RequestID, insertToken, nowMS, nowMS,
	)
	if err != nil {
		return domain.PoolAdjustmentResult{}, err
	}

	var stored domain.PoolAdjustment
	var storedScopeType string
	var storedRequestID string
	var storedInsertToken string
	var storedStatus string
	var storedSnapshot sql.NullString
	if err := tx.QueryRowContext(ctx, `
		SELECT adjustment_id, app_code, pool_id, strategy_version, scope_type, direction,
		       amount_coins, reason, operator_admin_id, request_id, insert_token,
		       balance_before, balance_after, pool_snapshot_json, status, created_at_ms
		FROM lucky_pool_adjustments
		WHERE app_code = ? AND adjustment_id = ?
		FOR UPDATE`, appCode, command.AdjustmentID,
	).Scan(
		&stored.AdjustmentID, &stored.AppCode, &stored.PoolID, &stored.StrategyVersion, &storedScopeType, &stored.Direction,
		&stored.AmountCoins, &stored.Reason, &stored.OperatorAdminID, &storedRequestID, &storedInsertToken,
		&stored.BalanceBefore, &stored.BalanceAfter, &storedSnapshot, &storedStatus, &stored.CreatedAtMS,
	); err != nil {
		return domain.PoolAdjustmentResult{}, err
	}
	inserted := storedInsertToken == insertToken

	// request_id 允许随网络重试变化；其余字段共同定义资金命令，任何不一致都属于幂等冲突。
	if stored.PoolID != poolID || stored.StrategyVersion != command.StrategyVersion || storedScopeType != scopeType ||
		stored.Direction != command.Direction || stored.AmountCoins != command.AmountCoins || stored.Reason != command.Reason ||
		stored.OperatorAdminID != command.OperatorAdminID {
		return domain.PoolAdjustmentResult{}, xerr.New(xerr.IdempotencyConflict, "lucky gift pool adjustment payload does not match the existing adjustment_id")
	}
	if !inserted {
		if storedStatus != "applied" || !storedSnapshot.Valid {
			// 正常事务不会留下 pending 行；若人工修复或异常导入造成该状态，宁可阻断也不能猜测是否已经动账。
			return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool adjustment is not in an applied state")
		}
		var snapshot domain.PoolBalance
		if err := json.Unmarshal([]byte(storedSnapshot.String), &snapshot); err != nil {
			return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool adjustment snapshot is invalid")
		}
		if err := tx.Commit(); err != nil {
			return domain.PoolAdjustmentResult{}, err
		}
		return domain.PoolAdjustmentResult{Adjustment: stored, Pool: snapshot, IdempotentReplay: true}, nil
	}

	pool, err := r.getOrCreateAdjustmentPool(ctx, tx, appCode, poolID, command.StrategyVersion, scopeType, nowMS)
	if err != nil {
		return domain.PoolAdjustmentResult{}, err
	}
	stored.BalanceBefore = pool.Balance
	switch command.Direction {
	case domain.PoolAdjustmentIn:
		if pool.Balance > math.MaxInt64-command.AmountCoins || pool.ManualCreditTotal > math.MaxInt64-command.AmountCoins {
			return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool credit would overflow the ledger")
		}
		pool.Balance += command.AmountCoins
		pool.ManualCreditTotal += command.AmountCoins
	case domain.PoolAdjustmentOut:
		if pool.ManualDebitTotal > math.MaxInt64-command.AmountCoins || command.AmountCoins > pool.Balance || pool.Balance-command.AmountCoins < pool.ReserveFloor {
			return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool debit would cross the reserve floor")
		}
		pool.Balance -= command.AmountCoins
		pool.ManualDebitTotal += command.AmountCoins
	default:
		return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment direction is invalid")
	}
	stored.BalanceAfter = pool.Balance

	// 手工资金只更新 balance 和独立累计列；业务 total_in/total_out 仍只表达送礼入池与实际返奖。
	updateResult, err := tx.ExecContext(ctx, `
		UPDATE lucky_pools
		SET balance = ?, manual_credit_total = ?, manual_debit_total = ?, updated_at_ms = ?
		WHERE app_code = ? AND scope_type = ? AND scope_id = ?`,
		pool.Balance, pool.ManualCreditTotal, pool.ManualDebitTotal, nowMS, appCode, scopeType, poolID,
	)
	if err != nil {
		return domain.PoolAdjustmentResult{}, err
	}
	if rows, err := updateResult.RowsAffected(); err != nil {
		return domain.PoolAdjustmentResult{}, err
	} else if rows != 1 {
		return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool changed during adjustment")
	}

	snapshot := domain.PoolBalance{
		AppCode:           appCode,
		PoolID:            poolID,
		StrategyVersion:   command.StrategyVersion,
		Balance:           pool.Balance,
		ReserveFloor:      pool.ReserveFloor,
		AvailableBalance:  luckyAvailableBalance(pool.Balance, pool.ReserveFloor),
		TotalIn:           pool.TotalIn,
		TotalOut:          pool.TotalOut,
		ManualCreditTotal: pool.ManualCreditTotal,
		ManualDebitTotal:  pool.ManualDebitTotal,
		Materialized:      true,
		UpdatedAtMS:       nowMS,
	}
	snapshotJSON, err := json.Marshal(snapshot)
	if err != nil {
		return domain.PoolAdjustmentResult{}, err
	}
	finalizeResult, err := tx.ExecContext(ctx, `
		UPDATE lucky_pool_adjustments
		SET balance_before = ?, balance_after = ?, pool_snapshot_json = ?, status = 'applied', updated_at_ms = ?
		WHERE app_code = ? AND adjustment_id = ? AND status = 'pending'`,
		stored.BalanceBefore, stored.BalanceAfter, snapshotJSON, nowMS, appCode, command.AdjustmentID,
	)
	if err != nil {
		return domain.PoolAdjustmentResult{}, err
	}
	if rows, err := finalizeResult.RowsAffected(); err != nil {
		return domain.PoolAdjustmentResult{}, err
	} else if rows != 1 {
		return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool adjustment could not be finalized")
	}
	if err := tx.Commit(); err != nil {
		return domain.PoolAdjustmentResult{}, err
	}
	return domain.PoolAdjustmentResult{Adjustment: stored, Pool: snapshot}, nil
}

func (r *Repository) getOrCreateAdjustmentPool(ctx context.Context, tx *sql.Tx, appCode, poolID, strategyVersion, scopeType string, nowMS int64) (luckyPool, error) {
	// 人工资金不能凭任意 pool_id 创建孤儿账本；指定策略至少要有一个已发布不可变版本。
	var configured int
	err := tx.QueryRowContext(ctx, `
		SELECT 1
		FROM lucky_gift_rule_versions
		WHERE app_code = ? AND pool_id = ?
		  AND COALESCE(NULLIF(strategy_version, ''), 'fixed_v2') = ?
		LIMIT 1`, appCode, poolID, strategyVersion,
	).Scan(&configured)
	if errors.Is(err, sql.ErrNoRows) {
		return luckyPool{}, xerr.New(xerr.NotFound, "lucky gift pool strategy is not configured")
	}
	if err != nil {
		return luckyPool{}, err
	}
	if strategyVersion == domain.StrategyDynamicV3 {
		return r.getOrCreateLuckyPool(ctx, tx, appCode, scopeType, poolID, 0, 0, nowMS)
	}
	var existing luckyPool
	err = tx.QueryRowContext(ctx, `
		SELECT scope_type, scope_id, balance, reserve_floor, total_in, total_out,
		       manual_credit_total, manual_debit_total
		FROM lucky_pools
		WHERE app_code = ? AND scope_type = ? AND scope_id = ?
		FOR UPDATE`, appCode, scopeType, poolID,
	).Scan(
		&existing.ScopeType, &existing.ScopeID, &existing.Balance, &existing.ReserveFloor, &existing.TotalIn, &existing.TotalOut,
		&existing.ManualCreditTotal, &existing.ManualDebitTotal,
	)
	if err == nil {
		return existing, nil
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return luckyPool{}, err
	}

	// fixed_v2 首次人工调整仍需保持旧算法的启动水位。只读取最新 fixed_v2 规则的参考价格，
	// 不使用当前最新 dynamic_v3 配置，也不把旧固定账本从 0 重新解释。
	var referencePrice int64
	err = tx.QueryRowContext(ctx, `
		SELECT gift_price_reference
		FROM lucky_gift_rule_versions
		WHERE app_code = ? AND pool_id = ?
		  AND COALESCE(NULLIF(strategy_version, ''), 'fixed_v2') = 'fixed_v2'
		ORDER BY rule_version DESC
		LIMIT 1`, appCode, poolID,
	).Scan(&referencePrice)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return luckyPool{}, err
	}
	if errors.Is(err, sql.ErrNoRows) || referencePrice <= 0 {
		return luckyPool{}, xerr.New(xerr.NotFound, "fixed_v2 lucky gift pool is not configured")
	}
	if referencePrice > math.MaxInt64/(950+500+2_375) {
		return luckyPool{}, xerr.New(xerr.Conflict, "fixed_v2 lucky gift pool initial balance would overflow")
	}
	return r.getOrCreateLuckyPool(
		ctx, tx, appCode, scopeType, poolID,
		referencePrice*(950+500+2_375), referencePrice*(100+10+100), nowMS,
	)
}
