// Package transcode is the gateway's FlatBuffers<->REST bridge. Each message type
// maps to a handler that decodes the FlatBuffers request payload, calls the
// backend over REST, and encodes the FlatBuffers response. The registry is the
// authoritative message_type catalog; new operations are added here following the
// same vertical-slice pattern.
package transcode

import (
	"context"
	"encoding/json"
	"errors"
	"net/url"

	"scrabble/gateway/internal/backendclient"
	"scrabble/gateway/internal/connector"
	"scrabble/gateway/internal/vkauth"
	"scrabble/gateway/internal/vkid"
	fb "scrabble/pkg/fbs/scrabblefb"
)

// Message types in the vertical slice.
const (
	MsgAuthTelegram         = "auth.telegram"
	MsgAuthVK               = "auth.vk"
	MsgAuthGuest            = "auth.guest"
	MsgAuthEmailReq         = "auth.email.request"
	MsgAuthEmailLogin       = "auth.email.login"
	MsgAuthEmailConfirmLink = "auth.email.confirm_link"
	MsgProfileGet           = "profile.get"
	MsgBlockStatus          = "account.block_status"
	MsgGameSubmitPlay       = "game.submit_play"
	MsgGameState            = "game.state"
	MsgLobbyEnqueue         = "lobby.enqueue"
	MsgLobbyCancel          = "lobby.cancel"
	MsgLobbyPoll            = "lobby.poll"
	MsgChatPost             = "chat.post"
	MsgGamesList            = "games.list"
	MsgGamePass             = "game.pass"
	MsgGameExchange         = "game.exchange"
	MsgGameResign           = "game.resign"
	MsgGameHint             = "game.hint"
	MsgGameEvaluate         = "game.evaluate"
	MsgGameCheckWord        = "game.check_word"
	MsgGameComplaint        = "game.complaint"
	MsgGameHistory          = "game.history"
	MsgChatList             = "chat.list"
	MsgChatNudge            = "chat.nudge"
	MsgChatRead             = "chat.read"
	MsgDraftGet             = "draft.get"
	MsgDraftSave            = "draft.save"
	MsgGameHide             = "game.hide"
	MsgFeedbackSubmit       = "feedback.submit"
	MsgFeedbackGet          = "feedback.get"
	MsgFeedbackUnread       = "feedback.unread"
	MsgWalletGet            = "wallet.get"
	MsgWalletCatalog        = "wallet.catalog"
	MsgWalletBuy            = "wallet.buy"
	MsgWalletOrder          = "wallet.order"
	MsgWalletReward         = "wallet.reward"
)

// Request is one decoded Execute call.
type Request struct {
	Payload  []byte
	UserID   string // resolved account id; empty for auth (unauthenticated) ops
	ClientIP string
}

// Handler runs one operation and returns the FlatBuffers response payload.
type Handler func(ctx context.Context, req Request) ([]byte, error)

// Op is a registered message type and its policy flags.
type Op struct {
	Handler Handler
	// Auth marks an operation that requires a resolved session (X-User-ID).
	Auth bool
	// Email marks the costly email-code path that gets a stricter rate sub-limit.
	Email bool
	// NonGuest marks an operation a guest account may not perform; the gateway
	// rejects it with the guest_forbidden result code after resolving the session,
	// before any backend call.
	NonGuest bool
}

// Registry maps message types to their operations.
type Registry struct {
	ops map[string]Op
}

// TelegramValidator validates Telegram credentials via the connector side-service:
// Mini App launch data (auth) and Login Widget data (linking).
// *connector.Client implements it; a nil value disables the telegram auth and
// telegram-link paths.
type TelegramValidator interface {
	ValidateInitData(ctx context.Context, initData string) (connector.User, error)
	ValidateLoginWidget(ctx context.Context, data string) (connector.User, error)
}

// NewRegistry builds the slice's message-type catalog over the backend client.
// The Telegram auth op is registered only when a validator is supplied (the
// connector is configured); otherwise auth.telegram is simply unknown. Optional ops
// (e.g. WithVKAuth) are applied last from opts.
func NewRegistry(backend *backendclient.Client, tg TelegramValidator, opts ...Option) *Registry {
	r := &Registry{ops: make(map[string]Op)}
	if tg != nil {
		r.ops[MsgAuthTelegram] = Op{Handler: authTelegramHandler(backend, tg)}
	}
	r.ops[MsgAuthGuest] = Op{Handler: authGuestHandler(backend)}
	r.ops[MsgAuthEmailReq] = Op{Handler: authEmailRequestHandler(backend), Email: true}
	r.ops[MsgAuthEmailLogin] = Op{Handler: authEmailLoginHandler(backend), Email: true}
	r.ops[MsgAuthEmailConfirmLink] = Op{Handler: authEmailConfirmLinkHandler(backend)}
	r.ops[MsgProfileGet] = Op{Handler: profileHandler(backend), Auth: true}
	r.ops[MsgWalletGet] = Op{Handler: walletHandler(backend), Auth: true}
	r.ops[MsgWalletCatalog] = Op{Handler: walletCatalogHandler(backend), Auth: true}
	r.ops[MsgWalletBuy] = Op{Handler: walletBuyHandler(backend), Auth: true}
	r.ops[MsgWalletOrder] = Op{Handler: walletOrderHandler(backend, nil), Auth: true}
	r.ops[MsgWalletReward] = Op{Handler: walletRewardHandler(backend), Auth: true}
	r.ops[MsgBlockStatus] = Op{Handler: blockStatusHandler(backend), Auth: true}
	r.ops[MsgGameSubmitPlay] = Op{Handler: submitPlayHandler(backend), Auth: true}
	r.ops[MsgGameState] = Op{Handler: gameStateHandler(backend), Auth: true}
	r.ops[MsgLobbyEnqueue] = Op{Handler: enqueueHandler(backend), Auth: true}
	r.ops[MsgLobbyCancel] = Op{Handler: cancelHandler(backend), Auth: true}
	r.ops[MsgLobbyPoll] = Op{Handler: pollHandler(backend), Auth: true}
	r.ops[MsgChatPost] = Op{Handler: chatPostHandler(backend), Auth: true}
	r.ops[MsgGamesList] = Op{Handler: gamesListHandler(backend), Auth: true}
	r.ops[MsgGamePass] = Op{Handler: passHandler(backend), Auth: true}
	r.ops[MsgGameExchange] = Op{Handler: exchangeHandler(backend), Auth: true}
	r.ops[MsgGameResign] = Op{Handler: resignHandler(backend), Auth: true}
	r.ops[MsgGameHint] = Op{Handler: hintHandler(backend), Auth: true}
	r.ops[MsgGameEvaluate] = Op{Handler: evaluateHandler(backend), Auth: true}
	r.ops[MsgGameCheckWord] = Op{Handler: checkWordHandler(backend), Auth: true}
	r.ops[MsgGameComplaint] = Op{Handler: complaintHandler(backend), Auth: true}
	r.ops[MsgGameHistory] = Op{Handler: historyHandler(backend), Auth: true}
	r.ops[MsgChatList] = Op{Handler: chatListHandler(backend), Auth: true}
	r.ops[MsgChatNudge] = Op{Handler: nudgeHandler(backend), Auth: true}
	r.ops[MsgChatRead] = Op{Handler: markChatReadHandler(backend), Auth: true}
	r.ops[MsgDraftGet] = Op{Handler: getDraftHandler(backend), Auth: true}
	r.ops[MsgDraftSave] = Op{Handler: saveDraftHandler(backend), Auth: true}
	r.ops[MsgGameHide] = Op{Handler: hideGameHandler(backend), Auth: true}
	r.ops[MsgFeedbackSubmit] = Op{Handler: feedbackSubmitHandler(backend), Auth: true, NonGuest: true}
	r.ops[MsgFeedbackGet] = Op{Handler: feedbackGetHandler(backend), Auth: true}
	r.ops[MsgFeedbackUnread] = Op{Handler: feedbackUnreadHandler(backend), Auth: true}
	registerSocialOps(r, backend)
	registerLinkOps(r, backend, tg)
	for _, opt := range opts {
		opt(r, backend)
	}
	return r
}

// Option configures an optional registry operation at construction. It is kept out of
// NewRegistry's positional signature so existing call sites stay unaffected.
type Option func(r *Registry, backend *backendclient.Client)

// WithVKAuth registers the VK Mini App auth op (auth.vk), which verifies launch params
// in-process under the VK app secret. A blank secret leaves auth.vk unregistered, so
// the op is simply unknown wherever VK is not configured.
func WithVKAuth(secret string) Option {
	return func(r *Registry, backend *backendclient.Client) {
		if secret != "" {
			r.ops[MsgAuthVK] = Op{Handler: authVKHandler(backend, secret)}
		}
	}
}

// WithVKLink registers the VK web-link ops (link.vk.confirm/merge), which complete a
// browser VK ID authorization via the server-side code exchange. A nil exchanger leaves
// them unregistered, so the ops are unknown wherever VK ID web login is not configured.
func WithVKLink(ex VKIDExchanger) Option {
	return func(r *Registry, backend *backendclient.Client) {
		registerVKLinkOps(r, backend, ex)
	}
}

// WithTelegramStars re-registers the wallet.order op with a Telegram Stars invoice minter (the
// bot-link), so a Telegram-context order mints its Stars invoice link through the bot. A nil minter
// is a no-op, leaving the default (Stars-unavailable) handler in place.
func WithTelegramStars(minter InvoiceMinter) Option {
	return func(r *Registry, backend *backendclient.Client) {
		if minter != nil {
			r.ops[MsgWalletOrder] = Op{Handler: walletOrderHandler(backend, minter), Auth: true}
		}
	}
}

// Lookup returns the operation for messageType, and whether it is registered.
func (r *Registry) Lookup(messageType string) (Op, bool) {
	op, ok := r.ops[messageType]
	return op, ok
}

// DomainMessage returns the human-readable, already-localized reason a backend domain error carries
// for the user (e.g. an operator's payment-unavailable explanation), or "" when it carries none. It
// rides the Execute envelope's message field alongside the result code.
func DomainMessage(err error) string {
	var apiErr *backendclient.APIError
	if errors.As(err, &apiErr) {
		return apiErr.Message
	}
	return ""
}

// DomainCode maps an error to a stable result code to surface in the Execute
// envelope, reporting false for an unexpected error the caller should treat as a
// transport-level internal failure.
func DomainCode(err error) (string, bool) {
	var apiErr *backendclient.APIError
	if errors.As(err, &apiErr) {
		return apiErr.Code, true
	}
	if errors.Is(err, connector.ErrInvalidInitData) {
		return "invalid_init_data", true
	}
	if errors.Is(err, vkauth.ErrInvalid) {
		return "invalid_vk_params", true
	}
	if errors.Is(err, vkid.ErrInvalid) {
		return "invalid_vk_id", true
	}
	if errors.Is(err, connector.ErrInvalidLoginWidget) {
		return "invalid_login_widget", true
	}
	return "", false
}

func authTelegramHandler(backend *backendclient.Client, tg TelegramValidator) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsTelegramLoginRequest(req.Payload, 0)
		initData := string(in.InitData())
		user, err := tg.ValidateInitData(ctx, initData)
		if err != nil {
			return nil, err
		}
		// start_param rides inside the signed initData validated just above, so the launch
		// deep-link payload can be trusted here without a separate wire field; the backend
		// uses it to seed a brand-new account's variant preferences.
		startParam := ""
		if q, perr := url.ParseQuery(initData); perr == nil {
			startParam = q.Get("start_param")
		}
		sess, err := backend.TelegramAuth(ctx, user.ExternalID, user.LanguageCode, user.Username, user.FirstName, string(in.BrowserTz()), startParam, string(in.Subtype()))
		if err != nil {
			return nil, err
		}
		return encodeSession(sess), nil
	}
}

// authVKHandler verifies a VK Mini App launch in-process (HMAC over the signed vk_*
// params under the app secret) and provisions/finds the bound account. Unlike Telegram,
// VK omits the user's name from the signed params, so the client-supplied display_name
// rides the wire as a cosmetic seed for a brand-new account.
func authVKHandler(backend *backendclient.Client, secret string) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsVKLoginRequest(req.Payload, 0)
		user, err := vkauth.Verify(string(in.Params()), secret)
		if err != nil {
			return nil, err
		}
		sess, err := backend.VKAuth(ctx, user.ExternalID, user.Language, string(in.DisplayName()), string(in.BrowserTz()), user.Subtype())
		if err != nil {
			return nil, err
		}
		return encodeSession(sess), nil
	}
}

func authGuestHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		// The guest bootstrap historically carried no payload; the detected zone is
		// optional, so an absent or empty one simply yields no time-zone seed (rather
		// than panicking in GetRootAs* on a zero-length buffer).
		var browserTz, subtype, locale string
		if len(req.Payload) > 0 {
			g := fb.GetRootAsGuestLoginRequest(req.Payload, 0)
			browserTz = string(g.BrowserTz())
			subtype = string(g.Subtype())
			locale = string(g.Locale())
		}
		sess, err := backend.GuestAuth(ctx, browserTz, subtype, locale)
		if err != nil {
			return nil, err
		}
		return encodeSession(sess), nil
	}
}

func authEmailRequestHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsEmailRequestRequest(req.Payload, 0)
		if err := backend.EmailRequest(ctx, string(in.Email()), string(in.BrowserTz()), string(in.Language()), in.Pwa()); err != nil {
			return nil, err
		}
		return encodeAck(true), nil
	}
}

func authEmailLoginHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsEmailLoginRequest(req.Payload, 0)
		sess, err := backend.EmailLogin(ctx, string(in.Email()), string(in.Code()), string(in.Subtype()))
		if err != nil {
			return nil, err
		}
		return encodeSession(sess), nil
	}
}

func authEmailConfirmLinkHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsEmailConfirmLinkRequest(req.Payload, 0)
		res, err := backend.EmailConfirmLink(ctx, string(in.Token()))
		if err != nil {
			return nil, err
		}
		return encodeConfirmLinkResult(res), nil
	}
}

func profileHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		p, err := backend.Profile(ctx, req.UserID)
		if err != nil {
			return nil, err
		}
		return encodeProfile(p), nil
	}
}

func walletHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		w, err := backend.Wallet(ctx, req.UserID)
		if err != nil {
			return nil, err
		}
		return encodeWallet(w), nil
	}
}

func walletCatalogHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		cat, err := backend.Catalog(ctx, req.UserID)
		if err != nil {
			return nil, err
		}
		return encodeCatalog(cat), nil
	}
}

func walletBuyHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsWalletBuyRequest(req.Payload, 0)
		w, err := backend.WalletBuy(ctx, req.UserID, string(in.ProductId()))
		if err != nil {
			return nil, err
		}
		return encodeWallet(w), nil
	}
}

// InvoiceMinter mints a Telegram Stars invoice link for a created order via the bot (only the bot
// reaches Telegram). The gateway bot-link Hub implements it; it is nil where the bot-link is off,
// which leaves the Telegram rail unavailable.
type InvoiceMinter interface {
	MintInvoice(ctx context.Context, title, description, orderID string, amountStars int64) (string, error)
}

// errTelegramStarsUnavailable is returned when a Telegram Stars order is requested but no invoice
// minter (the bot-link) is wired — the rail is not available on this gateway.
var errTelegramStarsUnavailable = errors.New("telegram stars rail unavailable")

func walletOrderHandler(backend *backendclient.Client, minter InvoiceMinter) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsWalletOrderRequest(req.Payload, 0)
		o, err := backend.WalletOrder(ctx, req.UserID, string(in.ProductId()))
		if err != nil {
			return nil, err
		}
		// Telegram Stars: mint the invoice link here via the bot-link and return it to the client
		// as the redirect URL it opens with WebApp.openInvoice. The title doubles as the invoice
		// description (both are required and the pack title is the whole product).
		if o.Rail == "telegram" {
			if minter == nil {
				return nil, errTelegramStarsUnavailable
			}
			link, merr := minter.MintInvoice(ctx, o.InvoiceTitle, o.InvoiceTitle, o.OrderID, o.InvoiceAmount)
			if merr != nil {
				return nil, merr
			}
			o.RedirectURL = link
		}
		return encodeWalletOrder(o), nil
	}
}

// walletRewardHandler credits a watched rewarded video and returns the updated wallet. The nonce is
// the client's per-view idempotency key.
func walletRewardHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsWalletRewardRequest(req.Payload, 0)
		w, err := backend.WalletReward(ctx, req.UserID, string(in.Nonce()))
		if err != nil {
			return nil, err
		}
		return encodeWallet(w), nil
	}
}

func blockStatusHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		bs, err := backend.BlockStatus(ctx, req.UserID)
		if err != nil {
			return nil, err
		}
		return encodeBlockStatus(bs), nil
	}
}

func submitPlayHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsSubmitPlayRequest(req.Payload, 0)
		res, err := backend.SubmitPlay(ctx, req.UserID, string(in.GameId()), decodeTiles(in))
		if err != nil {
			return nil, err
		}
		return encodeMoveResult(res), nil
	}
}

func gameStateHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsStateRequest(req.Payload, 0)
		st, err := backend.GameState(ctx, req.UserID, string(in.GameId()), in.IncludeAlphabet())
		if err != nil {
			return nil, err
		}
		return encodeState(st), nil
	}
}

func enqueueHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsEnqueueRequest(req.Payload, 0)
		m, err := backend.Enqueue(ctx, req.UserID, string(in.Variant()), in.MultipleWordsPerTurn(), in.VsAi())
		if err != nil {
			return nil, err
		}
		return encodeMatch(m), nil
	}
}

func pollHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		m, err := backend.Poll(ctx, req.UserID)
		if err != nil {
			return nil, err
		}
		return encodeMatch(m), nil
	}
}

// cancelHandler removes the caller from the auto-match pool. It carries no result;
// it echoes an empty (unmatched) Match so the client has a well-formed payload.
func cancelHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		if err := backend.Cancel(ctx, req.UserID); err != nil {
			return nil, err
		}
		return encodeMatch(backendclient.MatchResp{}), nil
	}
}

func chatPostHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsChatPostRequest(req.Payload, 0)
		c, err := backend.ChatPost(ctx, req.UserID, string(in.GameId()), string(in.Body()), req.ClientIP)
		if err != nil {
			return nil, err
		}
		return encodeChat(c), nil
	}
}

// decodeTiles reads the index-addressed tiles to place from a SubmitPlayRequest.
func decodeTiles(in *fb.SubmitPlayRequest) []backendclient.PlayTileJSON {
	n := in.TilesLength()
	tiles := make([]backendclient.PlayTileJSON, 0, n)
	var t fb.PlayTile
	for i := 0; i < n; i++ {
		if in.Tiles(&t, i) {
			tiles = append(tiles, backendclient.PlayTileJSON{
				Row:    int(t.Row()),
				Col:    int(t.Col()),
				Letter: int(t.Letter()),
				Blank:  t.Blank(),
			})
		}
	}
	return tiles
}

// decodeEvalTiles reads the index-addressed tentative tiles from an EvalRequest.
func decodeEvalTiles(in *fb.EvalRequest) []backendclient.PlayTileJSON {
	n := in.TilesLength()
	tiles := make([]backendclient.PlayTileJSON, 0, n)
	var t fb.PlayTile
	for i := 0; i < n; i++ {
		if in.Tiles(&t, i) {
			tiles = append(tiles, backendclient.PlayTileJSON{
				Row:    int(t.Row()),
				Col:    int(t.Col()),
				Letter: int(t.Letter()),
				Blank:  t.Blank(),
			})
		}
	}
	return tiles
}

// bytesToInts widens a FlatBuffers ubyte vector (an alphabet-index list) to []int for the
// backend JSON edge (rack-exchange tiles and the word-check query).
func bytesToInts(bs []byte) []int {
	out := make([]int, len(bs))
	for i, b := range bs {
		out[i] = int(b)
	}
	return out
}

func gamesListHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		res, err := backend.GamesList(ctx, req.UserID)
		if err != nil {
			return nil, err
		}
		return encodeGameList(res), nil
	}
}

func passHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsGameActionRequest(req.Payload, 0)
		res, err := backend.Pass(ctx, req.UserID, string(in.GameId()))
		if err != nil {
			return nil, err
		}
		return encodeMoveResult(res), nil
	}
}

func resignHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsGameActionRequest(req.Payload, 0)
		res, err := backend.Resign(ctx, req.UserID, string(in.GameId()))
		if err != nil {
			return nil, err
		}
		return encodeMoveResult(res), nil
	}
}

func exchangeHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsExchangeRequest(req.Payload, 0)
		res, err := backend.Exchange(ctx, req.UserID, string(in.GameId()), bytesToInts(in.TilesBytes()))
		if err != nil {
			return nil, err
		}
		return encodeMoveResult(res), nil
	}
}

func hintHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsGameActionRequest(req.Payload, 0)
		res, err := backend.Hint(ctx, req.UserID, string(in.GameId()))
		if err != nil {
			return nil, err
		}
		return encodeHintResult(res), nil
	}
}

func evaluateHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsEvalRequest(req.Payload, 0)
		res, err := backend.Evaluate(ctx, req.UserID, string(in.GameId()), decodeEvalTiles(in))
		if err != nil {
			return nil, err
		}
		return encodeEvalResult(res), nil
	}
}

func checkWordHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsCheckWordRequest(req.Payload, 0)
		res, err := backend.CheckWord(ctx, req.UserID, string(in.GameId()), bytesToInts(in.WordBytes()))
		if err != nil {
			return nil, err
		}
		return encodeWordCheck(res), nil
	}
}

func complaintHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsComplaintRequest(req.Payload, 0)
		if err := backend.Complaint(ctx, req.UserID, string(in.GameId()), string(in.Word()), string(in.Note())); err != nil {
			return nil, err
		}
		return encodeAck(true), nil
	}
}

func historyHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsGameActionRequest(req.Payload, 0)
		res, err := backend.History(ctx, req.UserID, string(in.GameId()))
		if err != nil {
			return nil, err
		}
		return encodeHistory(res), nil
	}
}

func chatListHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsGameActionRequest(req.Payload, 0)
		res, err := backend.ChatList(ctx, req.UserID, string(in.GameId()))
		if err != nil {
			return nil, err
		}
		return encodeChatList(res), nil
	}
}

func nudgeHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsGameActionRequest(req.Payload, 0)
		res, err := backend.Nudge(ctx, req.UserID, string(in.GameId()))
		if err != nil {
			return nil, err
		}
		return encodeChat(res), nil
	}
}

// markChatReadHandler acknowledges the caller has read the game's chat. It reuses
// GameActionRequest for the game id and echoes an Ack.
func markChatReadHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsGameActionRequest(req.Payload, 0)
		if err := backend.MarkChatRead(ctx, req.UserID, string(in.GameId())); err != nil {
			return nil, err
		}
		return encodeAck(true), nil
	}
}

// getDraftHandler returns the player's saved composition. It reuses
// GameActionRequest for the game id and wraps the backend's raw JSON in a DraftView.
func getDraftHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsGameActionRequest(req.Payload, 0)
		raw, err := backend.GetDraft(ctx, req.UserID, string(in.GameId()))
		if err != nil {
			return nil, err
		}
		return encodeDraftView(string(raw)), nil
	}
}

// saveDraftHandler upserts the player's composition, forwarding the opaque JSON
// string verbatim. It echoes an empty DraftView as a well-formed acknowledgement.
func saveDraftHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsDraftRequest(req.Payload, 0)
		if err := backend.SaveDraft(ctx, req.UserID, string(in.GameId()), json.RawMessage(in.Json())); err != nil {
			return nil, err
		}
		return encodeDraftView(""), nil
	}
}

// hideGameHandler hides a finished game from the caller's own list. It reuses
// GameActionRequest for the game id and echoes an Ack.
func hideGameHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsGameActionRequest(req.Payload, 0)
		if err := backend.HideGame(ctx, req.UserID, string(in.GameId())); err != nil {
			return nil, err
		}
		return encodeAck(true), nil
	}
}

func feedbackSubmitHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		in := fb.GetRootAsFeedbackSubmitRequest(req.Payload, 0)
		if err := backend.FeedbackSubmit(ctx, req.UserID, string(in.Body()), in.AttachmentBytes(), string(in.AttachmentName()), string(in.Channel()), string(in.Version()), string(in.BrowserTz()), req.ClientIP); err != nil {
			return nil, err
		}
		return encodeAck(true), nil
	}
}

func feedbackGetHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		st, err := backend.FeedbackGet(ctx, req.UserID)
		if err != nil {
			return nil, err
		}
		return encodeFeedbackState(st), nil
	}
}

func feedbackUnreadHandler(backend *backendclient.Client) Handler {
	return func(ctx context.Context, req Request) ([]byte, error) {
		u, err := backend.FeedbackUnread(ctx, req.UserID)
		if err != nil {
			return nil, err
		}
		return encodeFeedbackUnread(u), nil
	}
}
