// Package mysql 实现 room-service 的 MySQL 持久化边界。
package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"strconv"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	roomservice "hyapp/services/room-service/internal/room/service"
)

// GetRoomSeatConfig 读取当前 App 的房间座位数配置。
func (r *Repository) GetRoomSeatConfig(ctx context.Context) (roomservice.RoomSeatConfig, bool, error) {
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, allowed_seat_counts, default_seat_count, updated_at_ms
		 FROM room_seat_configs
		 WHERE app_code = ?`,
		appcode.FromContext(ctx),
	)

	var config roomservice.RoomSeatConfig
	var rawAllowed string
	if err := row.Scan(&config.AppCode, &rawAllowed, &config.DefaultSeatCount, &config.UpdatedAtMS); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return roomservice.RoomSeatConfig{}, false, nil
		}
		return roomservice.RoomSeatConfig{}, false, err
	}
	if err := json.Unmarshal([]byte(rawAllowed), &config.AllowedSeatCounts); err != nil {
		return roomservice.RoomSeatConfig{}, false, err
	}

	return config, true, nil
}

// UpsertRoomSeatConfig 写入当前 App 的房间座位数配置。
func (r *Repository) UpsertRoomSeatConfig(ctx context.Context, config roomservice.RoomSeatConfig) error {
	appCode := normalizedRecordAppCode(ctx, config.AppCode)
	rawAllowed, err := json.Marshal(config.AllowedSeatCounts)
	if err != nil {
		return err
	}
	nowMS := config.UpdatedAtMS
	if nowMS <= 0 {
		nowMS = time.Now().UTC().UnixMilli()
	}
	_, err = r.db.ExecContext(ctx,
		`INSERT INTO room_seat_configs (app_code, allowed_seat_counts, default_seat_count, created_at_ms, updated_at_ms)
		 VALUES (?, ?, ?, ?, ?)
		 ON DUPLICATE KEY UPDATE
		   allowed_seat_counts = VALUES(allowed_seat_counts),
		   default_seat_count = VALUES(default_seat_count),
		   updated_at_ms = VALUES(updated_at_ms)`,
		appCode,
		string(rawAllowed),
		config.DefaultSeatCount,
		nowMS,
		nowMS,
	)
	return err
}

// GetRoomRocketConfig 读取当前 App 的语音房火箭配置。
func (r *Repository) GetRoomRocketConfig(ctx context.Context) (roomservice.RoomRocketConfig, bool, error) {
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, enabled, config_version, fuel_source, launch_delay_ms,
		        broadcast_enabled, broadcast_scope, broadcast_delay_ms, reward_stack_policy,
		        levels_json, gift_fuel_rules_json, updated_by_admin_id, created_at_ms, updated_at_ms
		 FROM room_rocket_configs
		 WHERE app_code = ?`,
		appcode.FromContext(ctx),
	)

	var config roomservice.RoomRocketConfig
	var enabled int
	var broadcastEnabled int
	var rawLevels string
	var rawRules string
	if err := row.Scan(
		&config.AppCode,
		&enabled,
		&config.ConfigVersion,
		&config.FuelSource,
		&config.LaunchDelayMS,
		&broadcastEnabled,
		&config.BroadcastScope,
		&config.BroadcastDelayMS,
		&config.RewardStackPolicy,
		&rawLevels,
		&rawRules,
		&config.UpdatedByAdminID,
		&config.CreatedAtMS,
		&config.UpdatedAtMS,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return roomservice.RoomRocketConfig{}, false, nil
		}
		return roomservice.RoomRocketConfig{}, false, err
	}
	config.Enabled = enabled == 1
	config.BroadcastEnabled = broadcastEnabled == 1
	if err := json.Unmarshal([]byte(rawLevels), &config.Levels); err != nil {
		return roomservice.RoomRocketConfig{}, false, err
	}
	if err := json.Unmarshal([]byte(rawRules), &config.GiftFuelRules); err != nil {
		return roomservice.RoomRocketConfig{}, false, err
	}

	return config, true, nil
}

// UpsertRoomRocketConfig 写入当前 App 的语音房火箭配置。
func (r *Repository) UpsertRoomRocketConfig(ctx context.Context, config roomservice.RoomRocketConfig) error {
	appCode := normalizedRecordAppCode(ctx, config.AppCode)
	rawLevels, err := json.Marshal(config.Levels)
	if err != nil {
		return err
	}
	rawRules, err := json.Marshal(config.GiftFuelRules)
	if err != nil {
		return err
	}
	nowMS := config.UpdatedAtMS
	if nowMS <= 0 {
		nowMS = time.Now().UTC().UnixMilli()
	}
	createdAtMS := config.CreatedAtMS
	if createdAtMS <= 0 {
		createdAtMS = nowMS
	}
	_, err = r.db.ExecContext(ctx,
		`INSERT INTO room_rocket_configs (
		   app_code, enabled, config_version, fuel_source, launch_delay_ms,
		   broadcast_enabled, broadcast_scope, broadcast_delay_ms, reward_stack_policy,
		   levels_json, gift_fuel_rules_json, updated_by_admin_id, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		 ON DUPLICATE KEY UPDATE
		   enabled = VALUES(enabled),
		   config_version = VALUES(config_version),
		   fuel_source = VALUES(fuel_source),
		   launch_delay_ms = VALUES(launch_delay_ms),
		   broadcast_enabled = VALUES(broadcast_enabled),
		   broadcast_scope = VALUES(broadcast_scope),
		   broadcast_delay_ms = VALUES(broadcast_delay_ms),
		   reward_stack_policy = VALUES(reward_stack_policy),
		   levels_json = VALUES(levels_json),
		   gift_fuel_rules_json = VALUES(gift_fuel_rules_json),
		   updated_by_admin_id = VALUES(updated_by_admin_id),
		   updated_at_ms = VALUES(updated_at_ms)`,
		appCode,
		boolToInt(config.Enabled),
		config.ConfigVersion,
		config.FuelSource,
		config.LaunchDelayMS,
		boolToInt(config.BroadcastEnabled),
		config.BroadcastScope,
		config.BroadcastDelayMS,
		config.RewardStackPolicy,
		string(rawLevels),
		string(rawRules),
		config.UpdatedByAdminID,
		createdAtMS,
		nowMS,
	)
	return err
}

func (r *Repository) AdminListRooms(ctx context.Context, query roomservice.AdminRoomListQuery) ([]roomservice.AdminRoomListEntry, int64, error) {
	if query.Page <= 0 {
		query.Page = 1
	}
	if query.PageSize <= 0 {
		query.PageSize = 20
	}
	if query.PageSize > 100 {
		query.PageSize = 100
	}
	if query.NowMS <= 0 {
		query.NowMS = time.Now().UTC().UnixMilli()
	}
	where, args := adminRoomWhere(ctx, query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM room_list_entries rle `+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	contributionExpr := adminRoomContributionExpr(query.NowMS)
	rows, err := r.db.QueryContext(ctx, adminRoomSelectSQL(contributionExpr)+where+` ORDER BY `+adminRoomOrderBy(query, contributionExpr)+` LIMIT ? OFFSET ?`, append(args, query.PageSize, (query.Page-1)*query.PageSize)...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	items := make([]roomservice.AdminRoomListEntry, 0, query.PageSize)
	for rows.Next() {
		item, err := scanAdminRoom(rows)
		if err != nil {
			return nil, 0, err
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, 0, err
	}
	return items, total, nil
}

func (r *Repository) AdminGetRoom(ctx context.Context, roomID string, nowMS int64) (roomservice.AdminRoomListEntry, bool, error) {
	contributionExpr := adminRoomContributionExpr(nowMS)
	row := r.db.QueryRowContext(ctx, adminRoomSelectSQL(contributionExpr)+` WHERE rle.app_code = ? AND rle.room_id = ?`, appcode.FromContext(ctx), strings.TrimSpace(roomID))
	item, err := scanAdminRoom(row)
	if errors.Is(err, sql.ErrNoRows) {
		return roomservice.AdminRoomListEntry{}, false, nil
	}
	if err != nil {
		return roomservice.AdminRoomListEntry{}, false, err
	}
	return item, true, nil
}

func adminRoomSelectSQL(contributionExpr string) string {
	return `
		SELECT rle.room_id, rle.room_short_id, rle.visible_region_id, rle.owner_user_id,
		       rle.title, rle.cover_url, rle.mode, rle.status, rle.locked, rle.close_reason,
		       rle.closed_by_admin_id, rle.closed_by_admin_name, rle.closed_at_ms,
		       ` + contributionExpr + ` AS room_contribution, rle.online_count, rle.seat_count, rle.occupied_seat_count,
		       rle.sort_score, rle.created_at_ms, rle.updated_at_ms
		FROM room_list_entries rle `
}

func adminRoomWhere(ctx context.Context, query roomservice.AdminRoomListQuery) (string, []any) {
	where := []string{"rle.app_code = ?"}
	app := appcode.Normalize(query.AppCode)
	if app == "" {
		app = appcode.FromContext(ctx)
	}
	args := []any{app}
	switch strings.TrimSpace(query.Status) {
	case "closed":
		where = append(where, "rle.status <> ?")
		args = append(args, "active")
	case "":
	default:
		where = append(where, "rle.status = ?")
		args = append(args, strings.TrimSpace(query.Status))
	}
	if query.RegionID > 0 {
		where = append(where, "rle.visible_region_id = ?")
		args = append(args, query.RegionID)
	}
	if query.OwnerUserID > 0 {
		where = append(where, "rle.owner_user_id = ?")
		args = append(args, query.OwnerUserID)
	}
	if ownerUserIDs := positiveUniqueAdminRoomOwnerIDs(query.OwnerUserIDs); len(ownerUserIDs) > 0 {
		placeholders := make([]string, 0, len(ownerUserIDs))
		for _, ownerUserID := range ownerUserIDs {
			placeholders = append(placeholders, "?")
			args = append(args, ownerUserID)
		}
		where = append(where, "rle.owner_user_id IN ("+strings.Join(placeholders, ",")+")")
	}
	if strings.TrimSpace(query.Keyword) != "" {
		like := "%" + strings.ReplaceAll(strings.TrimSpace(query.Keyword), "%", "\\%") + "%"
		where = append(where, "(rle.room_id LIKE ? ESCAPE '\\\\' OR rle.room_short_id LIKE ? ESCAPE '\\\\' OR rle.title LIKE ? ESCAPE '\\\\' OR CAST(rle.owner_user_id AS CHAR) LIKE ? ESCAPE '\\\\')")
		args = append(args, like, like, like, like)
	}
	return " WHERE " + strings.Join(where, " AND "), args
}

func positiveUniqueAdminRoomOwnerIDs(values []int64) []int64 {
	seen := make(map[int64]struct{}, len(values))
	result := make([]int64, 0, len(values))
	for _, value := range values {
		if value <= 0 {
			continue
		}
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	return result
}

func adminRoomOrderBy(query roomservice.AdminRoomListQuery, contributionExpr string) string {
	direction := "DESC"
	if strings.EqualFold(strings.TrimSpace(query.SortDirection), "asc") {
		direction = "ASC"
	}
	switch strings.TrimSpace(query.SortBy) {
	case "room_contribution", "heat":
		return contributionExpr + " " + direction + ", rle.created_at_ms DESC, rle.room_id DESC"
	default:
		return "rle.created_at_ms DESC, rle.room_id DESC"
	}
}

func adminRoomContributionExpr(nowMS int64) string {
	weekStartMS := roomContributionWeekStartMS(nowMS)
	return "CASE WHEN rle.contribution_week_start_ms = " + strconv.FormatInt(weekStartMS, 10) + " THEN rle.weekly_contribution ELSE 0 END"
}

func roomContributionWeekStartMS(nowMS int64) int64 {
	if nowMS <= 0 {
		nowMS = time.Now().UTC().UnixMilli()
	}
	dayStart := time.UnixMilli(nowMS).UTC()
	dayStart = time.Date(dayStart.Year(), dayStart.Month(), dayStart.Day(), 0, 0, 0, 0, time.UTC)
	// 房间贡献榜按 UTC 周一 01:00 切周，给 00:00 后的结算和榜单消费留出缓冲窗口。
	cutoff := dayStart.Add(time.Hour)
	if time.UnixMilli(nowMS).UTC().Before(cutoff) {
		dayStart = dayStart.AddDate(0, 0, -1)
	}
	weekday := int(dayStart.Weekday())
	if weekday == 0 {
		weekday = 7
	}
	return dayStart.AddDate(0, 0, 1-weekday).Add(time.Hour).UnixMilli()
}

func scanAdminRoom(scanner interface{ Scan(dest ...any) error }) (roomservice.AdminRoomListEntry, error) {
	var item roomservice.AdminRoomListEntry
	err := scanner.Scan(
		&item.RoomID,
		&item.RoomShortID,
		&item.VisibleRegionID,
		&item.OwnerUserID,
		&item.Title,
		&item.CoverURL,
		&item.Mode,
		&item.Status,
		&item.Locked,
		&item.CloseReason,
		&item.ClosedByAdminID,
		&item.ClosedByAdminName,
		&item.ClosedAtMS,
		&item.Heat,
		&item.OnlineCount,
		&item.SeatCount,
		&item.OccupiedSeatCount,
		&item.SortScore,
		&item.CreatedAtMS,
		&item.UpdatedAtMS,
	)
	return item, err
}

func (r *Repository) AdminListRoomPins(ctx context.Context, query roomservice.AdminRoomPinQuery) ([]roomservice.AdminRoomPinEntry, int64, error) {
	if query.Page <= 0 {
		query.Page = 1
	}
	if query.PageSize <= 0 {
		query.PageSize = 20
	}
	if query.PageSize > 100 {
		query.PageSize = 100
	}
	where, args := adminRoomPinWhere(ctx, query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) `+adminRoomPinFromSQL()+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	rows, err := r.db.QueryContext(ctx, adminRoomPinSelectSQL()+where+` ORDER BY p.weight DESC, p.expires_at_ms DESC, p.id DESC LIMIT ? OFFSET ?`, append(args, query.PageSize, (query.Page-1)*query.PageSize)...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	items := make([]roomservice.AdminRoomPinEntry, 0, query.PageSize)
	for rows.Next() {
		item, err := scanAdminRoomPin(rows)
		if err != nil {
			return nil, 0, err
		}
		items = append(items, item)
	}
	return items, total, rows.Err()
}

func (r *Repository) AdminCreateRoomPin(ctx context.Context, input roomservice.AdminCreateRoomPinInput) (roomservice.AdminRoomPinEntry, bool, error) {
	app := appcode.FromContext(ctx)
	room, exists, err := r.AdminGetRoom(ctx, input.RoomID, input.NowMS)
	if err != nil || !exists {
		return roomservice.AdminRoomPinEntry{}, false, err
	}
	if room.Status != "active" {
		return roomservice.AdminRoomPinEntry{}, false, nil
	}
	pinType := strings.TrimSpace(input.PinType)
	if pinType == "" {
		pinType = "region"
	}
	visibleRegionID := room.VisibleRegionID
	if pinType == "global" {
		visibleRegionID = 0
	}
	pinnedAtMS := input.PinnedAtMS
	if pinnedAtMS <= 0 {
		pinnedAtMS = input.NowMS
	}
	expiresAtMS := input.ExpiresAtMS
	if expiresAtMS <= 0 {
		expiresAtMS = pinnedAtMS + input.DurationDays*24*60*60*1000
	}
	if _, err := r.db.ExecContext(ctx, `
		INSERT INTO room_region_pins (
		  app_code, visible_region_id, pin_type, room_id, weight, status, pinned_at_ms, expires_at_ms,
		  cancelled_at_ms, created_by_admin_id, cancelled_by_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, 0, ?, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
		  weight = VALUES(weight), status = VALUES(status), pinned_at_ms = VALUES(pinned_at_ms),
		  expires_at_ms = VALUES(expires_at_ms), cancelled_at_ms = 0,
		  created_by_admin_id = VALUES(created_by_admin_id), cancelled_by_admin_id = 0,
		  created_at_ms = VALUES(created_at_ms), updated_at_ms = VALUES(updated_at_ms)
	`, app, visibleRegionID, pinType, room.RoomID, input.Weight, pinnedAtMS, expiresAtMS, input.AdminID, input.NowMS, input.NowMS); err != nil {
		return roomservice.AdminRoomPinEntry{}, false, err
	}
	return r.adminGetRoomPin(ctx, `WHERE p.app_code = ? AND p.pin_type = ? AND p.visible_region_id = ? AND p.room_id = ?`, app, pinType, visibleRegionID, room.RoomID)
}

func (r *Repository) AdminCancelRoomPin(ctx context.Context, pinID int64, adminID uint64, nowMS int64) (roomservice.AdminRoomPinEntry, bool, error) {
	app := appcode.FromContext(ctx)
	if _, err := r.db.ExecContext(ctx, `
		UPDATE room_region_pins
		SET status = 'cancelled', cancelled_at_ms = ?, cancelled_by_admin_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND id = ? AND status <> 'cancelled'
	`, nowMS, adminID, nowMS, app, pinID); err != nil {
		return roomservice.AdminRoomPinEntry{}, false, err
	}
	return r.adminGetRoomPin(ctx, `WHERE p.app_code = ? AND p.id = ?`, app, pinID)
}

func (r *Repository) adminGetRoomPin(ctx context.Context, where string, args ...any) (roomservice.AdminRoomPinEntry, bool, error) {
	item, err := scanAdminRoomPin(r.db.QueryRowContext(ctx, adminRoomPinSelectSQL()+where+` LIMIT 1`, args...))
	if errors.Is(err, sql.ErrNoRows) {
		return roomservice.AdminRoomPinEntry{}, false, nil
	}
	if err != nil {
		return roomservice.AdminRoomPinEntry{}, false, err
	}
	return item, true, nil
}

func adminRoomPinSelectSQL() string {
	return `
		SELECT p.app_code, p.id, p.visible_region_id, p.pin_type, p.room_id, p.weight, p.status,
		       p.pinned_at_ms, p.expires_at_ms, p.cancelled_at_ms,
		       p.created_by_admin_id, p.cancelled_by_admin_id, p.created_at_ms, p.updated_at_ms,
		       r.room_short_id, r.visible_region_id, r.title, r.cover_url, r.owner_user_id, r.status
	` + adminRoomPinFromSQL()
}

func adminRoomPinFromSQL() string {
	return ` FROM room_region_pins p
		JOIN room_list_entries r ON r.app_code = p.app_code AND r.room_id = p.room_id `
}

func adminRoomPinWhere(ctx context.Context, query roomservice.AdminRoomPinQuery) (string, []any) {
	nowMS := query.NowMS
	if nowMS <= 0 {
		nowMS = time.Now().UTC().UnixMilli()
	}
	where := []string{"p.app_code = ?"}
	args := []any{appcode.FromContext(ctx)}
	switch strings.TrimSpace(query.Status) {
	case "active":
		where = append(where, "p.status = ?", "p.expires_at_ms > ?")
		args = append(args, "active", nowMS)
	case "expired":
		where = append(where, "p.status = ?", "p.expires_at_ms <= ?")
		args = append(args, "active", nowMS)
	case "cancelled":
		where = append(where, "p.status = ?")
		args = append(args, "cancelled")
	}
	if query.RegionID > 0 {
		where = append(where, "p.visible_region_id = ?")
		args = append(args, query.RegionID)
	}
	if strings.TrimSpace(query.PinType) != "" {
		where = append(where, "p.pin_type = ?")
		args = append(args, strings.TrimSpace(query.PinType))
	}
	if strings.TrimSpace(query.Keyword) != "" {
		like := "%" + strings.ReplaceAll(strings.TrimSpace(query.Keyword), "%", "\\%") + "%"
		where = append(where, "(p.room_id LIKE ? ESCAPE '\\\\' OR r.room_short_id LIKE ? ESCAPE '\\\\' OR r.title LIKE ? ESCAPE '\\\\' OR CAST(r.owner_user_id AS CHAR) LIKE ? ESCAPE '\\\\')")
		args = append(args, like, like, like, like)
	}
	return ` WHERE ` + strings.Join(where, " AND "), args
}

func scanAdminRoomPin(scanner interface{ Scan(dest ...any) error }) (roomservice.AdminRoomPinEntry, error) {
	var item roomservice.AdminRoomPinEntry
	err := scanner.Scan(&item.AppCode, &item.ID, &item.VisibleRegionID, &item.PinType, &item.RoomID, &item.Weight, &item.Status, &item.PinnedAtMS, &item.ExpiresAtMS, &item.CancelledAtMS, &item.CreatedByAdminID, &item.CancelledByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.RoomShortID, &item.RoomVisibleRegionID, &item.Title, &item.CoverURL, &item.OwnerUserID, &item.RoomStatus)
	return item, err
}
