package backendclient

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"galaxy/gateway/internal/downstream"
	diplomailmodel "galaxy/model/diplomail"
	commonfbs "galaxy/schema/fbs/common"
	fbs "galaxy/schema/fbs/diplomail"

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

// ExecuteMailCommand routes one authenticated `user.games.mail.*`
// command into the matching `/api/v1/user/games/{game_id}/mail/...`
// backend REST endpoint. Each command decodes a FlatBuffers request
// payload, issues the REST call, decodes the JSON response, and
// re-encodes the result as a typed FlatBuffers envelope.
func (c *RESTClient) ExecuteMailCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) {
	if c == nil || c.httpClient == nil {
		return downstream.UnaryResult{}, errors.New("backendclient: execute mail command: nil client")
	}
	if ctx == nil {
		return downstream.UnaryResult{}, errors.New("backendclient: execute mail command: nil context")
	}
	if err := ctx.Err(); err != nil {
		return downstream.UnaryResult{}, err
	}
	if strings.TrimSpace(command.UserID) == "" {
		return downstream.UnaryResult{}, errors.New("backendclient: execute mail command: user_id must not be empty")
	}

	switch command.MessageType {
	case diplomailmodel.MessageTypeUserGamesMailInbox:
		return c.executeMailInbox(ctx, command.UserID, command.PayloadBytes)
	case diplomailmodel.MessageTypeUserGamesMailSent:
		return c.executeMailSent(ctx, command.UserID, command.PayloadBytes)
	case diplomailmodel.MessageTypeUserGamesMailMessageGet:
		return c.executeMailMessageGet(ctx, command.UserID, command.PayloadBytes)
	case diplomailmodel.MessageTypeUserGamesMailSend:
		return c.executeMailSend(ctx, command.UserID, command.PayloadBytes)
	case diplomailmodel.MessageTypeUserGamesMailBroadcast:
		return c.executeMailBroadcast(ctx, command.UserID, command.PayloadBytes)
	case diplomailmodel.MessageTypeUserGamesMailAdmin:
		return c.executeMailAdmin(ctx, command.UserID, command.PayloadBytes)
	case diplomailmodel.MessageTypeUserGamesMailRead:
		return c.executeMailRead(ctx, command.UserID, command.PayloadBytes)
	case diplomailmodel.MessageTypeUserGamesMailDelete:
		return c.executeMailDelete(ctx, command.UserID, command.PayloadBytes)
	default:
		return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute mail command: unsupported message type %q", command.MessageType)
	}
}

// mailMessageJSON mirrors the backend's `UserMailMessageDetail` wire
// shape from `backend/openapi.yaml`. Pointer fields are nullable in
// the OpenAPI spec; the encoder treats empty strings as "absent".
type mailMessageJSON struct {
	MessageID         string  `json:"message_id"`
	GameID            string  `json:"game_id"`
	GameName          string  `json:"game_name,omitempty"`
	Kind              string  `json:"kind"`
	SenderKind        string  `json:"sender_kind"`
	SenderUserID      *string `json:"sender_user_id,omitempty"`
	SenderUsername    *string `json:"sender_username,omitempty"`
	SenderRaceName    *string `json:"sender_race_name,omitempty"`
	Subject           string  `json:"subject,omitempty"`
	Body              string  `json:"body"`
	BodyLang          string  `json:"body_lang"`
	BroadcastScope    string  `json:"broadcast_scope"`
	CreatedAt         string  `json:"created_at"`
	RecipientUserID   string  `json:"recipient_user_id"`
	RecipientUserName string  `json:"recipient_user_name,omitempty"`
	RecipientRaceName *string `json:"recipient_race_name,omitempty"`
	ReadAt            *string `json:"read_at,omitempty"`
	DeletedAt         *string `json:"deleted_at,omitempty"`
	TranslatedSubject *string `json:"translated_subject,omitempty"`
	TranslatedBody    *string `json:"translated_body,omitempty"`
	TranslationLang   *string `json:"translation_lang,omitempty"`
	Translator        *string `json:"translator,omitempty"`
}

// mailRecipientStateJSON mirrors `UserMailRecipientState`.
type mailRecipientStateJSON struct {
	MessageID string  `json:"message_id"`
	ReadAt    *string `json:"read_at,omitempty"`
	DeletedAt *string `json:"deleted_at,omitempty"`
}

// mailBroadcastReceiptJSON mirrors `UserMailBroadcastReceipt`.
type mailBroadcastReceiptJSON struct {
	MessageID      string `json:"message_id"`
	GameID         string `json:"game_id"`
	GameName       string `json:"game_name,omitempty"`
	Kind           string `json:"kind"`
	SenderKind     string `json:"sender_kind"`
	Subject        string `json:"subject,omitempty"`
	Body           string `json:"body"`
	BodyLang       string `json:"body_lang"`
	BroadcastScope string `json:"broadcast_scope"`
	CreatedAt      string `json:"created_at"`
	RecipientCount int    `json:"recipient_count"`
}

type mailInboxJSON struct {
	Items []mailMessageJSON `json:"items"`
}

func (c *RESTClient) executeMailInbox(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
	if len(payload) == 0 {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.inbox: payload is empty")
	}
	flat := fbs.GetRootAsInboxRequest(payload, 0)
	gameID := readUUID(flat.GameId(nil))
	if gameID == uuid.Nil {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.inbox: game_id is missing")
	}
	target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/inbox"
	respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.inbox: %w", err)
	}
	if status < 200 || status >= 300 {
		return projectMailErrorResponse(status, respBody)
	}
	var resp mailInboxJSON
	if err := json.Unmarshal(respBody, &resp); err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("decode mail inbox response: %w", err)
	}
	out := encodeMailMessageList(resp.Items, fbs.InboxResponseStart, fbs.InboxResponseAddItems, fbs.InboxResponseEnd, fbs.FinishInboxResponseBuffer)
	return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: out}, nil
}

func (c *RESTClient) executeMailSent(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
	if len(payload) == 0 {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.sent: payload is empty")
	}
	flat := fbs.GetRootAsSentRequest(payload, 0)
	gameID := readUUID(flat.GameId(nil))
	if gameID == uuid.Nil {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.sent: game_id is missing")
	}
	target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/sent"
	respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.sent: %w", err)
	}
	if status < 200 || status >= 300 {
		return projectMailErrorResponse(status, respBody)
	}
	var resp mailInboxJSON
	if err := json.Unmarshal(respBody, &resp); err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("decode mail sent response: %w", err)
	}
	out := encodeMailMessageList(resp.Items, fbs.SentResponseStart, fbs.SentResponseAddItems, fbs.SentResponseEnd, fbs.FinishSentResponseBuffer)
	return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: out}, nil
}

func (c *RESTClient) executeMailMessageGet(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
	if len(payload) == 0 {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.message.get: payload is empty")
	}
	flat := fbs.GetRootAsMessageGetRequest(payload, 0)
	gameID := readUUID(flat.GameId(nil))
	messageID := readUUID(flat.MessageId(nil))
	if gameID == uuid.Nil || messageID == uuid.Nil {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.message.get: game_id and message_id are required")
	}
	target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages/" + url.PathEscape(messageID.String())
	respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.message.get: %w", err)
	}
	if status < 200 || status >= 300 {
		return projectMailErrorResponse(status, respBody)
	}
	var msg mailMessageJSON
	if err := json.Unmarshal(respBody, &msg); err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("decode mail message response: %w", err)
	}
	builder := flatbuffers.NewBuilder(512)
	msgOff := encodeMailMessage(builder, &msg)
	fbs.MessageGetResponseStart(builder)
	fbs.MessageGetResponseAddMessage(builder, msgOff)
	root := fbs.MessageGetResponseEnd(builder)
	fbs.FinishMessageGetResponseBuffer(builder, root)
	return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
}

func (c *RESTClient) executeMailSend(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
	if len(payload) == 0 {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.send: payload is empty")
	}
	flat := fbs.GetRootAsSendRequest(payload, 0)
	gameID := readUUID(flat.GameId(nil))
	if gameID == uuid.Nil {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.send: game_id is missing")
	}
	body := struct {
		RecipientUserID   string `json:"recipient_user_id,omitempty"`
		RecipientRaceName string `json:"recipient_race_name,omitempty"`
		Subject           string `json:"subject,omitempty"`
		Body              string `json:"body"`
	}{
		RecipientUserID:   string(flat.RecipientUserId()),
		RecipientRaceName: string(flat.RecipientRaceName()),
		Subject:           string(flat.Subject()),
		Body:              string(flat.Body()),
	}
	target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages"
	respBody, status, err := c.do(ctx, http.MethodPost, target, userID, body)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.send: %w", err)
	}
	if status < 200 || status >= 300 {
		return projectMailErrorResponse(status, respBody)
	}
	var msg mailMessageJSON
	if err := json.Unmarshal(respBody, &msg); err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("decode mail send response: %w", err)
	}
	builder := flatbuffers.NewBuilder(512)
	msgOff := encodeMailMessage(builder, &msg)
	fbs.SendResponseStart(builder)
	fbs.SendResponseAddMessage(builder, msgOff)
	root := fbs.SendResponseEnd(builder)
	fbs.FinishSendResponseBuffer(builder, root)
	return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
}

func (c *RESTClient) executeMailBroadcast(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
	if len(payload) == 0 {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.broadcast: payload is empty")
	}
	flat := fbs.GetRootAsBroadcastRequest(payload, 0)
	gameID := readUUID(flat.GameId(nil))
	if gameID == uuid.Nil {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.broadcast: game_id is missing")
	}
	body := struct {
		Subject string `json:"subject,omitempty"`
		Body    string `json:"body"`
	}{
		Subject: string(flat.Subject()),
		Body:    string(flat.Body()),
	}
	target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/broadcast"
	respBody, status, err := c.do(ctx, http.MethodPost, target, userID, body)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.broadcast: %w", err)
	}
	if status < 200 || status >= 300 {
		return projectMailErrorResponse(status, respBody)
	}
	var receipt mailBroadcastReceiptJSON
	if err := json.Unmarshal(respBody, &receipt); err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("decode mail broadcast response: %w", err)
	}
	builder := flatbuffers.NewBuilder(256)
	recOff := encodeMailBroadcastReceipt(builder, &receipt)
	fbs.BroadcastResponseStart(builder)
	fbs.BroadcastResponseAddReceipt(builder, recOff)
	root := fbs.BroadcastResponseEnd(builder)
	fbs.FinishBroadcastResponseBuffer(builder, root)
	return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
}

func (c *RESTClient) executeMailAdmin(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
	if len(payload) == 0 {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.admin: payload is empty")
	}
	flat := fbs.GetRootAsAdminRequest(payload, 0)
	gameID := readUUID(flat.GameId(nil))
	if gameID == uuid.Nil {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.admin: game_id is missing")
	}
	target := string(flat.Target())
	body := struct {
		Target            string `json:"target"`
		RecipientUserID   string `json:"recipient_user_id,omitempty"`
		RecipientRaceName string `json:"recipient_race_name,omitempty"`
		Recipients        string `json:"recipients,omitempty"`
		Subject           string `json:"subject,omitempty"`
		Body              string `json:"body"`
	}{
		Target:            target,
		RecipientUserID:   string(flat.RecipientUserId()),
		RecipientRaceName: string(flat.RecipientRaceName()),
		Recipients:        string(flat.Recipients()),
		Subject:           string(flat.Subject()),
		Body:              string(flat.Body()),
	}
	url := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/admin"
	respBody, status, err := c.do(ctx, http.MethodPost, url, userID, body)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.admin: %w", err)
	}
	if status < 200 || status >= 300 {
		return projectMailErrorResponse(status, respBody)
	}
	builder := flatbuffers.NewBuilder(512)
	if target == "all" {
		var receipt mailBroadcastReceiptJSON
		if err := json.Unmarshal(respBody, &receipt); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("decode mail admin broadcast response: %w", err)
		}
		recOff := encodeMailBroadcastReceipt(builder, &receipt)
		fbs.AdminResponseStart(builder)
		fbs.AdminResponseAddReceipt(builder, recOff)
		root := fbs.AdminResponseEnd(builder)
		fbs.FinishAdminResponseBuffer(builder, root)
	} else {
		var msg mailMessageJSON
		if err := json.Unmarshal(respBody, &msg); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("decode mail admin send response: %w", err)
		}
		msgOff := encodeMailMessage(builder, &msg)
		fbs.AdminResponseStart(builder)
		fbs.AdminResponseAddMessage(builder, msgOff)
		root := fbs.AdminResponseEnd(builder)
		fbs.FinishAdminResponseBuffer(builder, root)
	}
	return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
}

func (c *RESTClient) executeMailRead(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
	if len(payload) == 0 {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.read: payload is empty")
	}
	flat := fbs.GetRootAsReadRequest(payload, 0)
	gameID := readUUID(flat.GameId(nil))
	messageID := readUUID(flat.MessageId(nil))
	if gameID == uuid.Nil || messageID == uuid.Nil {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.read: game_id and message_id are required")
	}
	target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages/" + url.PathEscape(messageID.String()) + "/read"
	respBody, status, err := c.do(ctx, http.MethodPost, target, userID, struct{}{})
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.read: %w", err)
	}
	if status < 200 || status >= 300 {
		return projectMailErrorResponse(status, respBody)
	}
	return encodeRecipientStateResponse(respBody, fbs.ReadResponseStart, fbs.ReadResponseAddState, fbs.ReadResponseEnd, fbs.FinishReadResponseBuffer)
}

func (c *RESTClient) executeMailDelete(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
	if len(payload) == 0 {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.delete: payload is empty")
	}
	flat := fbs.GetRootAsDeleteRequest(payload, 0)
	gameID := readUUID(flat.GameId(nil))
	messageID := readUUID(flat.MessageId(nil))
	if gameID == uuid.Nil || messageID == uuid.Nil {
		return downstream.UnaryResult{}, errors.New("execute user.games.mail.delete: game_id and message_id are required")
	}
	target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages/" + url.PathEscape(messageID.String())
	respBody, status, err := c.do(ctx, http.MethodDelete, target, userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.delete: %w", err)
	}
	if status < 200 || status >= 300 {
		return projectMailErrorResponse(status, respBody)
	}
	return encodeRecipientStateResponse(respBody, fbs.DeleteResponseStart, fbs.DeleteResponseAddState, fbs.DeleteResponseEnd, fbs.FinishDeleteResponseBuffer)
}

// encodeRecipientStateResponse decodes the JSON recipient-state body
// and emits the corresponding FlatBuffers Read/Delete envelope. The
// caller supplies the trio of envelope start / add-state / end / finish
// functions so this helper covers both endpoints with the same shape.
func encodeRecipientStateResponse(respBody []byte,
	startFn func(*flatbuffers.Builder),
	addStateFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
	endFn func(*flatbuffers.Builder) flatbuffers.UOffsetT,
	finishFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
) (downstream.UnaryResult, error) {
	var state mailRecipientStateJSON
	if err := json.Unmarshal(respBody, &state); err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("decode mail recipient state: %w", err)
	}
	builder := flatbuffers.NewBuilder(128)
	stateOff := encodeMailRecipientState(builder, &state)
	startFn(builder)
	addStateFn(builder, stateOff)
	root := endFn(builder)
	finishFn(builder, root)
	return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
}

// encodeMailMessageList is a shared helper that encodes a slice of
// mailMessageJSON items into either an InboxResponse or a
// SentResponse FlatBuffers envelope. The two envelopes have the same
// shape (just a `items` vector of MailMessage) so the trio of
// constructor functions parameterises the helper.
func encodeMailMessageList(items []mailMessageJSON,
	startFn func(*flatbuffers.Builder),
	addItemsFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
	endFn func(*flatbuffers.Builder) flatbuffers.UOffsetT,
	finishFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
) []byte {
	builder := flatbuffers.NewBuilder(1024)
	offsets := make([]flatbuffers.UOffsetT, 0, len(items))
	for i := range items {
		offsets = append(offsets, encodeMailMessage(builder, &items[i]))
	}
	// FlatBuffers vectors are built in reverse: prepend each offset.
	builder.StartVector(4, len(offsets), 4)
	for i := len(offsets) - 1; i >= 0; i-- {
		builder.PrependUOffsetT(offsets[i])
	}
	itemsVec := builder.EndVector(len(offsets))
	startFn(builder)
	addItemsFn(builder, itemsVec)
	root := endFn(builder)
	finishFn(builder, root)
	return builder.FinishedBytes()
}

// encodeMailMessage builds a MailMessage table inside builder. Returns
// the offset of the finished table. Strings are interned through the
// builder; missing JSON fields (nil pointers, empty strings) yield
// empty FB strings which the readers treat as absent.
func encodeMailMessage(builder *flatbuffers.Builder, m *mailMessageJSON) flatbuffers.UOffsetT {
	messageIDOff := builder.CreateString(m.MessageID)
	gameIDOff := builder.CreateString(m.GameID)
	gameNameOff := builder.CreateString(m.GameName)
	kindOff := builder.CreateString(m.Kind)
	senderKindOff := builder.CreateString(m.SenderKind)
	senderUserIDOff := builder.CreateString(stringPtrValue(m.SenderUserID))
	senderUsernameOff := builder.CreateString(stringPtrValue(m.SenderUsername))
	senderRaceNameOff := builder.CreateString(stringPtrValue(m.SenderRaceName))
	subjectOff := builder.CreateString(m.Subject)
	bodyOff := builder.CreateString(m.Body)
	bodyLangOff := builder.CreateString(m.BodyLang)
	broadcastScopeOff := builder.CreateString(m.BroadcastScope)
	recipientUserIDOff := builder.CreateString(m.RecipientUserID)
	recipientUserNameOff := builder.CreateString(m.RecipientUserName)
	recipientRaceNameOff := builder.CreateString(stringPtrValue(m.RecipientRaceName))
	translatedSubjectOff := builder.CreateString(stringPtrValue(m.TranslatedSubject))
	translatedBodyOff := builder.CreateString(stringPtrValue(m.TranslatedBody))
	translationLangOff := builder.CreateString(stringPtrValue(m.TranslationLang))
	translatorOff := builder.CreateString(stringPtrValue(m.Translator))

	fbs.MailMessageStart(builder)
	fbs.MailMessageAddMessageId(builder, messageIDOff)
	fbs.MailMessageAddGameId(builder, gameIDOff)
	fbs.MailMessageAddGameName(builder, gameNameOff)
	fbs.MailMessageAddKind(builder, kindOff)
	fbs.MailMessageAddSenderKind(builder, senderKindOff)
	fbs.MailMessageAddSenderUserId(builder, senderUserIDOff)
	fbs.MailMessageAddSenderUsername(builder, senderUsernameOff)
	fbs.MailMessageAddSenderRaceName(builder, senderRaceNameOff)
	fbs.MailMessageAddSubject(builder, subjectOff)
	fbs.MailMessageAddBody(builder, bodyOff)
	fbs.MailMessageAddBodyLang(builder, bodyLangOff)
	fbs.MailMessageAddBroadcastScope(builder, broadcastScopeOff)
	fbs.MailMessageAddCreatedAtMs(builder, parseRFC3339Millis(m.CreatedAt))
	fbs.MailMessageAddRecipientUserId(builder, recipientUserIDOff)
	fbs.MailMessageAddRecipientUserName(builder, recipientUserNameOff)
	fbs.MailMessageAddRecipientRaceName(builder, recipientRaceNameOff)
	fbs.MailMessageAddReadAtMs(builder, parseRFC3339Millis(stringPtrValue(m.ReadAt)))
	fbs.MailMessageAddDeletedAtMs(builder, parseRFC3339Millis(stringPtrValue(m.DeletedAt)))
	fbs.MailMessageAddTranslatedSubject(builder, translatedSubjectOff)
	fbs.MailMessageAddTranslatedBody(builder, translatedBodyOff)
	fbs.MailMessageAddTranslationLang(builder, translationLangOff)
	fbs.MailMessageAddTranslator(builder, translatorOff)
	return fbs.MailMessageEnd(builder)
}

// encodeMailRecipientState builds a MailRecipientState table.
func encodeMailRecipientState(builder *flatbuffers.Builder, s *mailRecipientStateJSON) flatbuffers.UOffsetT {
	messageIDOff := builder.CreateString(s.MessageID)
	fbs.MailRecipientStateStart(builder)
	fbs.MailRecipientStateAddMessageId(builder, messageIDOff)
	fbs.MailRecipientStateAddReadAtMs(builder, parseRFC3339Millis(stringPtrValue(s.ReadAt)))
	fbs.MailRecipientStateAddDeletedAtMs(builder, parseRFC3339Millis(stringPtrValue(s.DeletedAt)))
	return fbs.MailRecipientStateEnd(builder)
}

// encodeMailBroadcastReceipt builds a MailBroadcastReceipt table.
func encodeMailBroadcastReceipt(builder *flatbuffers.Builder, r *mailBroadcastReceiptJSON) flatbuffers.UOffsetT {
	messageIDOff := builder.CreateString(r.MessageID)
	gameIDOff := builder.CreateString(r.GameID)
	gameNameOff := builder.CreateString(r.GameName)
	kindOff := builder.CreateString(r.Kind)
	senderKindOff := builder.CreateString(r.SenderKind)
	subjectOff := builder.CreateString(r.Subject)
	bodyOff := builder.CreateString(r.Body)
	bodyLangOff := builder.CreateString(r.BodyLang)
	broadcastScopeOff := builder.CreateString(r.BroadcastScope)
	fbs.MailBroadcastReceiptStart(builder)
	fbs.MailBroadcastReceiptAddMessageId(builder, messageIDOff)
	fbs.MailBroadcastReceiptAddGameId(builder, gameIDOff)
	fbs.MailBroadcastReceiptAddGameName(builder, gameNameOff)
	fbs.MailBroadcastReceiptAddKind(builder, kindOff)
	fbs.MailBroadcastReceiptAddSenderKind(builder, senderKindOff)
	fbs.MailBroadcastReceiptAddSubject(builder, subjectOff)
	fbs.MailBroadcastReceiptAddBody(builder, bodyOff)
	fbs.MailBroadcastReceiptAddBodyLang(builder, bodyLangOff)
	fbs.MailBroadcastReceiptAddBroadcastScope(builder, broadcastScopeOff)
	fbs.MailBroadcastReceiptAddCreatedAtMs(builder, parseRFC3339Millis(r.CreatedAt))
	fbs.MailBroadcastReceiptAddRecipientCount(builder, int32(r.RecipientCount))
	return fbs.MailBroadcastReceiptEnd(builder)
}

// projectMailErrorResponse maps a non-2xx response into a UnaryResult
// carrying the backend error envelope, reusing the shared user-mail
// error-projection. 503 is bubbled as ErrDownstreamUnavailable.
func projectMailErrorResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
	if statusCode == http.StatusServiceUnavailable {
		return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
	}
	if statusCode >= 400 && statusCode <= 599 {
		return projectUserBackendError(statusCode, payload)
	}
	return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
}

// readUUID converts the common.UUID struct (or its absence) into a
// google/uuid.UUID. Returns uuid.Nil when the input is nil.
func readUUID(u *commonfbs.UUID) uuid.UUID {
	if u == nil {
		return uuid.Nil
	}
	var out uuid.UUID
	hi := u.Hi()
	lo := u.Lo()
	for i := 0; i < 8; i++ {
		out[i] = byte(hi >> (56 - 8*i))
		out[i+8] = byte(lo >> (56 - 8*i))
	}
	return out
}

// stringPtrValue returns "" for nil and the dereferenced value
// otherwise. Used to flatten nullable JSON strings into the
// always-present FlatBuffers string slot.
func stringPtrValue(p *string) string {
	if p == nil {
		return ""
	}
	return *p
}

// parseRFC3339Millis parses an RFC 3339 timestamp string (the format
// the backend mail handler emits) into Unix milliseconds. Returns 0
// when the input is empty or unparseable, matching the "absent"
// convention for the *_at_ms wire fields.
func parseRFC3339Millis(s string) int64 {
	if s == "" {
		return 0
	}
	t, err := time.Parse(time.RFC3339Nano, s)
	if err != nil {
		return 0
	}
	return t.UnixMilli()
}
