package dice

import (
	"encoding/json"
	"sort"
	"strings"

	"hyapp/pkg/xerr"
)

const (
	// 默认平台值只用于自研骰子没有第三方 provider 配置时的订单快照，保证 game_orders 唯一键仍有稳定平台维度。
	DefaultPlatformCode   = "dice"
	DefaultProviderGameID = "dice"
	// DefaultFeeBPS 用万分比表达平台费，结算时先从总奖池扣除，再把剩余奖池均分给最高分赢家。
	DefaultFeeBPS        = int64(500)
	DefaultPoolBPS       = int64(100)
	DefaultJoinTTLMillis = int64(60_000)
	DefaultRobotWaitMS   = int64(3_000)
	DefaultGameID        = "dice"
	SelfGameIDRock       = "rock"
	// 首版禁止 1 人局，避免单人场景下“自己赢自己”的派奖语义不清；3 人局只是多一行 participant。
	DefaultMinPlayers = int32(2)
	DefaultMaxPlayers = int32(2)
	HardMaxPlayers    = int32(6)
	// 单局胜场制每个参与者只扔一颗骰子；数组仍保留是为了兼容旧接口字段和未来玩法扩展。
	PointsPerParticipant  = 1
	ProviderOrderPrefix   = "dice"
	MatchIDPrefix         = "dice_match"
	ParticipantResultWin  = "win"
	ParticipantResultLose = "lose"
	ParticipantResultDraw = "draw"

	RPSGestureRock     = "rock"
	RPSGesturePaper    = "paper"
	RPSGestureScissors = "scissors"

	MatchStatusCreated        = "created"
	MatchStatusJoining        = "joining"
	MatchStatusReady          = "ready"
	MatchStatusSettling       = "settling"
	MatchStatusPayoutApplying = "payout_applying"
	MatchStatusSettled        = "settled"
	MatchStatusFailed         = "failed"
	MatchStatusCanceled       = "canceled"

	MatchPhaseWaiting    = "waiting"
	MatchPhaseCountdown  = "countdown"
	MatchPhaseRolling    = "rolling"
	MatchPhaseComparing  = "comparing"
	MatchPhaseSettled    = "settled"
	MatchPhaseCanceled   = "canceled"
	MatchPhaseFailed     = "failed"
	CountdownMillis      = int64(3_000)
	RollingMillis        = int64(3_000)
	ComparingMillis      = int64(3_000)
	DrawRerollMillis     = int64(2_000)
	SettlementShowMillis = int64(3_000)

	ParticipantStatusJoined          = "joined"
	ParticipantStatusDebitSucceeded  = "debit_succeeded"
	ParticipantStatusDebitFailed     = "debit_failed"
	ParticipantStatusPayoutSucceeded = "payout_succeeded"
	ParticipantStatusRefundSucceeded = "refund_succeeded"
	ParticipantStatusSettled         = "settled"

	ParticipantTypeUser  = "user"
	ParticipantTypeRobot = "robot"

	MatchModeHuman = "human"
	MatchModeRobot = "robot"

	ForcedResultPlayerWin                  = "player_win"
	ForcedResultPlayerWinWithoutProtection = "player_win_no_protection"
	ForcedResultPlayerLose                 = "player_lose"

	ConfigStatusActive   = "active"
	ConfigStatusDisabled = "disabled"

	RobotStatusActive   = "active"
	RobotStatusDisabled = "disabled"

	ExploreGameCodeDice          = "game_dice"
	ExploreGameCodeRock          = "game_rock"
	DefaultExploreWinnerPageSize = int32(20)

	PoolDirectionIn  = "in"
	PoolDirectionOut = "out"
)

// StakeOption 是后台配置给 H5 展示的下注档位；排序字段只影响展示，不参与结算公式。
type StakeOption struct {
	StakeCoin int64
	Enabled   bool
	SortOrder int32
}

// Config 是自研骰子游戏的运营配置快照；每局创建时会把费率复制到 match，保证历史局按当时配置结算。
type Config struct {
	AppCode          string
	GameID           string
	Status           string
	StakeOptions     []StakeOption
	FeeBPS           int32
	PoolBPS          int32
	MinPlayers       int32
	MaxPlayers       int32
	RobotEnabled     bool
	RobotMatchWaitMS int64
	PoolBalanceCoin  int64
	CreatedAtMS      int64
	UpdatedAtMS      int64
}

// Robot 是 game-service 对“哪些真实 App 用户可作为机器人”的最小登记，不复制用户头像昵称。
type Robot struct {
	AppCode          string
	GameID           string
	UserID           int64
	Status           string
	CreatedByAdminID int64
	CreatedAtMS      int64
	UpdatedAtMS      int64
	LastUsedAtMS     int64
	UsedCount        int64
}

// ExploreWinner 是 Explore Square 展示用的轻量获胜事实；只由真实游戏结算写入，读接口不能主动制造默认播报。
type ExploreWinner struct {
	AppCode     string
	WinID       string
	GameCode    string
	GameID      string
	UserID      int64
	DisplayName string
	AvatarURL   string
	CoinAmount  int64
	WonAtMS     int64
	CreatedAtMS int64
	UpdatedAtMS int64
}

// PoolAdjustment 是奖池余额变化事实；所有机器人对局和后台手工调整都必须留下流水。
type PoolAdjustment struct {
	AdjustmentID string
	AppCode      string
	GameID       string
	StakeCoin    int64
	MatchID      string
	UserID       int64
	Direction    string
	AmountCoin   int64
	Reason       string
	BalanceAfter int64
	CreatedBy    int64
	CreatedAtMS  int64
}

// Match 是骰子局的持久化事实；平台和 provider_game_id 是下单快照，不直接暴露给 App。
type Match struct {
	AppCode        string
	MatchID        string
	GameID         string
	PlatformCode   string
	ProviderGameID string
	RoomID         string
	RegionID       int64
	CountryID      int64
	MinPlayers     int32
	MaxPlayers     int32
	CurrentPlayers int32
	StakeCoin      int64
	RoundNo        int32
	Status         string
	Result         string
	Participants   []Participant
	JoinDeadlineMS int64
	ReadyAtMS      int64
	CreatedAtMS    int64
	UpdatedAtMS    int64
	SettledAtMS    int64
	CanceledAtMS   int64
	FeeBPS         int32
	PoolBPS        int32
	MatchMode      string
	ForcedResult   string
	PoolDeltaCoin  int64
}

// Participant 是一局内单个用户的座位、扣款、投骰和派奖快照。
type Participant struct {
	AppCode         string
	MatchID         string
	UserID          int64
	ParticipantType string
	SeatNo          int32
	Status          string
	StakeCoin       int64
	DicePoints      []int32
	RPSGesture      string
	Result          string
	PayoutCoin      int64
	DebitOrderID    string
	PayoutOrderID   string
	RefundOrderID   string
	BalanceAfter    int64
	JoinedAtMS      int64
	UpdatedAtMS     int64
}

// NormalizeConfig 把后台配置收口成可执行值；空配置仍返回安全默认，保证新环境可以直接打开骰子 H5。
func NormalizeConfig(config Config) (Config, error) {
	config.GameID = strings.TrimSpace(config.GameID)
	if config.GameID == "" {
		config.GameID = DefaultGameID
	}
	config.Status = strings.TrimSpace(config.Status)
	if config.Status == "" {
		config.Status = ConfigStatusActive
	}
	if config.Status != ConfigStatusActive && config.Status != ConfigStatusDisabled {
		return Config{}, xerr.New(xerr.InvalidArgument, "dice config status is invalid")
	}
	if config.FeeBPS < 0 || config.FeeBPS >= 10_000 {
		return Config{}, xerr.New(xerr.InvalidArgument, "dice fee_bps is invalid")
	}
	if config.FeeBPS == 0 {
		config.FeeBPS = int32(DefaultFeeBPS)
	}
	if config.PoolBPS < 0 || int64(config.FeeBPS)+int64(config.PoolBPS) >= 10_000 {
		return Config{}, xerr.New(xerr.InvalidArgument, "dice pool_bps is invalid")
	}
	if config.PoolBPS == 0 {
		config.PoolBPS = int32(DefaultPoolBPS)
	}
	minPlayers, maxPlayers, err := NormalizePlayerBounds(config.MinPlayers, config.MaxPlayers)
	if err != nil {
		return Config{}, err
	}
	config.MinPlayers = minPlayers
	config.MaxPlayers = maxPlayers
	if config.RobotMatchWaitMS <= 0 {
		config.RobotMatchWaitMS = DefaultRobotWaitMS
	}
	config.StakeOptions = NormalizeStakeOptions(config.StakeOptions)
	return config, nil
}

// NormalizeStakeOptions 去重、过滤非法档位并保持后台排序；空配置给出本地可用的保底档位。
func NormalizeStakeOptions(options []StakeOption) []StakeOption {
	if len(options) == 0 {
		return []StakeOption{
			{StakeCoin: 100, Enabled: true, SortOrder: 10},
			{StakeCoin: 500, Enabled: true, SortOrder: 20},
			{StakeCoin: 1000, Enabled: true, SortOrder: 30},
			{StakeCoin: 8000, Enabled: true, SortOrder: 40},
		}
	}
	seen := make(map[int64]struct{}, len(options))
	out := make([]StakeOption, 0, len(options))
	for _, option := range options {
		if option.StakeCoin <= 0 {
			continue
		}
		if _, ok := seen[option.StakeCoin]; ok {
			continue
		}
		seen[option.StakeCoin] = struct{}{}
		out = append(out, option)
	}
	sort.SliceStable(out, func(left int, right int) bool {
		if out[left].SortOrder == out[right].SortOrder {
			return out[left].StakeCoin < out[right].StakeCoin
		}
		return out[left].SortOrder > out[right].SortOrder
	})
	if len(out) == 0 {
		return NormalizeStakeOptions(nil)
	}
	return out
}

// StakeEnabled 明确校验下注金额是否来自后台启用档位，避免 H5 篡改 stake_coin 打出任意金额。
func StakeEnabled(config Config, stakeCoin int64) bool {
	for _, option := range config.StakeOptions {
		if option.StakeCoin == stakeCoin && option.Enabled {
			return true
		}
	}
	return false
}

// StakeOptionsJSON 把后台档位保存成稳定 JSON；仓储不做字符串拼接，避免后续字段扩展破坏解析。
func StakeOptionsJSON(options []StakeOption) string {
	raw, err := json.Marshal(NormalizeStakeOptions(options))
	if err != nil {
		return "[]"
	}
	return string(raw)
}

// ParseStakeOptions 解析数据库 JSON；坏配置回退为空，后续 NormalizeConfig 会补安全默认。
func ParseStakeOptions(raw string) []StakeOption {
	var options []StakeOption
	if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &options); err != nil {
		return nil
	}
	return NormalizeStakeOptions(options)
}

// NormalizePlayerBounds 把客户端可调人数压回当前可承载范围；多人扩展只需要调上限，不需要换表。
func NormalizePlayerBounds(minPlayers int32, maxPlayers int32) (int32, int32, error) {
	// 客户端不传人数时走产品默认值；传了非法值则直接拒绝，不在后端静默纠正成另一种玩法。
	if minPlayers <= 0 {
		minPlayers = DefaultMinPlayers
	}
	if maxPlayers <= 0 {
		maxPlayers = DefaultMaxPlayers
	}
	if minPlayers < 2 {
		return 0, 0, xerr.New(xerr.InvalidArgument, "dice min_players must be at least 2")
	}
	if maxPlayers < minPlayers {
		return 0, 0, xerr.New(xerr.InvalidArgument, "dice max_players must be greater than or equal to min_players")
	}
	if maxPlayers > HardMaxPlayers {
		return 0, 0, xerr.New(xerr.InvalidArgument, "dice max_players is too large")
	}
	return minPlayers, maxPlayers, nil
}

// Score 只读取单颗骰子点数；校验长度可以防止旧三骰数据或未投骰数据被当作新规则结算。
func Score(points []int32) (int32, error) {
	// 点数必须来自服务端随机或已落库事实；长度不对说明本局还没完成投骰，不能推导结果。
	if len(points) != PointsPerParticipant {
		return 0, xerr.New(xerr.InvalidArgument, "dice points are incomplete")
	}
	point := points[0]
	if point < 1 || point > 6 {
		return 0, xerr.New(xerr.InvalidArgument, "dice point is invalid")
	}
	return point, nil
}

// IsRPSGameID 标识复用 self-game match 表的真实石头剪刀布玩法；room-rps 有独立领域模型，不走这里。
func IsRPSGameID(gameID string) bool {
	return strings.TrimSpace(gameID) == SelfGameIDRock
}

// NormalizeRPSGesture 把 H5 输入收口为三种稳定英文枚举；后续落库、结算和响应都只使用这三个值。
func NormalizeRPSGesture(value string) (string, bool) {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case RPSGestureRock:
		return RPSGestureRock, true
	case RPSGesturePaper:
		return RPSGesturePaper, true
	case RPSGestureScissors:
		return RPSGestureScissors, true
	default:
		return "", false
	}
}

// RPSGesturePoint 只给 rps 结算事实写入 dice_points_json 做幂等标记和轻量展示兜底，不参与胜负判断。
func RPSGesturePoint(gesture string) (int32, bool) {
	switch strings.TrimSpace(gesture) {
	case RPSGestureRock:
		return 1, true
	case RPSGesturePaper:
		return 2, true
	case RPSGestureScissors:
		return 3, true
	default:
		return 0, false
	}
}

// RPSWinningGestureFor 返回能击败入参的手势；机器人奖池不足强制玩家输时只改机器人预选，不改真人事实。
func RPSWinningGestureFor(gesture string) (string, bool) {
	switch strings.TrimSpace(gesture) {
	case RPSGestureRock:
		return RPSGesturePaper, true
	case RPSGesturePaper:
		return RPSGestureScissors, true
	case RPSGestureScissors:
		return RPSGestureRock, true
	default:
		return "", false
	}
}

// RPSLosingGestureFor 返回会输给入参的手势；新手局需要玩家必赢时只改机器人预选，不改真人已经提交的手势。
func RPSLosingGestureFor(gesture string) (string, bool) {
	switch strings.TrimSpace(gesture) {
	case RPSGestureRock:
		return RPSGestureScissors, true
	case RPSGesturePaper:
		return RPSGestureRock, true
	case RPSGestureScissors:
		return RPSGesturePaper, true
	default:
		return "", false
	}
}

// rpsCompare 返回 left 相对 right 的胜负：1 表示 left 赢，-1 表示 left 输，0 表示和局。
func rpsCompare(left string, right string) (int, error) {
	left = strings.TrimSpace(left)
	right = strings.TrimSpace(right)
	if _, ok := RPSGesturePoint(left); !ok {
		return 0, xerr.New(xerr.InvalidArgument, "rps gesture is invalid")
	}
	if _, ok := RPSGesturePoint(right); !ok {
		return 0, xerr.New(xerr.InvalidArgument, "rps gesture is invalid")
	}
	if left == right {
		return 0, nil
	}
	if left == RPSGestureRock && right == RPSGestureScissors ||
		left == RPSGesturePaper && right == RPSGestureRock ||
		left == RPSGestureScissors && right == RPSGesturePaper {
		return 1, nil
	}
	return -1, nil
}

// SettleRPSByGestureWithPool 使用双方已经落库的预选手势结算；rock 不随机、不重选，和局直接退还双方本金并结束本局。
func SettleRPSByGestureWithPool(participants []Participant, feeBPS int64, poolBPS int64) ([]Participant, string, error) {
	// 真实石头剪刀布当前产品只开放 2 人对战；多人局没有明确胜负和分账语义，必须在规则层拒绝。
	if len(participants) != int(DefaultMinPlayers) {
		return nil, "", xerr.New(xerr.Conflict, "rps match requires two participants")
	}
	if feeBPS < 0 || feeBPS >= 10_000 {
		return nil, "", xerr.New(xerr.InvalidArgument, "dice fee_bps is invalid")
	}
	if poolBPS < 0 || feeBPS+poolBPS >= 10_000 {
		return nil, "", xerr.New(xerr.InvalidArgument, "dice pool_bps is invalid")
	}
	settled := append([]Participant(nil), participants...)
	sort.SliceStable(settled, func(left int, right int) bool {
		return settled[left].SeatNo < settled[right].SeatNo
	})

	var totalStake int64
	for index := range settled {
		// rps 的手势必须来自 match/create/join 阶段的预选事实；roll 阶段不能补造或重随机。
		gesture, ok := NormalizeRPSGesture(settled[index].RPSGesture)
		if !ok {
			return nil, "", xerr.New(xerr.InvalidArgument, "rps gesture is invalid")
		}
		if settled[index].StakeCoin <= 0 {
			return nil, "", xerr.New(xerr.InvalidArgument, "dice stake_coin is invalid")
		}
		point, _ := RPSGesturePoint(gesture)
		settled[index].RPSGesture = gesture
		settled[index].DicePoints = []int32{point}
		settled[index].Result = ParticipantResultLose
		settled[index].PayoutCoin = 0
		totalStake += settled[index].StakeCoin
	}

	compare, err := rpsCompare(settled[0].RPSGesture, settled[1].RPSGesture)
	if err != nil {
		return nil, "", err
	}
	if compare == 0 {
		// 预选手势相同就是本局和局；双方已扣本金后各退回自己的 stake_coin，不进入平台费或奖池抽成。
		for index := range settled {
			settled[index].Result = ParticipantResultDraw
			settled[index].PayoutCoin = settled[index].StakeCoin
		}
		return settled, ParticipantResultDraw, nil
	}

	winnerIndex := 0
	if compare < 0 {
		winnerIndex = 1
	}
	// 非和局沿用 self-game 经济模型：总奖池先扣平台费和入池比例，剩余整数金额一次性给赢家。
	payoutPool := totalStake * (10_000 - feeBPS - poolBPS) / 10_000
	settled[winnerIndex].Result = ParticipantResultWin
	settled[winnerIndex].PayoutCoin = payoutPool
	return settled, ParticipantResultWin, nil
}

// SettleByHighestScore 使用单骰最高点数结算；全部平局只标记 draw，由 service 保持同一局继续重投。
func SettleByHighestScore(participants []Participant, feeBPS int64) ([]Participant, string, error) {
	return SettleByHighestScoreWithPool(participants, feeBPS, 0)
}

// SettleByHighestScoreWithPool 使用单骰最高点数结算，并把平台抽水和入池比例都从总奖池里扣出。
func SettleByHighestScoreWithPool(participants []Participant, feeBPS int64, poolBPS int64) ([]Participant, string, error) {
	// 规则层再次兜底人数，避免绕过 service 或仓储测试桩时把未成局数据结算成有效账务。
	if len(participants) < int(DefaultMinPlayers) {
		return nil, "", xerr.New(xerr.Conflict, "dice match does not have enough participants")
	}
	if feeBPS < 0 || feeBPS >= 10_000 {
		return nil, "", xerr.New(xerr.InvalidArgument, "dice fee_bps is invalid")
	}
	if poolBPS < 0 || feeBPS+poolBPS >= 10_000 {
		return nil, "", xerr.New(xerr.InvalidArgument, "dice pool_bps is invalid")
	}
	settled := append([]Participant(nil), participants...)
	// 按座位稳定排序后再输出结果，客户端刷新时参与者顺序不会因为 map/查询顺序变化而跳动。
	sort.SliceStable(settled, func(left int, right int) bool {
		return settled[left].SeatNo < settled[right].SeatNo
	})

	var highScore int32
	winnerIndexes := make([]int, 0, len(settled))
	for index := range settled {
		// 每个参与者先取单骰点数，同时借 Score 校验 dice_points_json 是否完整合法。
		score, err := Score(settled[index].DicePoints)
		if err != nil {
			return nil, "", err
		}
		if index == 0 || score > highScore {
			highScore = score
			winnerIndexes = winnerIndexes[:0]
			winnerIndexes = append(winnerIndexes, index)
			continue
		}
		if score == highScore {
			winnerIndexes = append(winnerIndexes, index)
		}
	}

	if len(winnerIndexes) == len(settled) {
		// 全员最高分相同视为和局；首版产品要求 2 秒后继续扔，所以这里不产生 payout，也不进入平台费和奖池抽成。
		for index := range settled {
			settled[index].Result = ParticipantResultDraw
			settled[index].PayoutCoin = 0
		}
		return settled, ParticipantResultDraw, nil
	}

	var totalStake int64
	for index := range settled {
		// 押注必须是正数；一旦有坏账务快照，整局停止结算，由上层标记 failed 等待人工或补偿。
		if settled[index].StakeCoin <= 0 {
			return nil, "", xerr.New(xerr.InvalidArgument, "dice stake_coin is invalid")
		}
		totalStake += settled[index].StakeCoin
		settled[index].Result = ParticipantResultLose
		settled[index].PayoutCoin = 0
	}
	// 非和局时先扣平台费和入池金额，再把整数奖池按最高分赢家均分；除不尽的余数留在平台费侧，避免多发币。
	payoutPool := totalStake * (10_000 - feeBPS - poolBPS) / 10_000
	share := payoutPool / int64(len(winnerIndexes))
	for _, index := range winnerIndexes {
		settled[index].Result = ParticipantResultWin
		settled[index].PayoutCoin = share
	}
	return settled, ParticipantResultWin, nil
}

// HasRolls 判断本局是否已经保存过骰子点数；重试结算必须复用旧点数，不能重新随机。
func HasRolls(participants []Participant) bool {
	for _, participant := range participants {
		if len(participant.DicePoints) > 0 {
			return true
		}
	}
	return false
}
