package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
	"time"
)

// GetMyVip 返回用户当前 VIP 状态；过期只影响 active 投影，不直接修改历史事实。
func (r *Repository) GetMyVip(ctx context.Context, userID int64) (ledger.UserVip, error) {
	if r == nil || r.db == nil {
		return ledger.UserVip{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	vip, err := r.queryUserVip(ctx, r.db, userID, time.Now().UnixMilli(), false)
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.UserVip{UserID: userID}, nil
	}
	return vip, err
}

// ListVipPackages 在一个 repeatable-read 快照中返回最终 VIP 状态和全部 active 套餐。
// state、program、level、benefit 与奖励资源必须使用同一事务，否则后台改版瞬间可能返回跨版本组合。
func (r *Repository) ListVipPackages(ctx context.Context, userID int64) (ledger.VipState, []ledger.VipLevel, error) {
	if r == nil || r.db == nil {
		return ledger.VipState{}, nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	nowMS := time.Now().UnixMilli()
	// VIP 读取不再删除过期 trial-card equipment；快照查询会忽略失效卡片，
	// 物理残留统一由 Wallet 单写者清理，避免读取套餐时与 appearance/gift-panel 争写锁。
	if err := r.ensureTieredVipResourceState(ctx, userID, nowMS); err != nil {
		return ledger.VipState{}, nil, err
	}
	tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelRepeatableRead})
	if err != nil {
		return ledger.VipState{}, nil, err
	}
	defer func() { _ = tx.Rollback() }()
	config, err := r.requireVipProgramConfig(ctx, tx)
	if err != nil {
		return ledger.VipState{}, nil, err
	}
	state, err := r.getVipStateFromSnapshot(ctx, tx, userID, nowMS, config)
	if err != nil {
		return ledger.VipState{}, nil, err
	}
	levels, err := r.listVipLevelsFromSnapshot(ctx, tx, config, nowMS, state.PaidVip.Level)
	return state, levels, err
}

// ListAdminVipLevels 返回同一 repeatable-read 快照中的 program 与全部动态等级。
func (r *Repository) ListAdminVipLevels(ctx context.Context) (ledger.VipProgramConfig, []ledger.VipLevel, error) {
	if r == nil || r.db == nil {
		return ledger.VipProgramConfig{}, nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelRepeatableRead})
	if err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	defer func() { _ = tx.Rollback() }()
	config, err := r.requireVipProgramConfig(ctx, tx)
	if err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	levels, err := r.listAdminVipLevelsFromSnapshot(ctx, tx, config, time.Now().UnixMilli())
	return config, levels, err
}

// listAdminVipLevelsFromSnapshot 在调用者事务内装配完整后台等级读模型。
// 必须先耗尽并关闭 level rows，再查询每级 rewards/benefits；MySQL 单连接不允许在活动 rows 上嵌套查询。
func (r *Repository) listAdminVipLevelsFromSnapshot(ctx context.Context, querier vipSnapshotQuerier, config ledger.VipProgramConfig, nowMS int64) ([]ledger.VipLevel, error) {
	rows, err := querier.QueryContext(ctx, `
		SELECT level, name, status, price_coin, duration_ms, reward_resource_group_id,
		       required_recharge_coin_amount, sort_order, created_at_ms, updated_at_ms
		FROM vip_levels
		WHERE app_code = ? AND level BETWEEN 1 AND ?
		ORDER BY level ASC`,
		appcode.FromContext(ctx), config.LevelCount,
	)
	if err != nil {
		return nil, err
	}
	levels, err := r.scanAndCloseVipLevels(rows, int(config.LevelCount))
	if err != nil {
		return nil, err
	}
	for index := range levels {
		level := &levels[index]
		if level.RewardResourceGroupID > 0 {
			level.RewardItems, err = r.vipRewardItemsWithQuery(ctx, querier, level.RewardResourceGroupID, nowMS+level.DurationMS)
			if err != nil {
				return nil, err
			}
		}
		level.Benefits, err = r.listVipLevelBenefits(ctx, querier, level.Level, false)
		if err != nil {
			return nil, err
		}
		level.ConfigVersion = config.ConfigVersion
	}
	return levels, nil
}

func (r *Repository) listVipLevelsFromSnapshot(ctx context.Context, querier vipSnapshotQuerier, config ledger.VipProgramConfig, nowMS int64, currentLevel int32) ([]ledger.VipLevel, error) {
	rows, err := querier.QueryContext(ctx, `
		SELECT level, name, status, price_coin, duration_ms, reward_resource_group_id,
		       required_recharge_coin_amount, sort_order, created_at_ms, updated_at_ms
		FROM vip_levels
		WHERE app_code = ? AND status = ? AND level BETWEEN 1 AND ?
		ORDER BY sort_order DESC, level ASC`,
		appcode.FromContext(ctx), ledger.VipStatusActive, config.LevelCount,
	)
	if err != nil {
		return nil, err
	}
	levels, err := r.scanAndCloseVipLevels(rows, int(config.LevelCount))
	if err != nil {
		return nil, err
	}
	for index := range levels {
		level := &levels[index]
		// VIP 累充门槛已下线，旧字段只作为兼容输出保留；购买资格只看当前 VIP 等级和配置启停。
		level.UserRechargeCoinAmount = 0
		level.RechargeGateRequired = false
		level.RequiredRechargeCoinAmount = 0
		level.CanPurchase = currentLevel == 0 || level.Level >= currentLevel
		if !level.CanPurchase {
			level.PurchaseLockedReason = "current_vip_higher"
		}
		level.RewardItems, err = r.vipRewardItemsWithQuery(ctx, querier, level.RewardResourceGroupID, nowMS+level.DurationMS)
		if err != nil {
			return nil, err
		}
		level.Benefits, err = r.listVipLevelBenefits(ctx, querier, level.Level, true)
		if err != nil {
			return nil, err
		}
		level.ConfigVersion = config.ConfigVersion
	}
	return levels, nil
}

// scanAndCloseVipLevels 把主游标生命周期收敛在一个函数内，返回后调用方才能安全执行同事务子查询。
func (r *Repository) scanAndCloseVipLevels(rows *sql.Rows, capacity int) ([]ledger.VipLevel, error) {
	defer rows.Close()
	levels := make([]ledger.VipLevel, 0, capacity)
	for rows.Next() {
		level, err := r.scanVipLevel(rows)
		if err != nil {
			return nil, err
		}
		levels = append(levels, level)
	}
	return levels, rows.Err()
}

func (r *Repository) scanVipLevel(scanner scanTarget) (ledger.VipLevel, error) {
	var level ledger.VipLevel
	err := scanner.Scan(
		&level.Level,
		&level.Name,
		&level.Status,
		&level.PriceCoin,
		&level.DurationMS,
		&level.RewardResourceGroupID,
		&level.RequiredRechargeCoinAmount,
		&level.SortOrder,
		&level.CreatedAtMS,
		&level.UpdatedAtMS,
	)
	// 线上历史行可能还保存 VIP6+ 累充门槛；扫描后立即归零，保证后台、App 列表和购买事务读到同一个“无门槛”事实。
	level.RechargeGateRequired = false
	level.RequiredRechargeCoinAmount = 0
	return level, err
}

func (r *Repository) ensureUserVipForUpdate(ctx context.Context, tx *sql.Tx, userID int64, nowMs int64) (ledger.UserVip, error) {
	if _, err := tx.ExecContext(ctx, `
		INSERT IGNORE INTO user_vip_memberships (
		   app_code, user_id, level, name, status, program_type, config_version,
		   started_at_ms, expires_at_ms, created_at_ms, updated_at_ms
		 )
		 SELECT ?, ?, 0, '', 'inactive', program_type, config_version, 0, 0, ?, ?
		 FROM vip_program_configs WHERE app_code = ?`,
		appcode.FromContext(ctx), userID, nowMs, nowMs, appcode.FromContext(ctx),
	); err != nil {
		return ledger.UserVip{}, err
	}
	return r.queryUserVip(ctx, tx, userID, nowMs, true)
}

func (r *Repository) queryUserVip(ctx context.Context, querier interface {
	QueryRowContext(context.Context, string, ...any) *sql.Row
}, userID int64, nowMs int64, lock bool) (ledger.UserVip, error) {
	query := `
		SELECT m.user_id, m.level, COALESCE(v.name, m.name), m.started_at_ms, m.expires_at_ms,
		       m.updated_at_ms, m.program_type, m.config_version
		FROM user_vip_memberships m
		LEFT JOIN vip_levels v ON v.app_code = m.app_code AND v.level = m.level
		WHERE m.app_code = ? AND m.user_id = ?`
	if lock {
		query += ` FOR UPDATE`
	}
	var vip ledger.UserVip
	if err := querier.QueryRowContext(ctx, query, appcode.FromContext(ctx), userID).Scan(
		&vip.UserID,
		&vip.Level,
		&vip.Name,
		&vip.StartedAtMS,
		&vip.ExpiresAtMS,
		&vip.UpdatedAtMS,
		&vip.ProgramType,
		&vip.ConfigVersion,
	); err != nil {
		return ledger.UserVip{}, err
	}
	vip.Active = vip.Level > 0 && vip.ExpiresAtMS > nowMs
	if !vip.Active {
		vip.Level = 0
		vip.Name = ""
	}
	return vip, nil
}

func (r *Repository) vipRewardItems(ctx context.Context, groupID int64, expiresAtMS int64) ([]ledger.VipRewardItem, error) {
	return r.vipRewardItemsWithQuery(ctx, r.db, groupID, expiresAtMS)
}

// vipRewardItemsWithQuery 必须沿用调用者给定的 DB/事务；套餐列表通过它确保资源组也来自同一配置快照。
func (r *Repository) vipRewardItemsWithQuery(ctx context.Context, querier vipBenefitQuerier, groupID int64, expiresAtMS int64) ([]ledger.VipRewardItem, error) {
	if groupID <= 0 {
		return nil, nil
	}
	items, err := r.listResourceGroupItemsWithQuery(ctx, querier, groupID)
	if err != nil {
		return nil, err
	}
	rewards := make([]ledger.VipRewardItem, 0, len(items))
	for _, item := range items {
		if resourcedomain.NormalizeGroupItemType(item.ItemType) != resourcedomain.GroupItemTypeResource {
			continue
		}
		rewards = append(rewards, ledger.VipRewardItem{
			ResourceID:   item.Resource.ResourceID,
			ResourceCode: item.Resource.ResourceCode,
			ResourceType: item.Resource.ResourceType,
			Name:         item.Resource.Name,
			Quantity:     item.Quantity,
			ExpiresAtMS:  expiresAtMS,
			AssetURL:     item.Resource.AssetURL,
			PreviewURL:   item.Resource.PreviewURL,
			AnimationURL: item.Resource.AnimationURL,
		})
	}
	return rewards, nil
}

func (r *Repository) rewardItemsByGrant(ctx context.Context, tx *sql.Tx, grantID string, expiresAtMS int64) ([]ledger.VipRewardItem, error) {
	if grantID == "" {
		return nil, nil
	}
	rows, err := tx.QueryContext(ctx, `
		SELECT resource_id, resource_snapshot_json, quantity, result_type
		FROM resource_grant_items
		WHERE app_code = ? AND grant_id = ?
		ORDER BY grant_item_id ASC`,
		appcode.FromContext(ctx), grantID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := make([]ledger.VipRewardItem, 0)
	for rows.Next() {
		var resourceID int64
		var snapshotJSON string
		var quantity int64
		var resultType string
		if err := rows.Scan(&resourceID, &snapshotJSON, &quantity, &resultType); err != nil {
			return nil, err
		}
		item := ledger.VipRewardItem{ResourceID: resourceID, Quantity: quantity}
		switch resultType {
		case resourcedomain.ResultEntitlement:
			var resource resourcedomain.Resource
			if err := json.Unmarshal([]byte(snapshotJSON), &resource); err != nil {
				return nil, xerr.New(xerr.LedgerConflict, "vip reward resource snapshot is invalid")
			}
			item.ResourceID = resource.ResourceID
			item.ResourceCode = resource.ResourceCode
			item.ResourceType = resource.ResourceType
			item.Name = resource.Name
			item.ExpiresAtMS = expiresAtMS
			item.AssetURL = resource.AssetURL
			item.PreviewURL = resource.PreviewURL
			item.AnimationURL = resource.AnimationURL
		case resourcedomain.ResultWalletCredit:
			var snapshot struct {
				WalletAssetType   string `json:"wallet_asset_type"`
				WalletAssetAmount int64  `json:"wallet_asset_amount"`
			}
			if err := json.Unmarshal([]byte(snapshotJSON), &snapshot); err != nil || snapshot.WalletAssetType == "" {
				return nil, xerr.New(xerr.LedgerConflict, "vip reward wallet snapshot is invalid")
			}
			item.ResourceCode = snapshot.WalletAssetType
			item.ResourceType = "wallet_asset"
			item.Name = snapshot.WalletAssetType
			if snapshot.WalletAssetAmount > 0 {
				item.Quantity = snapshot.WalletAssetAmount
			}
		default:
			return nil, xerr.New(xerr.LedgerConflict, "vip reward result type is invalid")
		}
		items = append(items, item)
	}
	return items, rows.Err()
}
