package listing

import (
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	"hfb_sys/backend/internal/auditlog"
	"hfb_sys/backend/internal/model"
	"hfb_sys/backend/internal/modules/notification"
	"hfb_sys/backend/pkg/money"

	"gorm.io/datatypes"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type Repository struct {
	db *gorm.DB
}

func NewRepository(db *gorm.DB) *Repository {
	return &Repository{db: db}
}

func initialPublishState(reviewRequired bool) (string, string, *time.Time) {
	if reviewRequired {
		return "draft", "pending", nil
	}
	now := time.Now()
	return "published", "approved", &now
}

func (r *Repository) Create(ownerID uint64, req CreateRequest, reviewRequired bool) (*ListingDTO, error) {
	var dto *ListingDTO
	err := r.db.Transaction(func(tx *gorm.DB) error {
		screenshots, err := marshalScreenshots(req.ScreenshotURLS)
		if err != nil {
			return err
		}
		assetSummary, err := marshalAssetSummary(req.AssetSummary)
		if err != nil {
			return err
		}
		listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
		account := model.GameAccount{
			OwnerID:        ownerID,
			GameName:       "delta_force",
			ServerRegion:   req.ServerRegion,
			LoginPlatform:  req.LoginPlatform,
			Title:          req.Title,
			Description:    req.Description,
			RankLevel:      req.RankLevel,
			HafCoinAmount:  req.HafCoinAmount,
			AssetSummary:   assetSummary,
			ScreenshotURLS: screenshots,
			Status:         listingStatus,
		}
		if err := tx.Create(&account).Error; err != nil {
			return err
		}
		price := normalizedListingPrice(req)
		depositAmount := roundMoney(req.DepositAmount)
		listing := model.RentalListing{
			AccountID:     account.ID,
			OwnerID:       ownerID,
			Price:         price,
			DepositAmount: depositAmount,
			Status:        listingStatus,
			ReviewStatus:  reviewStatus,
			PublishedAt:   publishedAt,
		}
		if err := tx.Create(&listing).Error; err != nil {
			return err
		}
		dto = toDTO(account, listing)
		return nil
	})
	return dto, err
}

type externalUploadCreate struct {
	UploaderName     string
	ClientUploadTime *time.Time
	ClientIP         string
	RawPayload       []byte
	ParsedPayload    []byte
}

func (r *Repository) CreateFromExternalUpload(upload externalUploadCreate, req CreateRequest) (*ListingDTO, error) {
	var dto *ListingDTO
	err := r.db.Transaction(func(tx *gorm.DB) error {
		admin, err := r.findActiveUploadAdmin(tx, upload.UploaderName)
		if err != nil {
			return err
		}
		owner, err := r.ensureUploadOwnerUser(tx, admin)
		if err != nil {
			return err
		}
		screenshots, err := marshalScreenshots(req.ScreenshotURLS)
		if err != nil {
			return err
		}
		assetSummary, err := marshalAssetSummary(req.AssetSummary)
		if err != nil {
			return err
		}
		account := model.GameAccount{
			OwnerID:        owner.ID,
			GameName:       "delta_force",
			ServerRegion:   req.ServerRegion,
			LoginPlatform:  req.LoginPlatform,
			Title:          req.Title,
			Description:    req.Description,
			RankLevel:      req.RankLevel,
			HafCoinAmount:  req.HafCoinAmount,
			AssetSummary:   assetSummary,
			ScreenshotURLS: screenshots,
			Status:         "draft",
		}
		if err := tx.Create(&account).Error; err != nil {
			return err
		}
		listing := model.RentalListing{
			AccountID:     account.ID,
			OwnerID:       owner.ID,
			Price:         normalizedListingPrice(req),
			DepositAmount: roundMoney(req.DepositAmount),
			Status:        "draft",
			ReviewStatus:  "pending",
		}
		if err := tx.Create(&listing).Error; err != nil {
			return err
		}
		matchedAdminID := admin.ID
		ownerID := owner.ID
		listingID := listing.ID
		uploadRow := model.ListingUpload{
			UploaderName:     upload.UploaderName,
			MatchedAdminID:   &matchedAdminID,
			OwnerID:          &ownerID,
			ClientUploadTime: upload.ClientUploadTime,
			ClientIP:         upload.ClientIP,
			RawPayload:       datatypes.JSON(upload.RawPayload),
			ParsedPayload:    datatypes.JSON(upload.ParsedPayload),
			ListingID:        &listingID,
			Status:           "draft_created",
		}
		if err := tx.Create(&uploadRow).Error; err != nil {
			return err
		}
		dto = toDTO(account, listing)
		return nil
	})
	return dto, err
}

func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest, reviewRequired bool) (*ListingDTO, error) {
	var dto *ListingDTO
	err := r.db.Transaction(func(tx *gorm.DB) error {
		listing, account, err := r.findOwnedForUpdate(tx, ownerID, listingID)
		if err != nil {
			return err
		}
		if listing.Status == "rented" || listing.InTransaction {
			return ErrListingLocked
		}

		account.Title = req.Title
		account.Description = req.Description
		account.ServerRegion = req.ServerRegion
		account.LoginPlatform = req.LoginPlatform
		account.RankLevel = req.RankLevel
		account.HafCoinAmount = req.HafCoinAmount
		assetSummary, err := marshalAssetSummary(req.AssetSummary)
		if err != nil {
			return err
		}
		account.AssetSummary = assetSummary
		screenshots, err := marshalScreenshots(req.ScreenshotURLS)
		if err != nil {
			return err
		}
		account.ScreenshotURLS = screenshots

		price := normalizedListingPrice(req)
		listing.Price = price
		listing.DepositAmount = roundMoney(req.DepositAmount)
		listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
		listing.Status = listingStatus
		listing.ReviewStatus = reviewStatus
		listing.ReviewReason = ""
		listing.PublishedAt = publishedAt
		account.Status = listingStatus
		if err := tx.Save(account).Error; err != nil {
			return err
		}
		if err := tx.Save(listing).Error; err != nil {
			return err
		}
		dto = toDTO(*account, *listing)
		return nil
	})
	return dto, err
}

func (r *Repository) SubmitReview(ownerID uint64, listingID uint64, reviewRequired bool) (*ListingDTO, error) {
	var dto *ListingDTO
	err := r.db.Transaction(func(tx *gorm.DB) error {
		listing, account, err := r.findOwnedForUpdate(tx, ownerID, listingID)
		if err != nil {
			return err
		}
		if listing.Status == "rented" || listing.InTransaction {
			return ErrListingLocked
		}
		listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
		listing.Status = listingStatus
		listing.ReviewStatus = reviewStatus
		listing.ReviewReason = ""
		listing.PublishedAt = publishedAt
		account.Status = listingStatus
		if err := tx.Save(account).Error; err != nil {
			return err
		}
		if err := tx.Save(listing).Error; err != nil {
			return err
		}
		dto = toDTO(*account, *listing)
		return nil
	})
	return dto, err
}

func (r *Repository) ListPendingReview() ([]ListingDTO, error) {
	var rows []listingRow
	err := r.baseQuery().
		Where("l.review_status = ?", "pending").
		Order("l.updated_at ASC, l.id ASC").
		Limit(200).
		Scan(&rows).Error
	if err != nil {
		return nil, err
	}
	return rowsToDTO(rows), nil
}

func (r *Repository) ListAdmin(query AdminListQuery) (*AdminListResult, error) {
	page := query.Page
	if page <= 0 {
		page = 1
	}
	pageSize := query.PageSize
	if pageSize <= 0 {
		pageSize = query.Limit
	}
	if pageSize <= 0 {
		pageSize = 20
	}
	if pageSize > 100 {
		pageSize = 100
	}

	countDB := r.applyAdminListFilters(r.db.Table("rental_listings AS l"), query)
	var total int64
	if err := countDB.Count(&total).Error; err != nil {
		return nil, err
	}

	db := r.applyAdminListFilters(r.baseQuery(), query)
	offset := (page - 1) * pageSize
	if offset < 0 {
		offset = 0
	}
	var rows []listingRow
	err := db.Order("l.id DESC").Limit(pageSize).Offset(offset).Scan(&rows).Error
	if err != nil {
		return nil, err
	}
	return &AdminListResult{
		Items:    rowsToDTO(rows),
		Total:    total,
		Page:     page,
		PageSize: pageSize,
	}, nil
}

func (r *Repository) applyAdminListFilters(db *gorm.DB, query AdminListQuery) *gorm.DB {
	if query.OwnerID > 0 {
		db = db.Where("l.owner_id = ?", query.OwnerID)
	}
	if query.Status != "" {
		db = db.Where("l.status = ?", query.Status)
	}
	if query.ReviewStatus != "" {
		db = db.Where("l.review_status = ?", query.ReviewStatus)
	}
	return db
}

func (r *Repository) findActiveUploadAdmin(tx *gorm.DB, uploaderName string) (*model.AdminUser, error) {
	uploaderName = strings.TrimSpace(uploaderName)
	var admin model.AdminUser
	if err := tx.Where("username = ? AND status = ?", uploaderName, "active").First(&admin).Error; err == nil {
		return &admin, nil
	} else if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	var admins []model.AdminUser
	if err := tx.Where("nickname = ? AND status = ?", uploaderName, "active").Limit(2).Find(&admins).Error; err != nil {
		return nil, err
	}
	switch len(admins) {
	case 0:
		return nil, ErrUploaderNotFound
	case 1:
		return &admins[0], nil
	default:
		return nil, ErrUploaderAmbiguous
	}
}

func (r *Repository) ensureUploadOwnerUser(tx *gorm.DB, admin *model.AdminUser) (*model.User, error) {
	phone := fmt.Sprintf("admin:%d", admin.ID)
	nickname := strings.TrimSpace(admin.Nickname)
	if nickname == "" {
		nickname = admin.Username
	}
	var user model.User
	err := tx.Where("phone = ?", phone).First(&user).Error
	if err == nil {
		updates := map[string]any{
			"nickname":        nickname,
			"status":          "active",
			"realname_status": "verified",
		}
		if err := tx.Model(&user).Updates(updates).Error; err != nil {
			return nil, err
		}
		user.Nickname = nickname
		user.Status = "active"
		user.RealnameStatus = "verified"
		return &user, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}
	user = model.User{
		Phone:          phone,
		Nickname:       nickname,
		RealnameStatus: "verified",
		RiskStatus:     "normal",
		CreditScore:    100,
		Status:         "active",
	}
	if err := tx.Create(&user).Error; err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *Repository) FindAdmin(listingID uint64) (*ListingDTO, error) {
	return r.findDTO("l.id = ?", listingID)
}

func (r *Repository) AdminOffline(adminID uint64, listingID uint64, req AdminActionRequest, meta AuditMeta) (*ListingDTO, error) {
	return r.adminUpdateStatus(adminID, listingID, req, meta, "offline", "offline", "listing.admin_offline", "商品已被后台下架", "你的租号商品已被后台下架，请查看原因后处理。")
}

func (r *Repository) AdminMarkAbnormal(adminID uint64, listingID uint64, req AdminActionRequest, meta AuditMeta) (*ListingDTO, error) {
	return r.adminUpdateStatus(adminID, listingID, req, meta, "abnormal", "abnormal", "listing.mark_abnormal", "商品已被标记异常", "你的租号商品已被后台标记异常，请联系客服处理。")
}

func (r *Repository) adminUpdateStatus(adminID uint64, listingID uint64, req AdminActionRequest, meta AuditMeta, listingStatus string, accountStatus string, action string, title string, content string) (*ListingDTO, error) {
	var dto *ListingDTO
	err := r.db.Transaction(func(tx *gorm.DB) error {
		listing, account, err := r.findForReviewUpdate(tx, listingID)
		if err != nil {
			return err
		}
		if listing.Status == "rented" || listing.InTransaction {
			return ErrListingLocked
		}
		beforeListingStatus := listing.Status
		beforeAccountStatus := account.Status
		beforeReviewReason := listing.ReviewReason
		listing.Status = listingStatus
		listing.ReviewReason = req.Reason
		if listingStatus != "published" {
			listing.PublishedAt = nil
		}
		account.Status = accountStatus
		if err := tx.Save(account).Error; err != nil {
			return err
		}
		if err := tx.Save(listing).Error; err != nil {
			return err
		}
		if err := notification.Append(tx, notification.Entry{
			UserID:  listing.OwnerID,
			Type:    "listing_admin",
			Title:   title,
			Content: content,
			BizType: "listing",
			BizID:   &listingID,
		}); err != nil {
			return err
		}
		if err := appendAuditLog(tx, adminID, action, "listing", listing.ID, meta, map[string]any{
			"listing_id":            listing.ID,
			"account_id":            account.ID,
			"owner_id":              listing.OwnerID,
			"reason":                req.Reason,
			"before_listing_status": beforeListingStatus,
			"after_listing_status":  listing.Status,
			"before_account_status": beforeAccountStatus,
			"after_account_status":  account.Status,
			"before_review_reason":  beforeReviewReason,
			"after_review_reason":   listing.ReviewReason,
		}); err != nil {
			return err
		}
		dto = toDTO(*account, *listing)
		return nil
	})
	return dto, err
}

func (r *Repository) Approve(listingID uint64) (*ListingDTO, error) {
	var dto *ListingDTO
	err := r.db.Transaction(func(tx *gorm.DB) error {
		listing, account, err := r.findForReviewUpdate(tx, listingID)
		if err != nil {
			return err
		}
		if listing.Status == "rented" || listing.InTransaction {
			return ErrListingLocked
		}
		now := time.Now()
		listing.Status = "published"
		listing.ReviewStatus = "approved"
		listing.ReviewReason = ""
		listing.PublishedAt = &now
		account.Status = "published"
		if err := tx.Save(account).Error; err != nil {
			return err
		}
		if err := tx.Save(listing).Error; err != nil {
			return err
		}
		listingID := listing.ID
		if err := notification.Append(tx, notification.Entry{
			UserID:  listing.OwnerID,
			Type:    "listing_review",
			Title:   "发布审核通过",
			Content: "你的租号发布已审核通过并上架。",
			BizType: "listing",
			BizID:   &listingID,
		}); err != nil {
			return err
		}
		dto = toDTO(*account, *listing)
		return nil
	})
	return dto, err
}

func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req AdminPriceAdjustRequest, meta AuditMeta) (*ListingDTO, error) {
	var dto *ListingDTO
	err := r.db.Transaction(func(tx *gorm.DB) error {
		listing, account, err := r.findForReviewUpdate(tx, listingID)
		if err != nil {
			return err
		}
		if listing.Status == "rented" || listing.InTransaction {
			return ErrListingLocked
		}
		summary := decodeAssetSummary(account.AssetSummary)
		if summary == nil {
			summary = map[string]any{}
		}
		breakdown := ensurePriceBreakdown(summary)
		coinWan := float64(account.HafCoinAmount) / 10000
		consumablePrice := readSummaryNumber(breakdown["consumable_price"])
		if consumablePrice <= 0 {
			consumablePrice = consumableValue(summary)
		}
		sellerTotalPrice := readSummaryNumber(breakdown["seller_total_price"])
		if sellerTotalPrice <= 0 {
			sellerTotalPrice = math.Max(0, listing.Price-consumablePrice)
		}
		sellerCoinBasePrice := readSummaryNumber(breakdown["seller_coin_base_price"])
		if sellerCoinBasePrice <= 0 {
			sellerCoinBasePrice = math.Max(0, sellerTotalPrice-consumablePrice)
		}
		sellerRatio := readSummaryNumber(breakdown["seller_ratio"])
		if sellerRatio <= 0 && sellerCoinBasePrice > 0 {
			sellerRatio = roundRatio(coinWan / sellerCoinBasePrice)
		}
		buyerCoinBasePrice, buyerTotalPrice, buyerRatio := calculateAdminAdjustedPrice(req, coinWan, consumablePrice)
		if buyerCoinBasePrice <= 0 || buyerTotalPrice <= 0 || buyerRatio <= 0 {
			return ErrInvalidPrice
		}

		beforePrice := listing.Price
		beforeRatio := readSummaryNumber(breakdown["buyer_ratio"])
		if beforeRatio <= 0 && listing.Price > consumablePrice {
			beforeRatio = roundRatio(coinWan / (listing.Price - consumablePrice))
		}

		listing.Price = buyerTotalPrice
		summary["publish_ratio"] = buyerRatio
		breakdown["seller_coin_base_price"] = roundMoney(sellerCoinBasePrice)
		breakdown["seller_total_price"] = roundMoney(sellerTotalPrice)
		breakdown["seller_ratio"] = sellerRatio
		breakdown["buyer_coin_base_price"] = buyerCoinBasePrice
		breakdown["buyer_total_price"] = buyerTotalPrice
		breakdown["buyer_ratio"] = buyerRatio
		breakdown["platform_markup_amount"] = roundMoney(buyerTotalPrice - sellerTotalPrice)
		breakdown["platform_rule_type"] = "admin_adjusted"
		breakdown["admin_adjust_reason"] = strings.TrimSpace(req.Reason)
		breakdown["admin_adjusted_at"] = time.Now().Format(time.RFC3339)
		breakdown["admin_adjusted_by"] = adminID
		summary["price_breakdown"] = breakdown

		assetSummary, err := marshalAssetSummary(summary)
		if err != nil {
			return err
		}
		account.AssetSummary = assetSummary
		if err := tx.Save(account).Error; err != nil {
			return err
		}
		if err := tx.Save(listing).Error; err != nil {
			return err
		}
		if err := appendAuditLog(tx, adminID, "listing.adjust_review_price", "listing", listing.ID, meta, map[string]any{
			"listing_id":         listing.ID,
			"account_id":         account.ID,
			"owner_id":           listing.OwnerID,
			"before_price":       beforePrice,
			"after_price":        listing.Price,
			"before_buyer_ratio": beforeRatio,
			"after_buyer_ratio":  buyerRatio,
			"platform_markup":    breakdown["platform_markup_amount"],
			"adjust_reason":      req.Reason,
			"buyer_coin_base":    buyerCoinBasePrice,
			"consumable_price":   consumablePrice,
			"seller_total_price": sellerTotalPrice,
		}); err != nil {
			return err
		}
		dto = toDTO(*account, *listing)
		return nil
	})
	return dto, err
}

func (r *Repository) Reject(listingID uint64, req ReviewRequest) (*ListingDTO, error) {
	var dto *ListingDTO
	err := r.db.Transaction(func(tx *gorm.DB) error {
		listing, account, err := r.findForReviewUpdate(tx, listingID)
		if err != nil {
			return err
		}
		if listing.Status == "rented" || listing.InTransaction {
			return ErrListingLocked
		}
		listing.Status = "draft"
		listing.ReviewStatus = "rejected"
		listing.ReviewReason = req.Reason
		listing.PublishedAt = nil
		account.Status = "draft"
		if err := tx.Save(account).Error; err != nil {
			return err
		}
		if err := tx.Save(listing).Error; err != nil {
			return err
		}
		listingID := listing.ID
		if err := notification.Append(tx, notification.Entry{
			UserID:  listing.OwnerID,
			Type:    "listing_review",
			Title:   "发布审核未通过",
			Content: "你的租号发布未通过审核，请根据原因修改后重新提交。",
			BizType: "listing",
			BizID:   &listingID,
		}); err != nil {
			return err
		}
		dto = toDTO(*account, *listing)
		return nil
	})
	return dto, err
}

func (r *Repository) Offline(ownerID uint64, listingID uint64) error {
	return r.db.Transaction(func(tx *gorm.DB) error {
		listing, account, err := r.findOwnedForUpdate(tx, ownerID, listingID)
		if err != nil {
			return err
		}
		if listing.Status == "rented" || listing.InTransaction {
			return ErrListingLocked
		}
		listing.Status = "offline"
		account.Status = "offline"
		if err := tx.Save(account).Error; err != nil {
			return err
		}
		return tx.Save(listing).Error
	})
}

func (r *Repository) ListPublic(query PublicListQuery) (*PublicListResult, error) {
	var rows []listingRow
	err := r.baseQuery().
		Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false).
		Order("l.published_at DESC, l.id DESC").
		Scan(&rows).Error
	if err != nil {
		return nil, err
	}
	items := publicListings(rowsToDTO(rows))
	baseQuery := query
	baseQuery.Zone = ""
	items = filterPublicListings(items, baseQuery)
	zoneCounts := publicZoneCounts(items)
	if query.Zone != "" && query.Zone != "all" {
		items = filterPublicListings(items, query)
	}
	sortPublicListings(items, query.Sort)
	total := int64(len(items))
	page := query.Page
	if page <= 0 {
		page = 1
	}
	pageSize := query.PageSize
	if pageSize <= 0 {
		pageSize = 20
	}
	if pageSize > 50 {
		pageSize = 50
	}
	start := (page - 1) * pageSize
	if start < 0 {
		start = 0
	}
	if start >= len(items) {
		items = []ListingDTO{}
	} else {
		end := start + pageSize
		if end > len(items) {
			end = len(items)
		}
		items = items[start:end]
	}
	return &PublicListResult{
		Items:      items,
		Total:      total,
		Page:       page,
		PageSize:   pageSize,
		ZoneCounts: zoneCounts,
	}, nil
}

func (r *Repository) ListMine(ownerID uint64) ([]ListingDTO, error) {
	var rows []listingRow
	err := r.baseQuery().
		Where("l.owner_id = ?", ownerID).
		Order("l.id DESC").
		Scan(&rows).Error
	if err != nil {
		return nil, err
	}
	return sellerListings(rowsToDTO(rows)), nil
}

func filterPublicListings(items []ListingDTO, query PublicListQuery) []ListingDTO {
	filtered := make([]ListingDTO, 0, len(items))
	for _, item := range items {
		if !matchesPublicQuery(item, query) {
			continue
		}
		filtered = append(filtered, item)
	}
	return filtered
}

func matchesPublicQuery(item ListingDTO, query PublicListQuery) bool {
	if !matchesPublicZone(item, query.Zone) {
		return false
	}
	if keyword := strings.ToLower(strings.TrimSpace(query.Keyword)); keyword != "" && !strings.Contains(publicSearchText(item), keyword) {
		return false
	}
	if !matchesAny(query.Server, strings.TrimSpace(item.ServerRegion)) {
		return false
	}
	if len(query.Region) > 0 && !intersects(query.Region, assetRegionsFromSummary(item.AssetSummary)) {
		return false
	}
	if !matchesAny(query.LoginMethod, strings.TrimSpace(item.LoginPlatform)) {
		return false
	}
	if !matchesAny(query.Rank, strings.TrimSpace(item.RankLevel)) {
		return false
	}
	if !matchesAny(query.Insurance, readAssetString(item.AssetSummary, "season_insurance")) {
		return false
	}
	if !matchesAny(query.Stamina, readAssetString(item.AssetSummary, "stamina_level")) {
		return false
	}
	if !matchesAny(query.Load, readAssetString(item.AssetSummary, "load_level")) {
		return false
	}
	if len(query.SkinName) > 0 {
		if len(query.SkinGroup) > 0 {
			if !skinGroupsContainAny(item.AssetSummary, query.SkinGroup, query.SkinName) {
				return false
			}
		} else if !intersects(query.SkinName, skinNamesFromSummary(item.AssetSummary)) {
			return false
		}
	} else if len(query.SkinGroup) > 0 && !skinGroupsHaveAny(item.AssetSummary, query.SkinGroup) {
		return false
	}

	price := item.Price
	deposit := item.DepositAmount
	total := price + deposit
	coinM := coinMFromListing(item)
	if !numberInRange(coinM, NumberRange{Min: query.MinCoin, Max: query.MaxCoin}) {
		return false
	}
	if !numberInRange(price, NumberRange{Min: query.MinPrice, Max: query.MaxPrice}) {
		return false
	}
	if !numberInRange(deposit, NumberRange{Min: query.MinDeposit, Max: query.MaxDeposit}) {
		return false
	}
	if !numberInRange(total, NumberRange{Min: query.MinTotal, Max: query.MaxTotal}) {
		return false
	}
	if !numberInRange(readSummaryNumber(item.AssetSummary["fire_level"]), NumberRange{Min: query.MinFireLevel, Max: query.MaxFireLevel}) {
		return false
	}
	if !numberInRange(readSummaryNumber(item.AssetSummary["secret_kd"]), NumberRange{Min: query.MinSecretKD, Max: query.MaxSecretKD}) {
		return false
	}
	for resourceKey, resourceRange := range query.ResourceRanges {
		if !numberInRange(resourceQuantity(item.AssetSummary, resourceKey), resourceRange) {
			return false
		}
	}
	return true
}

func sortPublicListings(items []ListingDTO, sortKey string) {
	sort.SliceStable(items, func(i, j int) bool {
		a := items[i]
		b := items[j]
		switch sortKey {
		case "priceAsc":
			return a.Price < b.Price
		case "priceDesc":
			return a.Price > b.Price
		case "coinDesc":
			return a.HafCoinAmount > b.HafCoinAmount
		case "awmDesc":
			aAmmo := resourceQuantity(a.AssetSummary, "awmAmmo")
			bAmmo := resourceQuantity(b.AssetSummary, "awmAmmo")
			if aAmmo != bAmmo {
				return aAmmo > bAmmo
			}
			return a.HafCoinAmount > b.HafCoinAmount
		case "published", "recommended", "comprehensive", "":
			return publicRecentLess(a, b)
		default:
			return publicRecentLess(a, b)
		}
	})
}

func publicRecentLess(a ListingDTO, b ListingDTO) bool {
	aTime := time.Time{}
	bTime := time.Time{}
	if a.PublishedAt != nil {
		aTime = *a.PublishedAt
	}
	if b.PublishedAt != nil {
		bTime = *b.PublishedAt
	}
	if !aTime.Equal(bTime) {
		return aTime.After(bTime)
	}
	return a.ID > b.ID
}

func matchesPublicZone(item ListingDTO, zone string) bool {
	switch zone {
	case "", "all":
		return true
	case "sale":
		return item.IsAccelerated
	case "gift":
		return hasGiftResourcesSummary(item.AssetSummary)
	case "night":
		return isNightAvailableSummary(item.AssetSummary)
	case "password":
		return strings.Contains(item.LoginPlatform, "账密") || strings.Contains(item.LoginPlatform, "账号密码")
	case "highCoin":
		return coinMFromListing(item) >= 100
	default:
		return true
	}
}

func publicZoneCounts(items []ListingDTO) map[string]int64 {
	counts := map[string]int64{
		"all":      int64(len(items)),
		"sale":     0,
		"gift":     0,
		"night":    0,
		"password": 0,
		"highCoin": 0,
	}
	for _, item := range items {
		for _, zone := range []string{"sale", "gift", "night", "password", "highCoin"} {
			if matchesPublicZone(item, zone) {
				counts[zone]++
			}
		}
	}
	return counts
}

func publicSearchText(item ListingDTO) string {
	parts := []string{
		item.Title,
		item.Description,
		item.RankLevel,
		item.ServerRegion,
		item.LoginPlatform,
	}
	parts = append(parts, assetRegionsFromSummary(item.AssetSummary)...)
	parts = append(parts, skinNamesFromSummary(item.AssetSummary)...)
	return strings.ToLower(strings.Join(parts, " "))
}

func matchesAny(options []string, value string) bool {
	if len(options) == 0 {
		return true
	}
	for _, option := range options {
		if option == value {
			return true
		}
	}
	return false
}

func intersects(options []string, values []string) bool {
	if len(options) == 0 {
		return true
	}
	valueSet := make(map[string]struct{}, len(values))
	for _, value := range values {
		valueSet[value] = struct{}{}
	}
	for _, option := range options {
		if _, ok := valueSet[option]; ok {
			return true
		}
	}
	return false
}

func numberInRange(value float64, numberRange NumberRange) bool {
	if numberRange.Min != nil && value < *numberRange.Min {
		return false
	}
	if numberRange.Max != nil && value > *numberRange.Max {
		return false
	}
	return true
}

func coinMFromListing(item ListingDTO) float64 {
	return float64(item.HafCoinAmount) / 1000000
}

func readAssetString(summary map[string]any, key string) string {
	if summary == nil {
		return ""
	}
	value, _ := summary[key].(string)
	return strings.TrimSpace(value)
}

func assetRegionsFromSummary(summary map[string]any) []string {
	if summary == nil {
		return nil
	}
	values, ok := summary["common_regions"].([]any)
	if !ok {
		return nil
	}
	result := make([]string, 0, len(values))
	for _, value := range values {
		text, ok := value.(string)
		if ok && strings.TrimSpace(text) != "" {
			result = append(result, strings.TrimSpace(text))
		}
	}
	return result
}

func skinNamesFromSummary(summary map[string]any) []string {
	groups := skinGroupsFromSummary(summary)
	result := make([]string, 0)
	for _, skins := range groups {
		result = append(result, skins...)
	}
	return result
}

func skinGroupsContainAny(summary map[string]any, groupKeys []string, skinNames []string) bool {
	groups := skinGroupsFromSummary(summary)
	for _, groupKey := range groupKeys {
		if intersects(skinNames, groups[groupKey]) {
			return true
		}
	}
	return false
}

func skinGroupsHaveAny(summary map[string]any, groupKeys []string) bool {
	groups := skinGroupsFromSummary(summary)
	for _, groupKey := range groupKeys {
		if len(groups[groupKey]) > 0 {
			return true
		}
	}
	return false
}

func skinGroupsFromSummary(summary map[string]any) map[string][]string {
	result := make(map[string][]string)
	if summary == nil {
		return result
	}
	rawGroups, ok := summary["skin_groups"].(map[string]any)
	if !ok {
		return result
	}
	for key, rawSkins := range rawGroups {
		values, ok := rawSkins.([]any)
		if !ok {
			continue
		}
		for _, value := range values {
			text, ok := value.(string)
			if ok && strings.TrimSpace(text) != "" {
				result[key] = append(result[key], strings.TrimSpace(text))
			}
		}
	}
	return result
}

func resourceQuantity(summary map[string]any, resourceKey string) float64 {
	if summary == nil {
		return 0
	}
	resources, ok := summary["resources"].([]any)
	if !ok {
		return 0
	}
	for _, resource := range resources {
		row, ok := resource.(map[string]any)
		if !ok {
			continue
		}
		if rowKey, _ := row["key"].(string); rowKey == resourceKey {
			return readSummaryNumber(row["quantity"])
		}
	}
	return 0
}

func hasGiftResourcesSummary(summary map[string]any) bool {
	if summary == nil {
		return false
	}
	resources, ok := summary["resources"].([]any)
	if !ok {
		return false
	}
	for _, resource := range resources {
		row, ok := resource.(map[string]any)
		if !ok {
			continue
		}
		if mode, _ := row["mode"].(string); mode == "赠送" && readSummaryNumber(row["quantity"]) > 0 {
			return true
		}
	}
	return false
}

func isNightAvailableSummary(summary map[string]any) bool {
	if summary == nil {
		return false
	}
	onlineTime, ok := summary["online_time"].(map[string]any)
	if !ok {
		return false
	}
	start, okStart := parseTimeHourValue(onlineTime["start"])
	end, okEnd := parseTimeHourValue(onlineTime["end"])
	if !okStart || !okEnd {
		return false
	}
	return timeRangeCoversHour(start, end, 22) || timeRangeCoversHour(start, end, 23) || timeRangeCoversHour(start, end, 0)
}

func parseTimeHourValue(value any) (int, bool) {
	text, ok := value.(string)
	if !ok {
		return 0, false
	}
	parts := strings.Split(text, ":")
	hour, err := strconv.Atoi(parts[0])
	if err != nil || hour < 0 || hour > 23 {
		return 0, false
	}
	return hour, true
}

func timeRangeCoversHour(start int, end int, hour int) bool {
	if start == end {
		return true
	}
	if start < end {
		return hour >= start && hour <= end
	}
	return hour >= start || hour <= end
}

func (r *Repository) FindPublic(id uint64) (*ListingDTO, error) {
	dto, err := r.findDTO("l.id = ? AND l.status = ? AND l.review_status = ? AND l.in_transaction = ?", id, "published", "approved", false)
	if err != nil {
		return nil, err
	}
	applyPublicListingURLs(dto)
	return dto, nil
}

func (r *Repository) FindPublicCoverKey(id uint64) (string, error) {
	return r.FindPublicScreenshotKey(id, 0)
}

func (r *Repository) FindPublicScreenshotKey(id uint64, index int) (string, error) {
	if index < 0 {
		return "", gorm.ErrRecordNotFound
	}
	dto, err := r.findDTO("l.id = ? AND l.status = ? AND l.review_status = ? AND l.in_transaction = ?", id, "published", "approved", false)
	if err != nil {
		return "", err
	}
	if index >= len(dto.ScreenshotURLS) {
		return "", gorm.ErrRecordNotFound
	}
	if key := extractListingObjectKey(dto.ScreenshotURLS[index]); key != "" {
		return key, nil
	}
	return "", gorm.ErrRecordNotFound
}

func (r *Repository) FindMine(ownerID uint64, id uint64) (*ListingDTO, error) {
	dto, err := r.findDTO("l.id = ? AND l.owner_id = ?", id, ownerID)
	if err != nil {
		return nil, err
	}
	applySellerListingPrice(dto)
	return dto, nil
}

func (r *Repository) findOwnedForUpdate(tx *gorm.DB, ownerID uint64, listingID uint64) (*model.RentalListing, *model.GameAccount, error) {
	var listing model.RentalListing
	if err := tx.Where("id = ? AND owner_id = ?", listingID, ownerID).First(&listing).Error; err != nil {
		return nil, nil, err
	}
	var account model.GameAccount
	if err := tx.Where("id = ? AND owner_id = ?", listing.AccountID, ownerID).First(&account).Error; err != nil {
		return nil, nil, err
	}
	return &listing, &account, nil
}

func (r *Repository) findDTO(where string, args ...any) (*ListingDTO, error) {
	var row listingRow
	err := r.baseQuery().
		Where(where, args...).
		First(&row).Error
	if err != nil {
		return nil, err
	}
	dto := row.toDTO()
	return &dto, nil
}

func (r *Repository) baseQuery() *gorm.DB {
	return r.db.Table("rental_listings AS l").
		Select(`l.*, a.title, a.description, a.game_name, a.server_region, a.login_platform, a.rank_level,
			a.haf_coin_amount, a.asset_summary, a.screenshot_urls, COALESCE(u.phone, '') AS owner_phone, COALESCE(u.nickname, '') AS owner_nickname`).
		Joins("JOIN game_accounts AS a ON a.id = l.account_id").
		Joins("LEFT JOIN users AS u ON u.id = l.owner_id")
}

func (r *Repository) findForReviewUpdate(tx *gorm.DB, listingID uint64) (*model.RentalListing, *model.GameAccount, error) {
	var listing model.RentalListing
	if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, listingID).Error; err != nil {
		return nil, nil, err
	}
	var account model.GameAccount
	if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil {
		return nil, nil, err
	}
	return &listing, &account, nil
}

type listingRow struct {
	model.RentalListing
	Title          string
	OwnerPhone     string
	OwnerNickname  string
	Description    string
	GameName       string
	ServerRegion   string
	LoginPlatform  string
	RankLevel      string
	HafCoinAmount  int64
	AssetSummary   datatypes.JSON `gorm:"column:asset_summary"`
	ScreenshotURLS datatypes.JSON `gorm:"column:screenshot_urls"`
}

func rowsToDTO(rows []listingRow) []ListingDTO {
	items := make([]ListingDTO, 0, len(rows))
	for _, row := range rows {
		items = append(items, row.toDTO())
	}
	return items
}

func normalizedListingPrice(req CreateRequest) float64 {
	if req.AssetSummary != nil {
		if breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any); ok {
			buyerPrice := readSummaryNumber(breakdown["buyer_total_price"])
			if buyerPrice > 0 {
				return roundMoney(buyerPrice)
			}
		}
	}
	return roundMoney(req.Price)
}

func publicListings(items []ListingDTO) []ListingDTO {
	for index := range items {
		applyPublicListingURLs(&items[index])
	}
	return items
}

func sellerListings(items []ListingDTO) []ListingDTO {
	for index := range items {
		applySellerListingPrice(&items[index])
	}
	return items
}

func applyPublicListingURLs(item *ListingDTO) {
	item.ScreenshotURLS = publicScreenshotURLs(item.ID, item.ScreenshotURLS, item.Status, item.ReviewStatus)
	if item.AssetSummary != nil {
		delete(item.AssetSummary, "price_breakdown")
	}
}

func applySellerListingPrice(item *ListingDTO) {
	if item == nil || item.AssetSummary == nil {
		return
	}
	breakdown, ok := item.AssetSummary["price_breakdown"].(map[string]any)
	if !ok {
		return
	}
	sellerPrice := readSummaryNumber(breakdown["seller_total_price"])
	if sellerPrice > 0 {
		item.Price = sellerPrice
	}
	sellerRatio := readSummaryNumber(breakdown["seller_ratio"])
	if sellerRatio > 0 {
		item.AssetSummary["publish_ratio"] = sellerRatio
	}
	delete(breakdown, "buyer_coin_base_price")
	delete(breakdown, "buyer_total_price")
	delete(breakdown, "buyer_ratio")
	delete(breakdown, "platform_markup_amount")
	delete(breakdown, "platform_rule_type")
}

func (row listingRow) toDTO() ListingDTO {
	assetSummary := decodeAssetSummary(row.AssetSummary)
	screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS))
	return ListingDTO{
		ID:             row.ID,
		AccountID:      row.AccountID,
		OwnerID:        row.OwnerID,
		OwnerPhone:     row.OwnerPhone,
		OwnerNickname:  row.OwnerNickname,
		Title:          row.Title,
		Description:    row.Description,
		GameName:       row.GameName,
		ServerRegion:   row.ServerRegion,
		LoginPlatform:  row.LoginPlatform,
		RankLevel:      row.RankLevel,
		HafCoinAmount:  row.HafCoinAmount,
		AssetSummary:   assetSummary,
		ScreenshotURLS: screenshotURLS,
		CoverURL:       publicCoverURL(row.ID, screenshotURLS, row.Status, row.ReviewStatus),
		Price:          row.Price,
		DepositAmount:  row.DepositAmount,
		IsAccelerated:  isAcceleratedSale(assetSummary),
		InTransaction:  row.InTransaction,
		Status:         row.Status,
		ReviewStatus:   row.ReviewStatus,
		ReviewReason:   row.ReviewReason,
		PublishedAt:    row.PublishedAt,
		CreatedAt:      row.CreatedAt,
		UpdatedAt:      row.UpdatedAt,
	}
}

func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
	assetSummary := decodeAssetSummary(account.AssetSummary)
	screenshotURLS := cleanScreenshotURLs(decodeScreenshots(account.ScreenshotURLS))
	return &ListingDTO{
		ID:             listing.ID,
		AccountID:      account.ID,
		OwnerID:        listing.OwnerID,
		Title:          account.Title,
		Description:    account.Description,
		GameName:       account.GameName,
		ServerRegion:   account.ServerRegion,
		LoginPlatform:  account.LoginPlatform,
		RankLevel:      account.RankLevel,
		HafCoinAmount:  account.HafCoinAmount,
		AssetSummary:   assetSummary,
		ScreenshotURLS: screenshotURLS,
		CoverURL:       publicCoverURL(listing.ID, screenshotURLS, listing.Status, listing.ReviewStatus),
		Price:          listing.Price,
		DepositAmount:  listing.DepositAmount,
		IsAccelerated:  isAcceleratedSale(assetSummary),
		InTransaction:  listing.InTransaction,
		Status:         listing.Status,
		ReviewStatus:   listing.ReviewStatus,
		ReviewReason:   listing.ReviewReason,
		PublishedAt:    listing.PublishedAt,
		CreatedAt:      listing.CreatedAt,
		UpdatedAt:      listing.UpdatedAt,
	}
}

func marshalScreenshots(urls []string) (datatypes.JSON, error) {
	cleaned := cleanScreenshotURLs(urls)
	if len(cleaned) > 12 {
		cleaned = cleaned[:12]
	}
	raw, err := json.Marshal(cleaned)
	if err != nil {
		return nil, err
	}
	return datatypes.JSON(raw), nil
}

func marshalAssetSummary(summary map[string]any) (datatypes.JSON, error) {
	if summary == nil {
		return nil, nil
	}
	raw, err := json.Marshal(summary)
	if err != nil {
		return nil, err
	}
	return datatypes.JSON(raw), nil
}

func decodeScreenshots(raw datatypes.JSON) []string {
	if len(raw) == 0 {
		return []string{}
	}
	var urls []string
	if err := json.Unmarshal(raw, &urls); err != nil {
		return []string{}
	}
	return urls
}

func decodeAssetSummary(raw datatypes.JSON) map[string]any {
	if len(raw) == 0 {
		return nil
	}
	var summary map[string]any
	if err := json.Unmarshal(raw, &summary); err != nil {
		return nil
	}
	return summary
}

func isAcceleratedSale(summary map[string]any) bool {
	if summary == nil {
		return false
	}
	breakdown, ok := summary["price_breakdown"].(map[string]any)
	if !ok {
		return false
	}
	referenceRatio := readSummaryNumber(breakdown["seller_reference_ratio"])
	sellerRatio := readSummaryNumber(breakdown["seller_ratio"])
	acceleratedRatio := readSummaryNumber(breakdown["accelerated_sale_ratio"])
	if referenceRatio <= 0 {
		return false
	}
	return sellerRatio > referenceRatio || acceleratedRatio > referenceRatio
}

func readSummaryNumber(value any) float64 {
	switch typed := value.(type) {
	case float64:
		return typed
	case float32:
		return float64(typed)
	case int:
		return float64(typed)
	case int64:
		return float64(typed)
	case json.Number:
		number, err := typed.Float64()
		if err != nil {
			return 0
		}
		return number
	case string:
		number, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
		if err != nil {
			return 0
		}
		return number
	default:
		return 0
	}
}

func ensurePriceBreakdown(summary map[string]any) map[string]any {
	if summary == nil {
		return map[string]any{}
	}
	breakdown, ok := summary["price_breakdown"].(map[string]any)
	if ok {
		return breakdown
	}
	breakdown = map[string]any{}
	if raw, ok := summary["price_breakdown"].(map[string]interface{}); ok {
		for key, value := range raw {
			breakdown[key] = value
		}
	}
	return breakdown
}

func calculateAdminAdjustedPrice(req AdminPriceAdjustRequest, coinWan float64, consumablePrice float64) (float64, float64, float64) {
	if req.BuyerTotalPrice > 0 {
		buyerTotalPrice := roundMoney(req.BuyerTotalPrice)
		buyerCoinBasePrice := roundMoney(buyerTotalPrice - consumablePrice)
		if buyerCoinBasePrice <= 0 || coinWan <= 0 {
			return 0, 0, 0
		}
		return buyerCoinBasePrice, buyerTotalPrice, roundRatio(coinWan / buyerCoinBasePrice)
	}
	if req.BuyerRatio <= 0 || coinWan <= 0 {
		return 0, 0, 0
	}
	buyerCoinBasePrice := roundMoney(coinWan / req.BuyerRatio)
	buyerTotalPrice := roundMoney(buyerCoinBasePrice + consumablePrice)
	return buyerCoinBasePrice, buyerTotalPrice, roundRatio(coinWan / buyerCoinBasePrice)
}

func roundRatio(value float64) float64 {
	if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
		return 0
	}
	return math.Round(value*10) / 10
}

func cleanScreenshotURLs(urls []string) []string {
	cleaned := make([]string, 0, len(urls))
	seen := make(map[string]struct{}, len(urls))
	for _, url := range urls {
		url = strings.TrimSpace(url)
		if url == "" {
			continue
		}
		if _, ok := seen[url]; ok {
			continue
		}
		seen[url] = struct{}{}
		cleaned = append(cleaned, url)
	}
	return cleaned
}

func firstScreenshotURL(urls []string) string {
	if len(urls) == 0 {
		return ""
	}
	return urls[0]
}

func publicCoverURL(listingID uint64, urls []string, status string, reviewStatus string) string {
	fallback := firstScreenshotURL(urls)
	if status != "published" || reviewStatus != "approved" || extractListingObjectKey(fallback) == "" {
		return fallback
	}
	return "/api/listings/" + strconv.FormatUint(listingID, 10) + "/cover"
}

func publicScreenshotURLs(listingID uint64, urls []string, status string, reviewStatus string) []string {
	if status != "published" || reviewStatus != "approved" {
		return urls
	}
	publicURLs := make([]string, 0, len(urls))
	for index, fileURL := range urls {
		if extractListingObjectKey(fileURL) == "" {
			publicURLs = append(publicURLs, fileURL)
			continue
		}
		publicURLs = append(publicURLs, "/api/listings/"+strconv.FormatUint(listingID, 10)+"/screenshots/"+strconv.Itoa(index))
	}
	return publicURLs
}

func extractListingObjectKey(fileURL string) string {
	if fileURL == "" {
		return ""
	}
	parsed, err := url.Parse(fileURL)
	if err != nil {
		return ""
	}
	key := parsed.Query().Get("key")
	if key == "" {
		return ""
	}
	if !strings.HasPrefix(key, "listing/") || strings.Contains(key, "..") {
		return ""
	}
	return key
}

func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizType string, bizID uint64, meta AuditMeta, detail map[string]any) error {
	return auditlog.Append(tx, auditlog.Entry{
		ActorType: "admin",
		ActorID:   actorID,
		Action:    action,
		BizType:   bizType,
		BizID:     &bizID,
		Meta:      meta,
		Detail:    detail,
	})
}

func IsNotFound(err error) bool {
	return errors.Is(err, gorm.ErrRecordNotFound)
}

// roundMoney 使用统一的角精度（0.1元）
func roundMoney(value float64) float64 {
	return money.Round(value)
}
