package gamemanagement

import (
	"fmt"
	"strings"

	"hyapp-admin-server/internal/response"
	gamev1 "hyapp.local/api/proto/game/v1"

	"github.com/gin-gonic/gin"
)

func (h *Handler) GetRoomRPSConfig(c *gin.Context) {
	// 后台查询不读本地缓存，也不直接查 game 表；所有配置事实统一从 game-service 管理 RPC 返回，避免 admin-server 和运行时出现双写。
	resp, err := h.game.GetRoomRPSConfig(c.Request.Context(), &gamev1.GetRoomRPSConfigRequest{Meta: requestMeta(c)})
	if err != nil {
		response.ServerError(c, "获取房内猜拳配置失败")
		return
	}
	response.OK(c, gin.H{"config": roomRPSConfigFromProto(resp.GetConfig()), "serverTimeMs": resp.GetServerTimeMs()})
}

func (h *Handler) UpdateRoomRPSConfig(c *gin.Context) {
	var req roomRPSConfigRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "房内猜拳配置参数不正确")
		return
	}
	// JSON 绑定只负责类型检查，业务边界统一放到 toProto：默认启用、默认倒计时、固定四档礼物和礼物 ID 去重。
	config, err := req.toProto()
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	// admin-server 只校验后台可见的配置边界：开关、超时和 4 个礼物档位。
	// 礼物名称、价格、图标、扣款、托管和结算幂等由 game-service/wallet-service 统一处理，避免后台绕过运行时校验写入脏配置。
	resp, err := h.game.UpdateRoomRPSConfig(c.Request.Context(), &gamev1.UpdateRoomRPSConfigRequest{
		Meta:   requestMeta(c),
		Config: config,
	})
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	item := roomRPSConfigFromProto(resp.GetConfig())
	// 配置修改会影响所有房间入口和下注档位，必须写审计；审计目标使用 game_id，便于后续按玩法追踪变更。
	h.auditLog(c, "update-room-rps-config", "game_room_rps_configs", item.GameID, item.Status)
	response.OK(c, gin.H{"config": item, "serverTimeMs": resp.GetServerTimeMs()})
}

func (h *Handler) ListRoomRPSChallenges(c *gin.Context) {
	// 管理后台默认页大小比 App 大，但仍只允许正数；真正的上限和游标稳定性由 game-service 控制，防止 admin-server 复制分页规则。
	pageSize := int32(parsePositiveInt(firstQuery(c, "pageSize", "page_size"), 50))
	// 所有 ID 查询都兼容 camelCase 和 snake_case，前端生成代码和手工调试都能命中同一套过滤逻辑。
	initiatorUserID, ok := optionalInt64Query(c, "initiatorUserId", "initiator_user_id")
	if !ok {
		response.BadRequest(c, "发起人用户 ID 参数不正确")
		return
	}
	challengerUserID, ok := optionalInt64Query(c, "challengerUserId", "challenger_user_id")
	if !ok {
		response.BadRequest(c, "应战人用户 ID 参数不正确")
		return
	}
	startTimeMS, ok := optionalInt64Query(c, "startTimeMs", "start_time_ms")
	if !ok {
		response.BadRequest(c, "开始时间参数不正确")
		return
	}
	endTimeMS, ok := optionalInt64Query(c, "endTimeMs", "end_time_ms")
	if !ok {
		response.BadRequest(c, "结束时间参数不正确")
		return
	}
	// 列表接口只把后台筛选条件透传给 game-service；订单可见性、游标语义、状态枚举和结算状态仍以 game-service 查询结果为准。
	// admin-server 不做本地状态拼装，避免后续 game-service 增加 timeout/settlement_failed 补偿状态时后台漏同步。
	resp, err := h.game.ListRoomRPSChallenges(c.Request.Context(), &gamev1.ListRoomRPSChallengesRequest{
		Meta:             requestMeta(c),
		RoomId:           strings.TrimSpace(firstQuery(c, "roomId", "room_id")),
		Status:           strings.TrimSpace(c.Query("status")),
		InitiatorUserId:  initiatorUserID,
		ChallengerUserId: challengerUserID,
		StartTimeMs:      startTimeMS,
		EndTimeMs:        endTimeMS,
		PageSize:         pageSize,
		Cursor:           strings.TrimSpace(c.Query("cursor")),
	})
	if err != nil {
		response.ServerError(c, "获取房内猜拳订单失败")
		return
	}
	items := make([]roomRPSChallengeDTO, 0, len(resp.GetChallenges()))
	for _, item := range resp.GetChallenges() {
		// DTO 转换只做字段命名和大整数保护，不在后台列表里重新计算胜负或结算结果。
		items = append(items, roomRPSChallengeFromProto(item))
	}
	response.OK(c, gin.H{"items": items, "nextCursor": resp.GetNextCursor(), "pageSize": pageSize, "serverTimeMs": resp.GetServerTimeMs()})
}

func (h *Handler) GetRoomRPSChallenge(c *gin.Context) {
	challengeID := strings.TrimSpace(c.Param("challenge_id"))
	if challengeID == "" {
		response.BadRequest(c, "挑战单 ID 参数不正确")
		return
	}
	// 详情接口不接收 room_id 或用户 ID 作为辅助条件；challenge_id 是唯一事实入口，避免后台参数互相矛盾时误展示错误订单。
	resp, err := h.game.GetRoomRPSChallenge(c.Request.Context(), &gamev1.GetRoomRPSChallengeRequest{
		Meta:        requestMeta(c),
		ChallengeId: challengeID,
	})
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	response.OK(c, gin.H{"challenge": roomRPSChallengeFromProto(resp.GetChallenge()), "serverTimeMs": resp.GetServerTimeMs()})
}

func (h *Handler) RetryRoomRPSSettlement(c *gin.Context) {
	challengeID := strings.TrimSpace(c.Param("challenge_id"))
	if challengeID == "" {
		response.BadRequest(c, "挑战单 ID 参数不正确")
		return
	}
	// 重试入口只允许后台触发一次明确的运维命令。
	// 是否处于 settlement_failed、是否已经结算过、钱包补偿是否可重放，都由 game-service 按订单幂等事实判断，admin-server 不能预读后再决定。
	resp, err := h.game.RetryRoomRPSSettlement(c.Request.Context(), &gamev1.RetryRoomRPSSettlementRequest{
		Meta:        requestMeta(c),
		ChallengeId: challengeID,
	})
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	item := roomRPSChallengeFromProto(resp.GetChallenge())
	// 审计记录写回 game-service 返回的新结算状态，方便排查“点击重试但实际已被其它 worker 修复”的幂等场景。
	h.auditLog(c, "retry-room-rps-settlement", "game_room_rps_challenges", item.ChallengeID, item.SettlementStatus)
	response.OK(c, gin.H{"challenge": item, "serverTimeMs": resp.GetServerTimeMs()})
}

func (h *Handler) ExpireRoomRPSChallenge(c *gin.Context) {
	challengeID := strings.TrimSpace(c.Param("challenge_id"))
	if challengeID == "" {
		response.BadRequest(c, "挑战单 ID 参数不正确")
		return
	}
	// 手动过期只是运维兜底命令。
	// 只有 pending 且未被应战的挑战能过期；这个判断必须在 game-service 事务内完成，避免后台读到 pending 后并发被应战。
	resp, err := h.game.ExpireRoomRPSChallenge(c.Request.Context(), &gamev1.ExpireRoomRPSChallengeRequest{
		Meta:        requestMeta(c),
		ChallengeId: challengeID,
	})
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	item := roomRPSChallengeFromProto(resp.GetChallenge())
	// 审计记录写最终状态，而不是写“用户点击了过期”，这样能看出命令是否真的把订单推进到 timeout。
	h.auditLog(c, "expire-room-rps-challenge", "game_room_rps_challenges", item.ChallengeID, item.Status)
	response.OK(c, gin.H{"challenge": item, "serverTimeMs": resp.GetServerTimeMs()})
}

func optionalInt64Query(c *gin.Context, keys ...string) (int64, bool) {
	raw := strings.TrimSpace(firstQuery(c, keys...))
	if raw == "" {
		// 空值表示不筛选，不是筛选用户 0；game-service 看到 0 时按“无该条件”处理。
		return 0, true
	}
	var value int64
	if _, err := fmt.Sscan(raw, &value); err != nil || value < 0 {
		return 0, false
	}
	return value, true
}
