package mysql

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

	"hyapp/pkg/appcode"
	"hyapp/services/wallet-service/internal/domain/ledger"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
)

type giftSnapshotQuerier interface {
	sqlRowQuerier
	sqlQuerier
}

// enrichResourceGroupGiftSnapshots resolves every gift ID backed by a gift resource at one instant. Pinning
// calls this with row locks, so config, price and presentation cannot change between validation and persistence.
func (r *Repository) enrichResourceGroupGiftSnapshots(ctx context.Context, querier giftSnapshotQuerier, items []resourcedomain.ResourceGroupItem, nowMS int64, lock bool) error {
	for index := range items {
		if resourcedomain.NormalizeGroupItemType(items[index].ItemType) != resourcedomain.GroupItemTypeResource ||
			resourcedomain.NormalizeResourceType(items[index].Resource.ResourceType) != resourcedomain.TypeGift {
			continue
		}
		configs, err := r.listPinnableGiftConfigs(ctx, querier, items[index].ResourceID, nowMS, lock)
		if err != nil {
			return err
		}
		items[index].GiftConfigs = configs
	}
	return nil
}

func (r *Repository) listPinnableGiftConfigs(ctx context.Context, querier giftSnapshotQuerier, resourceID int64, nowMS int64, lock bool) ([]resourcedomain.GiftConfig, error) {
	query := giftConfigSelectSQL() + `
		WHERE gc.app_code = ? AND gc.resource_id = ?
		  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 > ?)
		ORDER BY gc.gift_id ASC`
	if lock {
		query += ` FOR UPDATE`
	}
	rows, err := querier.QueryContext(ctx, query, appcode.FromContext(ctx), resourceID, nowMS, nowMS)
	if err != nil {
		return nil, err
	}
	configs := make([]resourcedomain.GiftConfig, 0)
	for rows.Next() {
		config, scanErr := scanGiftConfig(rows)
		if scanErr != nil {
			_ = rows.Close()
			return nil, scanErr
		}
		configs = append(configs, config)
	}
	if err := rows.Err(); err != nil {
		_ = rows.Close()
		return nil, err
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}

	for index := range configs {
		price, err := r.latestPinnableGiftPrice(ctx, querier, configs[index].GiftID, nowMS, lock)
		if err != nil {
			return nil, err
		}
		applyGiftPriceToConfig(&configs[index], price)
		regions, err := listPinnableGiftRegions(ctx, querier, configs[index].GiftID, lock)
		if err != nil {
			return nil, err
		}
		configs[index].RegionIDs = regions
	}
	return configs, nil
}

func (r *Repository) latestPinnableGiftPrice(ctx context.Context, querier giftSnapshotQuerier, giftID string, nowMS int64, lock bool) (giftPrice, error) {
	if !lock {
		return r.latestGiftPrice(ctx, querier, giftID, nowMS)
	}
	row := querier.QueryRowContext(ctx, `
		SELECT gift_id, price_version, COALESCE(charge_asset_type, 'COIN'), coin_price, gift_point_amount, heat_value
		FROM wallet_gift_prices
		WHERE app_code = ? AND gift_id = ? AND status = 'active' AND effective_at_ms <= ?
		ORDER BY effective_at_ms DESC, price_version DESC
		LIMIT 1
		FOR UPDATE`, appcode.FromContext(ctx), giftID, nowMS)
	var price giftPrice
	if err := row.Scan(&price.GiftID, &price.PriceVersion, &price.ChargeAssetType, &price.CoinPrice, &price.GiftPointAmount, &price.HeatValue); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return giftPrice{}, fmt.Errorf("gift %s has no active pin-time price", giftID)
		}
		return giftPrice{}, err
	}
	price.ChargeAssetType = ledger.NormalizeGiftChargeAssetType(price.ChargeAssetType)
	return price, nil
}

func listPinnableGiftRegions(ctx context.Context, querier giftSnapshotQuerier, giftID string, lock bool) ([]int64, error) {
	query := `
		SELECT region_id FROM gift_config_regions
		WHERE app_code = ? AND gift_id = ?
		ORDER BY region_id ASC`
	if lock {
		query += ` FOR UPDATE`
	}
	rows, err := querier.QueryContext(ctx, query, appcode.FromContext(ctx), giftID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	regions := make([]int64, 0)
	for rows.Next() {
		var regionID int64
		if err := rows.Scan(&regionID); err != nil {
			return nil, err
		}
		regions = append(regions, regionID)
	}
	return regions, rows.Err()
}

func validatePinnedGiftConfigs(appCode string, resource resourcedomain.Resource, configs []resourcedomain.GiftConfig) error {
	if resourcedomain.NormalizeResourceType(resource.ResourceType) != resourcedomain.TypeGift || len(configs) == 0 {
		return errors.New("gift resource has no pinnable gift config")
	}
	resourceHash, err := resourcedomain.ResourceContentHash(resource)
	if err != nil {
		return err
	}
	seen := make(map[string]struct{}, len(configs))
	for _, config := range configs {
		giftID := strings.TrimSpace(config.GiftID)
		if giftID == "" || strings.TrimSpace(config.Name) == "" || strings.TrimSpace(config.PriceVersion) == "" ||
			appcode.Normalize(config.AppCode) != appcode.Normalize(appCode) || config.ResourceID != resource.ResourceID ||
			resourcedomain.NormalizeStatus(config.Status) != resourcedomain.StatusActive || config.CoinPrice < 0 || config.HeatValue < 0 ||
			!ledger.ValidGiftChargeAssetType(config.ChargeAssetType) || !resourcedomain.ValidGiftTypeCode(config.GiftTypeCode) {
			return fmt.Errorf("gift %q pin-time config is invalid", giftID)
		}
		if _, exists := seen[giftID]; exists {
			return fmt.Errorf("gift %q is duplicated in pin-time config", giftID)
		}
		seen[giftID] = struct{}{}
		nestedHash, hashErr := resourcedomain.ResourceContentHash(config.Resource)
		if hashErr != nil || nestedHash != resourceHash || config.Resource.ResourceID != resource.ResourceID {
			return fmt.Errorf("gift %q resource snapshot does not match group item", giftID)
		}
		if config.EffectiveFromMS > 0 && config.EffectiveToMS > 0 && config.EffectiveToMS <= config.EffectiveFromMS {
			return fmt.Errorf("gift %q effective range is invalid", giftID)
		}
		var presentation map[string]any
		if err := json.Unmarshal([]byte(config.PresentationJSON), &presentation); err != nil || presentation == nil ||
			normalizeCPRelationType(config.CPRelationType) != cpRelationTypeFromPresentationJSON(config.PresentationJSON) {
			return fmt.Errorf("gift %q presentation snapshot is invalid", giftID)
		}
		if len(config.RegionIDs) == 0 {
			return fmt.Errorf("gift %q has no pin-time region", giftID)
		}
		for _, regionID := range config.RegionIDs {
			if regionID < 0 {
				return fmt.Errorf("gift %q has invalid region", giftID)
			}
		}
		for _, effectType := range config.EffectTypes {
			if !resourcedomain.ValidGiftEffectType(effectType) {
				return fmt.Errorf("gift %q has invalid effect type", giftID)
			}
		}
	}
	return nil
}

func validatePinnedEntitlementGiftSnapshot(appCode string, resource resourcedomain.Resource, snapshotHash string, snapshotJSON string) error {
	if resourcedomain.NormalizeResourceType(resource.ResourceType) != resourcedomain.TypeGift {
		if strings.TrimSpace(snapshotHash) != "" || strings.TrimSpace(snapshotJSON) != "" {
			return fmt.Errorf("non-gift pinned entitlement contains gift semantics")
		}
		return nil
	}
	if !validSHA256(snapshotHash) || strings.TrimSpace(snapshotJSON) == "" {
		return fmt.Errorf("pinned gift entitlement snapshot is incomplete")
	}
	var configs []resourcedomain.GiftConfig
	if err := json.Unmarshal([]byte(snapshotJSON), &configs); err != nil {
		return fmt.Errorf("decode pinned gift entitlement snapshot: %w", err)
	}
	contentHash, err := resourcedomain.GiftConfigsContentHash(configs)
	if err != nil || contentHash != snapshotHash {
		return fmt.Errorf("pinned gift entitlement snapshot integrity check failed")
	}
	if err := validatePinnedGiftConfigs(appCode, resource, configs); err != nil {
		return fmt.Errorf("pinned gift entitlement snapshot is invalid: %w", err)
	}
	return nil
}

func decodePinnedGiftConfigs(entitlement resourcedomain.UserResourceEntitlement) ([]resourcedomain.GiftConfig, error) {
	if err := validatePinnedEntitlementGiftSnapshot(entitlement.AppCode, entitlement.Resource, entitlement.GiftSnapshotHash, entitlement.GiftSnapshotJSON); err != nil {
		return nil, err
	}
	var configs []resourcedomain.GiftConfig
	if err := json.Unmarshal([]byte(entitlement.GiftSnapshotJSON), &configs); err != nil {
		return nil, err
	}
	return configs, nil
}

func pinnedGiftAvailableInRegion(config resourcedomain.GiftConfig, regionID int64) bool {
	for _, allowed := range config.RegionIDs {
		if allowed == 0 || allowed == regionID {
			return true
		}
	}
	return false
}

func validatePinnedGiftRegionCoverage(items []resourcedomain.ResourceGroupItem, requiredRegionIDs []int64, requiredAllRegions bool) error {
	if !requiredAllRegions && len(requiredRegionIDs) == 0 {
		return nil
	}
	for _, item := range items {
		if resourcedomain.NormalizeGroupItemType(item.ItemType) != resourcedomain.GroupItemTypeResource ||
			resourcedomain.NormalizeResourceType(item.Resource.ResourceType) != resourcedomain.TypeGift {
			continue
		}
		if requiredAllRegions {
			global := false
			for _, config := range item.GiftConfigs {
				for _, regionID := range config.RegionIDs {
					if regionID == 0 {
						global = true
						break
					}
				}
				if global {
					break
				}
			}
			if !global {
				return fmt.Errorf("gift resource %d has no global pin-time config for all-regions activity", item.ResourceID)
			}
			continue
		}
		for _, requiredRegionID := range requiredRegionIDs {
			covered := false
			for _, config := range item.GiftConfigs {
				if pinnedGiftAvailableInRegion(config, requiredRegionID) {
					covered = true
					break
				}
			}
			if !covered {
				return fmt.Errorf("gift resource %d is unavailable in required region %d", item.ResourceID, requiredRegionID)
			}
		}
	}
	return nil
}
