package mysql

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

func (r *Repository) ListGiftConfigs(ctx context.Context, query resourcedomain.ListGiftConfigsQuery) ([]resourcedomain.GiftConfig, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, query.AppCode)
	query.AppCode = appcode.FromContext(ctx)
	query = normalizeGiftConfigListQuery(query)
	where, args := giftConfigWhereSQL(query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM gift_configs gc
		JOIN resources r ON r.app_code = gc.app_code AND r.resource_id = gc.resource_id
		`+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	// 后台列表与 App 礼物面板共用此顺序，必须按大排序值优先，避免两端看到不同礼物次序。
	rows, err := r.db.QueryContext(ctx, giftConfigSelectSQL()+where+`
		ORDER BY gc.sort_order DESC, gc.created_at_ms DESC
		LIMIT ? OFFSET ?`,
		append(args, query.PageSize, resourceOffset(query.Page, query.PageSize))...,
	)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()

	nowMs := time.Now().UnixMilli()
	items := make([]resourcedomain.GiftConfig, 0, query.PageSize)
	giftIDs := make([]string, 0, query.PageSize)
	for rows.Next() {
		item, err := scanGiftConfig(rows)
		if err != nil {
			return nil, 0, err
		}
		items = append(items, item)
		giftIDs = append(giftIDs, item.GiftID)
	}
	if err := rows.Err(); err != nil {
		return nil, 0, err
	}
	if len(items) == 0 {
		return items, total, nil
	}

	regionsByGiftID, err := r.listGiftConfigRegionIDsByGiftIDs(ctx, r.db, giftIDs)
	if err != nil {
		return nil, 0, err
	}
	pricesByGiftID, err := r.latestGiftPrices(ctx, r.db, giftIDs, nowMs)
	if err != nil {
		return nil, 0, err
	}
	for index := range items {
		items[index].RegionIDs = regionsByGiftID[items[index].GiftID]
		if price, ok := pricesByGiftID[items[index].GiftID]; ok {
			applyGiftPriceToConfig(&items[index], price)
		}
	}
	return items, total, nil
}

// GetGiftCatalogVersion returns the newest configuration timestamp that can change the App gift panel.
//
// Current owner rows cover create/update/status changes. Resource/GiftConfig outbox creation time is also part
// of the token because hard deletes or resource type changes can remove the row that would otherwise prove
// the cache is stale.
func (r *Repository) GetGiftCatalogVersion(ctx context.Context, appCode string) (int64, error) {
	if r == nil || r.db == nil {
		return 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, appCode)
	appCode = appcode.FromContext(ctx)

	var version int64
	err := r.db.QueryRowContext(ctx, `
		SELECT COALESCE(MAX(version_ms), 0)
		FROM (
			SELECT MAX(updated_at_ms) AS version_ms
			FROM gift_configs
			WHERE app_code = ?
			UNION ALL
			SELECT MAX(updated_at_ms) AS version_ms
			FROM wallet_gift_prices
			WHERE app_code = ?
			UNION ALL
			SELECT MAX(updated_at_ms) AS version_ms
			FROM gift_type_configs
			WHERE app_code = ?
			UNION ALL
			SELECT MAX(updated_at_ms) AS version_ms
			FROM gift_config_regions
			WHERE app_code = ?
			UNION ALL
			SELECT MAX(r.updated_at_ms) AS version_ms
			FROM resources r
			WHERE r.app_code = ?
			  AND r.resource_type = ?
			UNION ALL
			SELECT MAX(created_at_ms) AS version_ms
			FROM wallet_outbox
			WHERE app_code = ?
			  AND event_type IN ('GiftConfigChanged', 'ResourceChanged')
		) gift_catalog_versions`,
		appCode, appCode, appCode, appCode, appCode, resourcedomain.TypeGift, appCode,
	).Scan(&version)
	return version, err
}

func (r *Repository) getGiftConfig(ctx context.Context, giftID string) (resourcedomain.GiftConfig, error) {
	row := r.db.QueryRowContext(ctx, giftConfigSelectSQL()+`WHERE gc.app_code = ? AND gc.gift_id = ?`, appcode.FromContext(ctx), giftID)
	gift, err := scanGiftConfig(row)
	if errors.Is(err, sql.ErrNoRows) {
		return resourcedomain.GiftConfig{}, xerr.New(xerr.NotFound, "gift config not found")
	}
	if err != nil {
		return resourcedomain.GiftConfig{}, err
	}
	price, priceErr := r.latestGiftPrice(ctx, r.db, gift.GiftID, time.Now().UnixMilli())
	if priceErr == nil {
		applyGiftPriceToConfig(&gift, price)
	}
	gift.RegionIDs, err = r.listGiftConfigRegionIDs(ctx, r.db, gift.GiftID)
	if err != nil {
		return resourcedomain.GiftConfig{}, err
	}
	return gift, nil
}

func (r *Repository) getGiftConfigForUpdateTx(ctx context.Context, tx *sql.Tx, giftID string) (resourcedomain.GiftConfig, error) {
	row := tx.QueryRowContext(ctx, giftConfigSelectSQL()+`WHERE gc.app_code = ? AND gc.gift_id = ? FOR UPDATE`, appcode.FromContext(ctx), giftID)
	gift, err := scanGiftConfig(row)
	if errors.Is(err, sql.ErrNoRows) {
		return resourcedomain.GiftConfig{}, xerr.New(xerr.NotFound, "gift config not found")
	}
	if err != nil {
		return resourcedomain.GiftConfig{}, err
	}

	price, priceErr := r.latestGiftPrice(ctx, tx, gift.GiftID, time.Now().UnixMilli())
	if priceErr == nil {
		applyGiftPriceToConfig(&gift, price)
	}
	gift.RegionIDs, err = r.listGiftConfigRegionIDs(ctx, tx, gift.GiftID)
	if err != nil {
		return resourcedomain.GiftConfig{}, err
	}
	return gift, nil
}

func (r *Repository) replaceGiftConfigRegionsTx(ctx context.Context, tx *sql.Tx, giftID string, regionIDs []int64, nowMs int64) error {
	if _, err := tx.ExecContext(ctx, `DELETE FROM gift_config_regions WHERE app_code = ? AND gift_id = ?`, appcode.FromContext(ctx), giftID); err != nil {
		return err
	}
	for _, regionID := range regionIDs {
		if regionID < 0 {
			return xerr.New(xerr.InvalidArgument, "gift region is invalid")
		}
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO gift_config_regions (
			   app_code, gift_id, region_id, created_at_ms, updated_at_ms
			 ) VALUES (?, ?, ?, ?, ?)`,
			appcode.FromContext(ctx), giftID, regionID, nowMs, nowMs,
		); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) listGiftConfigRegionIDs(ctx context.Context, querier sqlQuerier, giftID string) ([]int64, error) {
	rows, err := querier.QueryContext(ctx, `
			SELECT region_id
		FROM gift_config_regions
		WHERE app_code = ? AND gift_id = ?
		ORDER BY region_id ASC`,
		appcode.FromContext(ctx), giftID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	regionIDs := make([]int64, 0)
	for rows.Next() {
		var regionID int64
		if err := rows.Scan(&regionID); err != nil {
			return nil, err
		}
		regionIDs = append(regionIDs, regionID)
	}
	return regionIDs, rows.Err()
}

func (r *Repository) listGiftConfigRegionIDsByGiftIDs(ctx context.Context, querier sqlQuerier, giftIDs []string) (map[string][]int64, error) {
	regionsByGiftID := make(map[string][]int64, len(giftIDs))
	if len(giftIDs) == 0 {
		return regionsByGiftID, nil
	}
	for _, giftID := range giftIDs {
		// 每个礼物都预置空切片，保持旧版逐个查询时“无区域行返回空列表”的响应语义。
		regionsByGiftID[giftID] = []int64{}
	}
	args := append([]any{appcode.FromContext(ctx)}, stringAnyArgs(giftIDs)...)
	rows, err := querier.QueryContext(ctx, `
			SELECT gift_id, region_id
			FROM gift_config_regions
			WHERE app_code = ? AND gift_id IN (`+placeholders(len(giftIDs))+`)
			ORDER BY gift_id ASC, region_id ASC`,
		args...,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	for rows.Next() {
		var giftID string
		var regionID int64
		if err := rows.Scan(&giftID, &regionID); err != nil {
			return nil, err
		}
		regionsByGiftID[giftID] = append(regionsByGiftID[giftID], regionID)
	}
	return regionsByGiftID, rows.Err()
}

func scanGiftConfig(scanner scanTarget) (resourcedomain.GiftConfig, error) {
	var gift resourcedomain.GiftConfig
	var resource resourcedomain.Resource
	var scopesJSON string
	var effectTypesJSON string
	if err := scanner.Scan(
		&gift.AppCode,
		&gift.GiftID,
		&gift.ResourceID,
		&gift.Status,
		&gift.Name,
		&gift.SortOrder,
		&gift.PresentationJSON,
		&gift.GiftTypeCode,
		&gift.VIPLevel,
		&gift.EffectiveFromMS,
		&gift.EffectiveToMS,
		&effectTypesJSON,
		&gift.CreatedByUserID,
		&gift.UpdatedByUserID,
		&gift.CreatedAtMS,
		&gift.UpdatedAtMS,
		&resource.AppCode,
		&resource.ResourceID,
		&resource.ResourceCode,
		&resource.ResourceType,
		&resource.Name,
		&resource.Status,
		&resource.Grantable,
		&resource.ManagerGrantEnabled,
		&resource.GrantStrategy,
		&resource.WalletAssetType,
		&resource.WalletAssetAmount,
		&resource.PriceType,
		&resource.CoinPrice,
		&resource.GiftPointAmount,
		&scopesJSON,
		&resource.AssetURL,
		&resource.PreviewURL,
		&resource.AnimationURL,
		&resource.MetadataJSON,
		&resource.SortOrder,
		&resource.CreatedByUserID,
		&resource.UpdatedByUserID,
		&resource.CreatedAtMS,
		&resource.UpdatedAtMS,
	); err != nil {
		return resourcedomain.GiftConfig{}, err
	}
	resource.UsageScopes = parseStringArray(scopesJSON)
	gift.Resource = resource
	gift.EffectTypes = parseStringArray(effectTypesJSON)

	gift.CPRelationType = cpRelationTypeFromPresentationJSON(gift.PresentationJSON)
	return gift, nil
}

func giftConfigSelectSQL() string {
	return `
		SELECT gc.app_code, gc.gift_id, gc.resource_id, gc.status, gc.name, gc.sort_order,
		       COALESCE(CAST(gc.presentation_json AS CHAR), '{}'),
		       COALESCE(gc.gift_type_code, 'normal'), COALESCE(gc.vip_level, 0),
		       COALESCE(gc.effective_from_ms, 0), COALESCE(gc.effective_to_ms, 0),
		       COALESCE(CAST(gc.effect_types_json AS CHAR), '[]'),
		       gc.created_by_user_id, gc.updated_by_user_id, gc.created_at_ms, gc.updated_at_ms,
		       ` + resourceColumnsWithAlias("r") + `
		FROM gift_configs gc
		JOIN resources r ON r.app_code = gc.app_code AND r.resource_id = gc.resource_id
		`
}

func giftConfigWhereSQL(query resourcedomain.ListGiftConfigsQuery) (string, []any) {
	where := `WHERE gc.app_code = ? AND r.status <> 'deleted'`
	args := []any{query.AppCode}
	if query.ActiveOnly {
		where += ` AND gc.status = 'active' AND r.status = 'active' AND r.resource_type = 'gift'
			AND (COALESCE(gc.effective_from_ms, 0) = 0 OR gc.effective_from_ms <= ?)
			AND (COALESCE(gc.effective_to_ms, 0) = 0 OR gc.effective_to_ms > ?)`
		nowMs := time.Now().UnixMilli()
		args = append(args, nowMs, nowMs)
	} else if query.Status != "" {
		where += ` AND gc.status = ?`
		args = append(args, query.Status)
	}
	if query.Keyword != "" {
		like := "%" + query.Keyword + "%"
		where += ` AND (gc.gift_id LIKE ? OR gc.name LIKE ? OR r.resource_code LIKE ?)`
		args = append(args, like, like, like)
	}
	if query.GiftTypeCode != "" {
		where += ` AND gc.gift_type_code = ?`
		args = append(args, query.GiftTypeCode)
	}
	if query.FilterRegion {
		where += ` AND EXISTS (
			SELECT 1
			FROM gift_config_regions gcr
			WHERE gcr.app_code = gc.app_code
			  AND gcr.gift_id = gc.gift_id
			  AND (gcr.region_id = 0 OR gcr.region_id = ?)
		)`
		args = append(args, query.RegionID)
	}
	return where, args
}

func normalizeGiftConfigListQuery(query resourcedomain.ListGiftConfigsQuery) resourcedomain.ListGiftConfigsQuery {
	query.Status = strings.ToLower(strings.TrimSpace(query.Status))
	query.Keyword = strings.TrimSpace(query.Keyword)

	if strings.TrimSpace(query.GiftTypeCode) != "" {
		query.GiftTypeCode = resourcedomain.NormalizeGiftTypeCode(query.GiftTypeCode)
	}
	// 房间礼物面板会一次预加载完整配置；礼物列表单独允许 500，避免复用资源列表 100 上限导致面板天然少数据。
	query.Page, query.PageSize = normalizePageWithMax(query.Page, query.PageSize, 500)
	return query
}
