package transcoder

import (
	"encoding/json"
	"errors"
	"fmt"

	model "galaxy/model/order"
	commonfbs "galaxy/schema/fbs/common"
	fbs "galaxy/schema/fbs/order"

	flatbuffers "github.com/google/flatbuffers/go"
	"github.com/google/uuid"
)

// JSONToUserGamesOrder decodes the engine's JSON response body for
// `PUT /api/v1/order` and `GET /api/v1/order` into the typed
// `*model.UserGamesOrder`. The model's `Commands` field is an
// interface (`order.DecodableCommand`), so plain `json.Unmarshal`
// can't reach it — this helper performs the same per-`@type`
// dispatch as `game/internal/repo.ParseOrder`, but stays inside the
// shared transcoder so non-engine callers (the gateway, tests) can
// reuse it without crossing module boundaries.
func JSONToUserGamesOrder(payload []byte) (*model.UserGamesOrder, error) {
	if len(payload) == 0 {
		return nil, errors.New("decode user games order json: payload is empty")
	}
	var raw struct {
		GameID    string            `json:"game_id"`
		UpdatedAt int64             `json:"updatedAt"`
		Commands  []json.RawMessage `json:"cmd"`
	}
	if err := json.Unmarshal(payload, &raw); err != nil {
		return nil, fmt.Errorf("decode user games order json: %w", err)
	}
	out := &model.UserGamesOrder{
		UpdatedAt: raw.UpdatedAt,
	}
	if raw.GameID != "" {
		gameID, err := uuid.Parse(raw.GameID)
		if err != nil {
			return nil, fmt.Errorf("decode user games order json: invalid game_id %q: %w", raw.GameID, err)
		}
		out.GameID = gameID
	}
	if len(raw.Commands) == 0 {
		return out, nil
	}
	out.Commands = make([]model.DecodableCommand, len(raw.Commands))
	for i, rawCmd := range raw.Commands {
		cmd, err := decodeJSONCommand(rawCmd)
		if err != nil {
			return nil, fmt.Errorf("decode user games order json command %d: %w", i, err)
		}
		out.Commands[i] = cmd
	}
	return out, nil
}

func decodeJSONCommand(raw json.RawMessage) (model.DecodableCommand, error) {
	meta := new(model.CommandMeta)
	if err := json.Unmarshal(raw, meta); err != nil {
		return nil, err
	}
	switch meta.CmdType {
	case model.CommandTypeRaceQuit:
		return unmarshalJSONCommand(raw, new(model.CommandRaceQuit))
	case model.CommandTypeRaceVote:
		return unmarshalJSONCommand(raw, new(model.CommandRaceVote))
	case model.CommandTypeRaceRelation:
		return unmarshalJSONCommand(raw, new(model.CommandRaceRelation))
	case model.CommandTypeShipClassCreate:
		return unmarshalJSONCommand(raw, new(model.CommandShipClassCreate))
	case model.CommandTypeShipClassMerge:
		return unmarshalJSONCommand(raw, new(model.CommandShipClassMerge))
	case model.CommandTypeShipClassRemove:
		return unmarshalJSONCommand(raw, new(model.CommandShipClassRemove))
	case model.CommandTypeShipGroupBreak:
		return unmarshalJSONCommand(raw, new(model.CommandShipGroupBreak))
	case model.CommandTypeShipGroupLoad:
		return unmarshalJSONCommand(raw, new(model.CommandShipGroupLoad))
	case model.CommandTypeShipGroupUnload:
		return unmarshalJSONCommand(raw, new(model.CommandShipGroupUnload))
	case model.CommandTypeShipGroupSend:
		return unmarshalJSONCommand(raw, new(model.CommandShipGroupSend))
	case model.CommandTypeShipGroupUpgrade:
		return unmarshalJSONCommand(raw, new(model.CommandShipGroupUpgrade))
	case model.CommandTypeShipGroupMerge:
		return unmarshalJSONCommand(raw, new(model.CommandShipGroupMerge))
	case model.CommandTypeShipGroupDismantle:
		return unmarshalJSONCommand(raw, new(model.CommandShipGroupDismantle))
	case model.CommandTypeShipGroupTransfer:
		return unmarshalJSONCommand(raw, new(model.CommandShipGroupTransfer))
	case model.CommandTypeShipGroupJoinFleet:
		return unmarshalJSONCommand(raw, new(model.CommandShipGroupJoinFleet))
	case model.CommandTypeFleetMerge:
		return unmarshalJSONCommand(raw, new(model.CommandFleetMerge))
	case model.CommandTypeFleetSend:
		return unmarshalJSONCommand(raw, new(model.CommandFleetSend))
	case model.CommandTypeScienceCreate:
		return unmarshalJSONCommand(raw, new(model.CommandScienceCreate))
	case model.CommandTypeScienceRemove:
		return unmarshalJSONCommand(raw, new(model.CommandScienceRemove))
	case model.CommandTypePlanetRename:
		return unmarshalJSONCommand(raw, new(model.CommandPlanetRename))
	case model.CommandTypePlanetProduce:
		return unmarshalJSONCommand(raw, new(model.CommandPlanetProduce))
	case model.CommandTypePlanetRouteSet:
		return unmarshalJSONCommand(raw, new(model.CommandPlanetRouteSet))
	case model.CommandTypePlanetRouteRemove:
		return unmarshalJSONCommand(raw, new(model.CommandPlanetRouteRemove))
	default:
		return nil, fmt.Errorf("unknown command type: %s", meta.CmdType)
	}
}

func unmarshalJSONCommand[T model.DecodableCommand](raw json.RawMessage, v T) (model.DecodableCommand, error) {
	if err := json.Unmarshal(raw, v); err != nil {
		return nil, err
	}
	return v, nil
}

type encodedCommand struct {
	cmdID         string
	cmdApplied    *bool
	cmdErrCode    *int
	payloadType   fbs.CommandPayload
	payloadOffset flatbuffers.UOffsetT
}

func encodeOrderCommand(builder *flatbuffers.Builder, command model.DecodableCommand, index int) (encodedCommand, error) {
	if command == nil {
		return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil", index)
	}

	switch cmd := command.(type) {
	case *model.CommandRaceQuit:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		fbs.CommandRaceQuitStart(builder)
		payload := fbs.CommandRaceQuitEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandRaceQuit, payload), nil
	case *model.CommandRaceVote:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		acceptor := builder.CreateString(cmd.Acceptor)
		fbs.CommandRaceVoteStart(builder)
		fbs.CommandRaceVoteAddAcceptor(builder, acceptor)
		payload := fbs.CommandRaceVoteEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandRaceVote, payload), nil
	case *model.CommandRaceRelation:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		relation, err := relationToFBS(cmd.Relation)
		if err != nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
		}
		acceptor := builder.CreateString(cmd.Acceptor)
		fbs.CommandRaceRelationStart(builder)
		fbs.CommandRaceRelationAddAcceptor(builder, acceptor)
		fbs.CommandRaceRelationAddRelation(builder, relation)
		payload := fbs.CommandRaceRelationEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandRaceRelation, payload), nil
	case *model.CommandShipClassCreate:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		name := builder.CreateString(cmd.Name)
		fbs.CommandShipClassCreateStart(builder)
		fbs.CommandShipClassCreateAddName(builder, name)
		fbs.CommandShipClassCreateAddDrive(builder, cmd.Drive)
		fbs.CommandShipClassCreateAddArmament(builder, int64(cmd.Armament))
		fbs.CommandShipClassCreateAddWeapons(builder, cmd.Weapons)
		fbs.CommandShipClassCreateAddShields(builder, cmd.Shields)
		fbs.CommandShipClassCreateAddCargo(builder, cmd.Cargo)
		payload := fbs.CommandShipClassCreateEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipClassCreate, payload), nil
	case *model.CommandShipClassMerge:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		name := builder.CreateString(cmd.Name)
		target := builder.CreateString(cmd.Target)
		fbs.CommandShipClassMergeStart(builder)
		fbs.CommandShipClassMergeAddName(builder, name)
		fbs.CommandShipClassMergeAddTarget(builder, target)
		payload := fbs.CommandShipClassMergeEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipClassMerge, payload), nil
	case *model.CommandShipClassRemove:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		name := builder.CreateString(cmd.Name)
		fbs.CommandShipClassRemoveStart(builder)
		fbs.CommandShipClassRemoveAddName(builder, name)
		payload := fbs.CommandShipClassRemoveEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipClassRemove, payload), nil
	case *model.CommandShipGroupBreak:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		id := builder.CreateString(cmd.ID)
		newID := builder.CreateString(cmd.NewID)
		fbs.CommandShipGroupBreakStart(builder)
		fbs.CommandShipGroupBreakAddId(builder, id)
		fbs.CommandShipGroupBreakAddNewId(builder, newID)
		fbs.CommandShipGroupBreakAddQuantity(builder, int64(cmd.Quantity))
		payload := fbs.CommandShipGroupBreakEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupBreak, payload), nil
	case *model.CommandShipGroupLoad:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		cargo, err := shipGroupCargoToFBS(cmd.Cargo)
		if err != nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
		}
		id := builder.CreateString(cmd.ID)
		fbs.CommandShipGroupLoadStart(builder)
		fbs.CommandShipGroupLoadAddId(builder, id)
		fbs.CommandShipGroupLoadAddCargo(builder, cargo)
		fbs.CommandShipGroupLoadAddQuantity(builder, cmd.Quantity)
		payload := fbs.CommandShipGroupLoadEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupLoad, payload), nil
	case *model.CommandShipGroupUnload:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		id := builder.CreateString(cmd.ID)
		fbs.CommandShipGroupUnloadStart(builder)
		fbs.CommandShipGroupUnloadAddId(builder, id)
		fbs.CommandShipGroupUnloadAddQuantity(builder, cmd.Quantity)
		payload := fbs.CommandShipGroupUnloadEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupUnload, payload), nil
	case *model.CommandShipGroupSend:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		id := builder.CreateString(cmd.ID)
		fbs.CommandShipGroupSendStart(builder)
		fbs.CommandShipGroupSendAddId(builder, id)
		fbs.CommandShipGroupSendAddDestination(builder, int64(cmd.Destination))
		payload := fbs.CommandShipGroupSendEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupSend, payload), nil
	case *model.CommandShipGroupUpgrade:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		tech, err := shipGroupUpgradeTechToFBS(cmd.Tech)
		if err != nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
		}
		id := builder.CreateString(cmd.ID)
		fbs.CommandShipGroupUpgradeStart(builder)
		fbs.CommandShipGroupUpgradeAddId(builder, id)
		fbs.CommandShipGroupUpgradeAddTech(builder, tech)
		fbs.CommandShipGroupUpgradeAddLevel(builder, cmd.Level)
		payload := fbs.CommandShipGroupUpgradeEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupUpgrade, payload), nil
	case *model.CommandShipGroupMerge:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		fbs.CommandShipGroupMergeStart(builder)
		payload := fbs.CommandShipGroupMergeEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupMerge, payload), nil
	case *model.CommandShipGroupDismantle:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		id := builder.CreateString(cmd.ID)
		fbs.CommandShipGroupDismantleStart(builder)
		fbs.CommandShipGroupDismantleAddId(builder, id)
		payload := fbs.CommandShipGroupDismantleEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupDismantle, payload), nil
	case *model.CommandShipGroupTransfer:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		id := builder.CreateString(cmd.ID)
		acceptor := builder.CreateString(cmd.Acceptor)
		fbs.CommandShipGroupTransferStart(builder)
		fbs.CommandShipGroupTransferAddId(builder, id)
		fbs.CommandShipGroupTransferAddAcceptor(builder, acceptor)
		payload := fbs.CommandShipGroupTransferEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupTransfer, payload), nil
	case *model.CommandShipGroupJoinFleet:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		id := builder.CreateString(cmd.ID)
		name := builder.CreateString(cmd.Name)
		fbs.CommandShipGroupJoinFleetStart(builder)
		fbs.CommandShipGroupJoinFleetAddId(builder, id)
		fbs.CommandShipGroupJoinFleetAddName(builder, name)
		payload := fbs.CommandShipGroupJoinFleetEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupJoinFleet, payload), nil
	case *model.CommandFleetMerge:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		name := builder.CreateString(cmd.Name)
		target := builder.CreateString(cmd.Target)
		fbs.CommandFleetMergeStart(builder)
		fbs.CommandFleetMergeAddName(builder, name)
		fbs.CommandFleetMergeAddTarget(builder, target)
		payload := fbs.CommandFleetMergeEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandFleetMerge, payload), nil
	case *model.CommandFleetSend:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		name := builder.CreateString(cmd.Name)
		fbs.CommandFleetSendStart(builder)
		fbs.CommandFleetSendAddName(builder, name)
		fbs.CommandFleetSendAddDestination(builder, int64(cmd.Destination))
		payload := fbs.CommandFleetSendEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandFleetSend, payload), nil
	case *model.CommandScienceCreate:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		name := builder.CreateString(cmd.Name)
		fbs.CommandScienceCreateStart(builder)
		fbs.CommandScienceCreateAddName(builder, name)
		fbs.CommandScienceCreateAddDrive(builder, cmd.Drive)
		fbs.CommandScienceCreateAddWeapons(builder, cmd.Weapons)
		fbs.CommandScienceCreateAddShields(builder, cmd.Shields)
		fbs.CommandScienceCreateAddCargo(builder, cmd.Cargo)
		payload := fbs.CommandScienceCreateEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandScienceCreate, payload), nil
	case *model.CommandScienceRemove:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		name := builder.CreateString(cmd.Name)
		fbs.CommandScienceRemoveStart(builder)
		fbs.CommandScienceRemoveAddName(builder, name)
		payload := fbs.CommandScienceRemoveEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandScienceRemove, payload), nil
	case *model.CommandPlanetRename:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		name := builder.CreateString(cmd.Name)
		fbs.CommandPlanetRenameStart(builder)
		fbs.CommandPlanetRenameAddNumber(builder, int64(cmd.Number))
		fbs.CommandPlanetRenameAddName(builder, name)
		payload := fbs.CommandPlanetRenameEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandPlanetRename, payload), nil
	case *model.CommandPlanetProduce:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		production, err := planetProductionToFBS(cmd.Production)
		if err != nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
		}
		subject := builder.CreateString(cmd.Subject)
		fbs.CommandPlanetProduceStart(builder)
		fbs.CommandPlanetProduceAddNumber(builder, int64(cmd.Number))
		fbs.CommandPlanetProduceAddProduction(builder, production)
		fbs.CommandPlanetProduceAddSubject(builder, subject)
		payload := fbs.CommandPlanetProduceEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandPlanetProduce, payload), nil
	case *model.CommandPlanetRouteSet:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		loadType, err := planetRouteLoadTypeToFBS(cmd.LoadType)
		if err != nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
		}
		fbs.CommandPlanetRouteSetStart(builder)
		fbs.CommandPlanetRouteSetAddOrigin(builder, int64(cmd.Origin))
		fbs.CommandPlanetRouteSetAddDestination(builder, int64(cmd.Destination))
		fbs.CommandPlanetRouteSetAddLoadType(builder, loadType)
		payload := fbs.CommandPlanetRouteSetEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandPlanetRouteSet, payload), nil
	case *model.CommandPlanetRouteRemove:
		if cmd == nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
		}
		loadType, err := planetRouteLoadTypeToFBS(cmd.LoadType)
		if err != nil {
			return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
		}
		fbs.CommandPlanetRouteRemoveStart(builder)
		fbs.CommandPlanetRouteRemoveAddOrigin(builder, int64(cmd.Origin))
		fbs.CommandPlanetRouteRemoveAddLoadType(builder, loadType)
		payload := fbs.CommandPlanetRouteRemoveEnd(builder)
		return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandPlanetRouteRemove, payload), nil
	default:
		return encodedCommand{}, fmt.Errorf("encode order command %d: unsupported command type %T", index, command)
	}
}

func encodedCommandFromMeta(meta model.CommandMeta, payloadType fbs.CommandPayload, payloadOffset flatbuffers.UOffsetT) encodedCommand {
	return encodedCommand{
		cmdID:         meta.CmdID,
		cmdApplied:    cloneBoolPointer(meta.CmdApplied),
		cmdErrCode:    cloneIntPointer(meta.CmdErrCode),
		payloadType:   payloadType,
		payloadOffset: payloadOffset,
	}
}

func decodeOrderCommand(flatCommand *fbs.CommandItem, index int) (model.DecodableCommand, error) {
	commandMeta := model.CommandMeta{
		CmdID:      string(flatCommand.CmdId()),
		CmdApplied: cloneBoolPointer(flatCommand.CmdApplied()),
	}

	if cmdErrCode := flatCommand.CmdErrorCode(); cmdErrCode != nil {
		decodedCmdErrCode, err := int64ToInt(*cmdErrCode, "cmd_error_code")
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}
		commandMeta.CmdErrCode = &decodedCmdErrCode
	}

	payloadType := flatCommand.PayloadType()
	if payloadType == fbs.CommandPayloadNONE {
		return nil, fmt.Errorf("decode order command %d: payload type is NONE", index)
	}

	payload := new(flatbuffers.Table)
	if !flatCommand.Payload(payload) {
		return nil, fmt.Errorf("decode order command %d: payload is missing", index)
	}

	switch payloadType {
	case fbs.CommandPayloadCommandRaceQuit:
		commandMeta.CmdType = model.CommandTypeRaceQuit
		return &model.CommandRaceQuit{CommandMeta: commandMeta}, nil
	case fbs.CommandPayloadCommandRaceVote:
		commandMeta.CmdType = model.CommandTypeRaceVote
		commandPayload := new(fbs.CommandRaceVote)
		commandPayload.Init(payload.Bytes, payload.Pos)
		return &model.CommandRaceVote{
			CommandMeta: commandMeta,
			Acceptor:    string(commandPayload.Acceptor()),
		}, nil
	case fbs.CommandPayloadCommandRaceRelation:
		commandPayload := new(fbs.CommandRaceRelation)
		commandPayload.Init(payload.Bytes, payload.Pos)

		relation, err := relationFromFBS(commandPayload.Relation())
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}

		commandMeta.CmdType = model.CommandTypeRaceRelation
		return &model.CommandRaceRelation{
			CommandMeta: commandMeta,
			Acceptor:    string(commandPayload.Acceptor()),
			Relation:    relation,
		}, nil
	case fbs.CommandPayloadCommandShipClassCreate:
		commandMeta.CmdType = model.CommandTypeShipClassCreate
		commandPayload := new(fbs.CommandShipClassCreate)
		commandPayload.Init(payload.Bytes, payload.Pos)
		armament, err := int64ToInt(commandPayload.Armament(), "armament")
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}
		return &model.CommandShipClassCreate{
			CommandMeta: commandMeta,
			Name:        string(commandPayload.Name()),
			Drive:       commandPayload.Drive(),
			Armament:    armament,
			Weapons:     commandPayload.Weapons(),
			Shields:     commandPayload.Shields(),
			Cargo:       commandPayload.Cargo(),
		}, nil
	case fbs.CommandPayloadCommandShipClassMerge:
		commandMeta.CmdType = model.CommandTypeShipClassMerge
		commandPayload := new(fbs.CommandShipClassMerge)
		commandPayload.Init(payload.Bytes, payload.Pos)
		return &model.CommandShipClassMerge{
			CommandMeta: commandMeta,
			Name:        string(commandPayload.Name()),
			Target:      string(commandPayload.Target()),
		}, nil
	case fbs.CommandPayloadCommandShipClassRemove:
		commandMeta.CmdType = model.CommandTypeShipClassRemove
		commandPayload := new(fbs.CommandShipClassRemove)
		commandPayload.Init(payload.Bytes, payload.Pos)
		return &model.CommandShipClassRemove{
			CommandMeta: commandMeta,
			Name:        string(commandPayload.Name()),
		}, nil
	case fbs.CommandPayloadCommandShipGroupBreak:
		commandMeta.CmdType = model.CommandTypeShipGroupBreak
		commandPayload := new(fbs.CommandShipGroupBreak)
		commandPayload.Init(payload.Bytes, payload.Pos)
		quantity, err := int64ToInt(commandPayload.Quantity(), "quantity")
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}
		return &model.CommandShipGroupBreak{
			CommandMeta: commandMeta,
			ID:          string(commandPayload.Id()),
			NewID:       string(commandPayload.NewId()),
			Quantity:    quantity,
		}, nil
	case fbs.CommandPayloadCommandShipGroupLoad:
		commandPayload := new(fbs.CommandShipGroupLoad)
		commandPayload.Init(payload.Bytes, payload.Pos)

		cargo, err := shipGroupCargoFromFBS(commandPayload.Cargo())
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}

		commandMeta.CmdType = model.CommandTypeShipGroupLoad
		return &model.CommandShipGroupLoad{
			CommandMeta: commandMeta,
			ID:          string(commandPayload.Id()),
			Cargo:       cargo,
			Quantity:    commandPayload.Quantity(),
		}, nil
	case fbs.CommandPayloadCommandShipGroupUnload:
		commandMeta.CmdType = model.CommandTypeShipGroupUnload
		commandPayload := new(fbs.CommandShipGroupUnload)
		commandPayload.Init(payload.Bytes, payload.Pos)
		return &model.CommandShipGroupUnload{
			CommandMeta: commandMeta,
			ID:          string(commandPayload.Id()),
			Quantity:    commandPayload.Quantity(),
		}, nil
	case fbs.CommandPayloadCommandShipGroupSend:
		commandMeta.CmdType = model.CommandTypeShipGroupSend
		commandPayload := new(fbs.CommandShipGroupSend)
		commandPayload.Init(payload.Bytes, payload.Pos)
		destination, err := int64ToInt(commandPayload.Destination(), "destination")
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}
		return &model.CommandShipGroupSend{
			CommandMeta: commandMeta,
			ID:          string(commandPayload.Id()),
			Destination: destination,
		}, nil
	case fbs.CommandPayloadCommandShipGroupUpgrade:
		commandPayload := new(fbs.CommandShipGroupUpgrade)
		commandPayload.Init(payload.Bytes, payload.Pos)

		tech, err := shipGroupUpgradeTechFromFBS(commandPayload.Tech())
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}

		commandMeta.CmdType = model.CommandTypeShipGroupUpgrade
		return &model.CommandShipGroupUpgrade{
			CommandMeta: commandMeta,
			ID:          string(commandPayload.Id()),
			Tech:        tech,
			Level:       commandPayload.Level(),
		}, nil
	case fbs.CommandPayloadCommandShipGroupMerge:
		commandMeta.CmdType = model.CommandTypeShipGroupMerge
		return &model.CommandShipGroupMerge{CommandMeta: commandMeta}, nil
	case fbs.CommandPayloadCommandShipGroupDismantle:
		commandMeta.CmdType = model.CommandTypeShipGroupDismantle
		commandPayload := new(fbs.CommandShipGroupDismantle)
		commandPayload.Init(payload.Bytes, payload.Pos)
		return &model.CommandShipGroupDismantle{
			CommandMeta: commandMeta,
			ID:          string(commandPayload.Id()),
		}, nil
	case fbs.CommandPayloadCommandShipGroupTransfer:
		commandMeta.CmdType = model.CommandTypeShipGroupTransfer
		commandPayload := new(fbs.CommandShipGroupTransfer)
		commandPayload.Init(payload.Bytes, payload.Pos)
		return &model.CommandShipGroupTransfer{
			CommandMeta: commandMeta,
			ID:          string(commandPayload.Id()),
			Acceptor:    string(commandPayload.Acceptor()),
		}, nil
	case fbs.CommandPayloadCommandShipGroupJoinFleet:
		commandMeta.CmdType = model.CommandTypeShipGroupJoinFleet
		commandPayload := new(fbs.CommandShipGroupJoinFleet)
		commandPayload.Init(payload.Bytes, payload.Pos)
		return &model.CommandShipGroupJoinFleet{
			CommandMeta: commandMeta,
			ID:          string(commandPayload.Id()),
			Name:        string(commandPayload.Name()),
		}, nil
	case fbs.CommandPayloadCommandFleetMerge:
		commandMeta.CmdType = model.CommandTypeFleetMerge
		commandPayload := new(fbs.CommandFleetMerge)
		commandPayload.Init(payload.Bytes, payload.Pos)
		return &model.CommandFleetMerge{
			CommandMeta: commandMeta,
			Name:        string(commandPayload.Name()),
			Target:      string(commandPayload.Target()),
		}, nil
	case fbs.CommandPayloadCommandFleetSend:
		commandMeta.CmdType = model.CommandTypeFleetSend
		commandPayload := new(fbs.CommandFleetSend)
		commandPayload.Init(payload.Bytes, payload.Pos)
		destination, err := int64ToInt(commandPayload.Destination(), "destination")
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}
		return &model.CommandFleetSend{
			CommandMeta: commandMeta,
			Name:        string(commandPayload.Name()),
			Destination: destination,
		}, nil
	case fbs.CommandPayloadCommandScienceCreate:
		commandMeta.CmdType = model.CommandTypeScienceCreate
		commandPayload := new(fbs.CommandScienceCreate)
		commandPayload.Init(payload.Bytes, payload.Pos)
		return &model.CommandScienceCreate{
			CommandMeta: commandMeta,
			Name:        string(commandPayload.Name()),
			Drive:       commandPayload.Drive(),
			Weapons:     commandPayload.Weapons(),
			Shields:     commandPayload.Shields(),
			Cargo:       commandPayload.Cargo(),
		}, nil
	case fbs.CommandPayloadCommandScienceRemove:
		commandMeta.CmdType = model.CommandTypeScienceRemove
		commandPayload := new(fbs.CommandScienceRemove)
		commandPayload.Init(payload.Bytes, payload.Pos)
		return &model.CommandScienceRemove{
			CommandMeta: commandMeta,
			Name:        string(commandPayload.Name()),
		}, nil
	case fbs.CommandPayloadCommandPlanetRename:
		commandMeta.CmdType = model.CommandTypePlanetRename
		commandPayload := new(fbs.CommandPlanetRename)
		commandPayload.Init(payload.Bytes, payload.Pos)
		number, err := int64ToInt(commandPayload.Number(), "number")
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}
		return &model.CommandPlanetRename{
			CommandMeta: commandMeta,
			Number:      number,
			Name:        string(commandPayload.Name()),
		}, nil
	case fbs.CommandPayloadCommandPlanetProduce:
		commandPayload := new(fbs.CommandPlanetProduce)
		commandPayload.Init(payload.Bytes, payload.Pos)

		production, err := planetProductionFromFBS(commandPayload.Production())
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}
		number, err := int64ToInt(commandPayload.Number(), "number")
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}

		commandMeta.CmdType = model.CommandTypePlanetProduce
		return &model.CommandPlanetProduce{
			CommandMeta: commandMeta,
			Number:      number,
			Production:  production,
			Subject:     string(commandPayload.Subject()),
		}, nil
	case fbs.CommandPayloadCommandPlanetRouteSet:
		commandPayload := new(fbs.CommandPlanetRouteSet)
		commandPayload.Init(payload.Bytes, payload.Pos)

		loadType, err := planetRouteLoadTypeFromFBS(commandPayload.LoadType())
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}
		origin, err := int64ToInt(commandPayload.Origin(), "origin")
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}
		destination, err := int64ToInt(commandPayload.Destination(), "destination")
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}

		commandMeta.CmdType = model.CommandTypePlanetRouteSet
		return &model.CommandPlanetRouteSet{
			CommandMeta: commandMeta,
			Origin:      origin,
			Destination: destination,
			LoadType:    loadType,
		}, nil
	case fbs.CommandPayloadCommandPlanetRouteRemove:
		commandPayload := new(fbs.CommandPlanetRouteRemove)
		commandPayload.Init(payload.Bytes, payload.Pos)

		loadType, err := planetRouteLoadTypeFromFBS(commandPayload.LoadType())
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}
		origin, err := int64ToInt(commandPayload.Origin(), "origin")
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", index, err)
		}

		commandMeta.CmdType = model.CommandTypePlanetRouteRemove
		return &model.CommandPlanetRouteRemove{
			CommandMeta: commandMeta,
			Origin:      origin,
			LoadType:    loadType,
		}, nil
	default:
		return nil, fmt.Errorf("decode order command %d: unknown command payload type %d", index, payloadType)
	}
}

// int64ToInt narrows v to a Go int. Returns an error when v overflows
// the platform `int` range (only possible on 32-bit builds; on 64-bit
// the check is a no-op). fieldName is used in the error for caller
// context.
func int64ToInt(value int64, field string) (int, error) {
	maxInt := int64(int(^uint(0) >> 1))
	minInt := -maxInt - 1

	if value < minInt || value > maxInt {
		return 0, fmt.Errorf("%s value %d overflows int", field, value)
	}

	return int(value), nil
}

func relationToFBS(value string) (fbs.Relation, error) {
	switch value {
	case "WAR":
		return fbs.RelationWAR, nil
	case "PEACE":
		return fbs.RelationPEACE, nil
	default:
		return fbs.RelationUNKNOWN, fmt.Errorf("unsupported relation value %q", value)
	}
}

func relationFromFBS(value fbs.Relation) (string, error) {
	switch value {
	case fbs.RelationWAR:
		return "WAR", nil
	case fbs.RelationPEACE:
		return "PEACE", nil
	case fbs.RelationUNKNOWN:
		return "", errors.New("relation value UNKNOWN is not allowed")
	default:
		return "", fmt.Errorf("unsupported relation enum value %d", value)
	}
}

func shipGroupCargoToFBS(value string) (fbs.ShipGroupCargo, error) {
	switch value {
	case "COL":
		return fbs.ShipGroupCargoCOL, nil
	case "MAT":
		return fbs.ShipGroupCargoMAT, nil
	case "CAP":
		return fbs.ShipGroupCargoCAP, nil
	default:
		return fbs.ShipGroupCargoUNKNOWN, fmt.Errorf("unsupported ship group cargo value %q", value)
	}
}

func shipGroupCargoFromFBS(value fbs.ShipGroupCargo) (string, error) {
	switch value {
	case fbs.ShipGroupCargoCOL:
		return "COL", nil
	case fbs.ShipGroupCargoMAT:
		return "MAT", nil
	case fbs.ShipGroupCargoCAP:
		return "CAP", nil
	case fbs.ShipGroupCargoUNKNOWN:
		return "", errors.New("ship group cargo value UNKNOWN is not allowed")
	default:
		return "", fmt.Errorf("unsupported ship group cargo enum value %d", value)
	}
}

func shipGroupUpgradeTechToFBS(value string) (fbs.ShipGroupUpgradeTech, error) {
	switch value {
	case "ALL":
		return fbs.ShipGroupUpgradeTechALL, nil
	case "DRIVE":
		return fbs.ShipGroupUpgradeTechDRIVE, nil
	case "WEAPONS":
		return fbs.ShipGroupUpgradeTechWEAPONS, nil
	case "SHIELDS":
		return fbs.ShipGroupUpgradeTechSHIELDS, nil
	case "CARGO":
		return fbs.ShipGroupUpgradeTechCARGO, nil
	default:
		return fbs.ShipGroupUpgradeTechUNKNOWN, fmt.Errorf("unsupported ship group upgrade tech value %q", value)
	}
}

func shipGroupUpgradeTechFromFBS(value fbs.ShipGroupUpgradeTech) (string, error) {
	switch value {
	case fbs.ShipGroupUpgradeTechALL:
		return "ALL", nil
	case fbs.ShipGroupUpgradeTechDRIVE:
		return "DRIVE", nil
	case fbs.ShipGroupUpgradeTechWEAPONS:
		return "WEAPONS", nil
	case fbs.ShipGroupUpgradeTechSHIELDS:
		return "SHIELDS", nil
	case fbs.ShipGroupUpgradeTechCARGO:
		return "CARGO", nil
	case fbs.ShipGroupUpgradeTechUNKNOWN:
		return "", errors.New("ship group upgrade tech value UNKNOWN is not allowed")
	default:
		return "", fmt.Errorf("unsupported ship group upgrade tech enum value %d", value)
	}
}

func planetProductionToFBS(value string) (fbs.PlanetProduction, error) {
	switch value {
	case "MAT":
		return fbs.PlanetProductionMAT, nil
	case "CAP":
		return fbs.PlanetProductionCAP, nil
	case "DRIVE":
		return fbs.PlanetProductionDRIVE, nil
	case "WEAPONS":
		return fbs.PlanetProductionWEAPONS, nil
	case "SHIELDS":
		return fbs.PlanetProductionSHIELDS, nil
	case "CARGO":
		return fbs.PlanetProductionCARGO, nil
	case "SCIENCE":
		return fbs.PlanetProductionSCIENCE, nil
	case "SHIP":
		return fbs.PlanetProductionSHIP, nil
	default:
		return fbs.PlanetProductionUNKNOWN, fmt.Errorf("unsupported planet production value %q", value)
	}
}

func planetProductionFromFBS(value fbs.PlanetProduction) (string, error) {
	switch value {
	case fbs.PlanetProductionMAT:
		return "MAT", nil
	case fbs.PlanetProductionCAP:
		return "CAP", nil
	case fbs.PlanetProductionDRIVE:
		return "DRIVE", nil
	case fbs.PlanetProductionWEAPONS:
		return "WEAPONS", nil
	case fbs.PlanetProductionSHIELDS:
		return "SHIELDS", nil
	case fbs.PlanetProductionCARGO:
		return "CARGO", nil
	case fbs.PlanetProductionSCIENCE:
		return "SCIENCE", nil
	case fbs.PlanetProductionSHIP:
		return "SHIP", nil
	case fbs.PlanetProductionUNKNOWN:
		return "", errors.New("planet production value UNKNOWN is not allowed")
	default:
		return "", fmt.Errorf("unsupported planet production enum value %d", value)
	}
}

func planetRouteLoadTypeToFBS(value string) (fbs.PlanetRouteLoadType, error) {
	switch value {
	case "MAT":
		return fbs.PlanetRouteLoadTypeMAT, nil
	case "CAP":
		return fbs.PlanetRouteLoadTypeCAP, nil
	case "COL":
		return fbs.PlanetRouteLoadTypeCOL, nil
	case "EMP":
		return fbs.PlanetRouteLoadTypeEMP, nil
	default:
		return fbs.PlanetRouteLoadTypeUNKNOWN, fmt.Errorf("unsupported planet route load type value %q", value)
	}
}

func planetRouteLoadTypeFromFBS(value fbs.PlanetRouteLoadType) (string, error) {
	switch value {
	case fbs.PlanetRouteLoadTypeMAT:
		return "MAT", nil
	case fbs.PlanetRouteLoadTypeCAP:
		return "CAP", nil
	case fbs.PlanetRouteLoadTypeCOL:
		return "COL", nil
	case fbs.PlanetRouteLoadTypeEMP:
		return "EMP", nil
	case fbs.PlanetRouteLoadTypeUNKNOWN:
		return "", errors.New("planet route load type value UNKNOWN is not allowed")
	default:
		return "", fmt.Errorf("unsupported planet route load type enum value %d", value)
	}
}

func cloneBoolPointer(value *bool) *bool {
	if value == nil {
		return nil
	}

	cloned := *value
	return &cloned
}

func cloneIntPointer(value *int) *int {
	if value == nil {
		return nil
	}

	cloned := *value
	return &cloned
}

// UserGamesCommandToPayload converts model.UserGamesCommand to
// FlatBuffers bytes suitable for the authenticated gateway transport.
// `GameID` is required.
func UserGamesCommandToPayload(req *model.UserGamesCommand) ([]byte, error) {
	if req == nil {
		return nil, errors.New("encode user games command payload: request is nil")
	}

	builder := flatbuffers.NewBuilder(1024)
	commandsVec, err := encodeCommandItemVector(builder, req.Commands, "user games command")
	if err != nil {
		return nil, err
	}

	fbs.UserGamesCommandStart(builder)
	hi, lo := uuidToHiLo(req.GameID)
	fbs.UserGamesCommandAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
	if commandsVec != 0 {
		fbs.UserGamesCommandAddCommands(builder, commandsVec)
	}
	offset := fbs.UserGamesCommandEnd(builder)
	fbs.FinishUserGamesCommandBuffer(builder, offset)

	return builder.FinishedBytes(), nil
}

// PayloadToUserGamesCommand converts FlatBuffers payload bytes into
// model.UserGamesCommand.
func PayloadToUserGamesCommand(data []byte) (result *model.UserGamesCommand, err error) {
	if len(data) == 0 {
		return nil, errors.New("decode user games command payload: data is empty")
	}

	defer func() {
		if recovered := recover(); recovered != nil {
			result = nil
			err = fmt.Errorf("decode user games command payload: panic recovered: %v", recovered)
		}
	}()

	flat := fbs.GetRootAsUserGamesCommand(data, 0)
	gameID := flat.GameId(nil)
	if gameID == nil {
		return nil, errors.New("decode user games command payload: game_id is missing")
	}
	out := &model.UserGamesCommand{
		GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
	}
	count := flat.CommandsLength()
	if count > 0 {
		out.Commands = make([]model.DecodableCommand, count)
		flatCommand := new(fbs.CommandItem)
		for i := 0; i < count; i++ {
			if !flat.Commands(flatCommand, i) {
				return nil, fmt.Errorf("decode user games command %d: command item is missing", i)
			}
			cmd, decodeErr := decodeOrderCommand(flatCommand, i)
			if decodeErr != nil {
				return nil, decodeErr
			}
			out.Commands[i] = cmd
		}
	}
	return out, nil
}

// UserGamesOrderToPayload converts model.UserGamesOrder to FlatBuffers
// bytes suitable for the authenticated gateway transport.
func UserGamesOrderToPayload(req *model.UserGamesOrder) ([]byte, error) {
	if req == nil {
		return nil, errors.New("encode user games order payload: request is nil")
	}

	builder := flatbuffers.NewBuilder(1024)
	commandsVec, err := encodeCommandItemVector(builder, req.Commands, "user games order")
	if err != nil {
		return nil, err
	}

	fbs.UserGamesOrderStart(builder)
	hi, lo := uuidToHiLo(req.GameID)
	fbs.UserGamesOrderAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
	fbs.UserGamesOrderAddUpdatedAt(builder, int64(req.UpdatedAt))
	if commandsVec != 0 {
		fbs.UserGamesOrderAddCommands(builder, commandsVec)
	}
	offset := fbs.UserGamesOrderEnd(builder)
	fbs.FinishUserGamesOrderBuffer(builder, offset)

	return builder.FinishedBytes(), nil
}

// PayloadToUserGamesOrder converts FlatBuffers payload bytes into
// model.UserGamesOrder.
func PayloadToUserGamesOrder(data []byte) (result *model.UserGamesOrder, err error) {
	if len(data) == 0 {
		return nil, errors.New("decode user games order payload: data is empty")
	}

	defer func() {
		if recovered := recover(); recovered != nil {
			result = nil
			err = fmt.Errorf("decode user games order payload: panic recovered: %v", recovered)
		}
	}()

	flat := fbs.GetRootAsUserGamesOrder(data, 0)
	gameID := flat.GameId(nil)
	if gameID == nil {
		return nil, errors.New("decode user games order payload: game_id is missing")
	}
	// updatedAt, convErr := int64ToInt(flat.UpdatedAt(), "updated_at")
	// if convErr != nil {
	// 	return nil, fmt.Errorf("decode user games order payload: %w", convErr)
	// }
	out := &model.UserGamesOrder{
		GameID:    uuidFromHiLo(gameID.Hi(), gameID.Lo()),
		UpdatedAt: flat.UpdatedAt(),
	}
	count := flat.CommandsLength()
	if count > 0 {
		out.Commands = make([]model.DecodableCommand, count)
		flatCommand := new(fbs.CommandItem)
		for i := 0; i < count; i++ {
			if !flat.Commands(flatCommand, i) {
				return nil, fmt.Errorf("decode user games order %d: command item is missing", i)
			}
			cmd, decodeErr := decodeOrderCommand(flatCommand, i)
			if decodeErr != nil {
				return nil, decodeErr
			}
			out.Commands[i] = cmd
		}
	}
	return out, nil
}

// EmptyUserGamesCommandResponsePayload returns a FlatBuffers-encoded
// empty `UserGamesCommandResponse` buffer. Used by gateway to ack a
// successful `MessageTypeUserGamesCommand` even though the engine
// returns 204 No Content — the typed envelope keeps the message-type
// contract symmetric with other authenticated routes.
func EmptyUserGamesCommandResponsePayload() []byte {
	builder := flatbuffers.NewBuilder(16)
	fbs.UserGamesCommandResponseStart(builder)
	offset := fbs.UserGamesCommandResponseEnd(builder)
	fbs.FinishUserGamesCommandResponseBuffer(builder, offset)
	return builder.FinishedBytes()
}

// UserGamesOrderResponseToPayload encodes the engine's response body
// for `PUT /api/v1/order` into the wire FlatBuffers envelope expected
// for `MessageTypeUserGamesOrder`. The engine populates per-command
// `cmdApplied` / `cmdErrorCode` fields, and they round-trip into the
// FBS `CommandItem` entries unchanged. A nil response is encoded as
// an empty envelope so the gateway can fall back to a batch-level
// "ok" answer when the engine body is unavailable.
func UserGamesOrderResponseToPayload(req *model.UserGamesOrder) ([]byte, error) {
	builder := flatbuffers.NewBuilder(1024)
	if req == nil {
		fbs.UserGamesOrderResponseStart(builder)
		offset := fbs.UserGamesOrderResponseEnd(builder)
		fbs.FinishUserGamesOrderResponseBuffer(builder, offset)
		return builder.FinishedBytes(), nil
	}

	commandsVec, err := encodeCommandItemVector(builder, req.Commands, "user games order response")
	if err != nil {
		return nil, err
	}

	fbs.UserGamesOrderResponseStart(builder)
	hi, lo := uuidToHiLo(req.GameID)
	fbs.UserGamesOrderResponseAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
	fbs.UserGamesOrderResponseAddUpdatedAt(builder, req.UpdatedAt)
	if commandsVec != 0 {
		fbs.UserGamesOrderResponseAddCommands(builder, commandsVec)
	}
	offset := fbs.UserGamesOrderResponseEnd(builder)
	fbs.FinishUserGamesOrderResponseBuffer(builder, offset)
	return builder.FinishedBytes(), nil
}

// PayloadToUserGamesOrderGet decodes the FlatBuffers payload of
// `MessageTypeUserGamesOrderGet` into the typed model. `Turn` is
// validated to be non-negative; the gateway and backend reject
// negative values before forwarding to the engine.
func PayloadToUserGamesOrderGet(data []byte) (result *model.UserGamesOrderGet, err error) {
	if len(data) == 0 {
		return nil, errors.New("decode user games order get payload: data is empty")
	}

	defer func() {
		if recovered := recover(); recovered != nil {
			result = nil
			err = fmt.Errorf("decode user games order get payload: panic recovered: %v", recovered)
		}
	}()

	flat := fbs.GetRootAsUserGamesOrderGet(data, 0)
	gameID := flat.GameId(nil)
	if gameID == nil {
		return nil, errors.New("decode user games order get payload: game_id is missing")
	}
	turn := flat.Turn()
	if turn < 0 {
		return nil, fmt.Errorf("decode user games order get payload: turn must be non-negative, got %d", turn)
	}
	return &model.UserGamesOrderGet{
		GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
		Turn:   int(turn),
	}, nil
}

// UserGamesOrderGetToPayload encodes a `model.UserGamesOrderGet`
// request into FlatBuffers bytes suitable for the authenticated
// gateway transport.
func UserGamesOrderGetToPayload(req *model.UserGamesOrderGet) ([]byte, error) {
	if req == nil {
		return nil, errors.New("encode user games order get payload: request is nil")
	}
	if req.Turn < 0 {
		return nil, fmt.Errorf("encode user games order get payload: turn must be non-negative, got %d", req.Turn)
	}
	builder := flatbuffers.NewBuilder(64)
	fbs.UserGamesOrderGetStart(builder)
	hi, lo := uuidToHiLo(req.GameID)
	fbs.UserGamesOrderGetAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
	fbs.UserGamesOrderGetAddTurn(builder, int64(req.Turn))
	offset := fbs.UserGamesOrderGetEnd(builder)
	fbs.FinishUserGamesOrderGetBuffer(builder, offset)
	return builder.FinishedBytes(), nil
}

// UserGamesOrderGetResponseToPayload encodes the typed response of
// `MessageTypeUserGamesOrderGet`. `found = false` corresponds to the
// engine's `204 No Content` answer; `order` is omitted in that case.
func UserGamesOrderGetResponseToPayload(order *model.UserGamesOrder, found bool) ([]byte, error) {
	builder := flatbuffers.NewBuilder(1024)

	var orderOffset flatbuffers.UOffsetT
	if found && order != nil {
		commandsVec, err := encodeCommandItemVector(builder, order.Commands, "user games order get response")
		if err != nil {
			return nil, err
		}
		fbs.UserGamesOrderStart(builder)
		hi, lo := uuidToHiLo(order.GameID)
		fbs.UserGamesOrderAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
		fbs.UserGamesOrderAddUpdatedAt(builder, order.UpdatedAt)
		if commandsVec != 0 {
			fbs.UserGamesOrderAddCommands(builder, commandsVec)
		}
		orderOffset = fbs.UserGamesOrderEnd(builder)
	}

	fbs.UserGamesOrderGetResponseStart(builder)
	fbs.UserGamesOrderGetResponseAddFound(builder, found)
	if orderOffset != 0 {
		fbs.UserGamesOrderGetResponseAddOrder(builder, orderOffset)
	}
	offset := fbs.UserGamesOrderGetResponseEnd(builder)
	fbs.FinishUserGamesOrderGetResponseBuffer(builder, offset)
	return builder.FinishedBytes(), nil
}

// PayloadToUserGamesOrderResponse decodes the engine's PUT response
// envelope into a typed `*UserGamesOrder`. Empty payloads decode to
// nil so callers can fall back to batch-level handling without a
// dedicated marker.
func PayloadToUserGamesOrderResponse(data []byte) (result *model.UserGamesOrder, err error) {
	if len(data) == 0 {
		return nil, nil
	}

	defer func() {
		if recovered := recover(); recovered != nil {
			result = nil
			err = fmt.Errorf("decode user games order response payload: panic recovered: %v", recovered)
		}
	}()

	flat := fbs.GetRootAsUserGamesOrderResponse(data, 0)
	gameID := flat.GameId(nil)
	if gameID == nil {
		// Empty envelope (gateway fallback). The caller treats this
		// as "no per-command detail" and synthesises a batch-level
		// answer.
		if flat.CommandsLength() == 0 {
			return nil, nil
		}
		return nil, errors.New("decode user games order response payload: game_id is missing")
	}
	out := &model.UserGamesOrder{
		GameID:    uuidFromHiLo(gameID.Hi(), gameID.Lo()),
		UpdatedAt: flat.UpdatedAt(),
	}
	count := flat.CommandsLength()
	if count > 0 {
		out.Commands = make([]model.DecodableCommand, count)
		flatCommand := new(fbs.CommandItem)
		for i := 0; i < count; i++ {
			if !flat.Commands(flatCommand, i) {
				return nil, fmt.Errorf("decode user games order response %d: command item is missing", i)
			}
			cmd, decodeErr := decodeOrderCommand(flatCommand, i)
			if decodeErr != nil {
				return nil, decodeErr
			}
			out.Commands[i] = cmd
		}
	}
	return out, nil
}

// PayloadToUserGamesOrderGetResponse decodes the FlatBuffers response
// of `MessageTypeUserGamesOrderGet`. When `found = false`, returns
// `(nil, false, nil)` matching the engine's `204 No Content`
// semantics.
func PayloadToUserGamesOrderGetResponse(data []byte) (order *model.UserGamesOrder, found bool, err error) {
	if len(data) == 0 {
		return nil, false, errors.New("decode user games order get response payload: data is empty")
	}

	defer func() {
		if recovered := recover(); recovered != nil {
			order = nil
			found = false
			err = fmt.Errorf("decode user games order get response payload: panic recovered: %v", recovered)
		}
	}()

	flat := fbs.GetRootAsUserGamesOrderGetResponse(data, 0)
	if !flat.Found() {
		return nil, false, nil
	}
	inner := flat.Order(nil)
	if inner == nil {
		return nil, true, errors.New("decode user games order get response payload: order is missing while found=true")
	}
	gameID := inner.GameId(nil)
	if gameID == nil {
		return nil, true, errors.New("decode user games order get response payload: order.game_id is missing")
	}
	out := &model.UserGamesOrder{
		GameID:    uuidFromHiLo(gameID.Hi(), gameID.Lo()),
		UpdatedAt: inner.UpdatedAt(),
	}
	count := inner.CommandsLength()
	if count > 0 {
		out.Commands = make([]model.DecodableCommand, count)
		flatCommand := new(fbs.CommandItem)
		for i := 0; i < count; i++ {
			if !inner.Commands(flatCommand, i) {
				return nil, true, fmt.Errorf("decode user games order get response %d: command item is missing", i)
			}
			cmd, decodeErr := decodeOrderCommand(flatCommand, i)
			if decodeErr != nil {
				return nil, true, decodeErr
			}
			out.Commands[i] = cmd
		}
	}
	return out, true, nil
}

// encodeCommandItemVector serialises a slice of DecodableCommand into a
// FlatBuffers vector of CommandItem. Used by UserGamesCommandToPayload
// and UserGamesOrderToPayload to keep the per-command encoding logic in
// one place.
func encodeCommandItemVector(builder *flatbuffers.Builder, commands []model.DecodableCommand, opLabel string) (flatbuffers.UOffsetT, error) {
	offsets := make([]flatbuffers.UOffsetT, len(commands))
	for i := range commands {
		encoded, err := encodeOrderCommand(builder, commands[i], i)
		if err != nil {
			return 0, fmt.Errorf("encode %s: %w", opLabel, err)
		}
		cmdID := builder.CreateString(encoded.cmdID)
		fbs.CommandItemStart(builder)
		fbs.CommandItemAddCmdId(builder, cmdID)
		if encoded.cmdApplied != nil {
			fbs.CommandItemAddCmdApplied(builder, *encoded.cmdApplied)
		}
		if encoded.cmdErrCode != nil {
			fbs.CommandItemAddCmdErrorCode(builder, int64(*encoded.cmdErrCode))
		}
		fbs.CommandItemAddPayloadType(builder, encoded.payloadType)
		fbs.CommandItemAddPayload(builder, encoded.payloadOffset)
		offsets[i] = fbs.CommandItemEnd(builder)
	}
	if len(offsets) == 0 {
		return 0, nil
	}
	// `UserGamesCommandStartCommandsVector` and the corresponding
	// `UserGamesOrderStartCommandsVector` are identical helpers (both
	// expand to `builder.StartVector(4, numElems, 4)`); we use the
	// command flavour for both message types so the helper has a
	// single dependency point.
	fbs.UserGamesCommandStartCommandsVector(builder, len(offsets))
	for i := len(offsets) - 1; i >= 0; i-- {
		builder.PrependUOffsetT(offsets[i])
	}
	return builder.EndVector(len(offsets)), nil
}
