// Package wire holds the FlatBuffers builders for the nested wire tables that both
// the backend's push-event encoder (backend/internal/notify) and the gateway's edge
// transcoder (gateway/internal/transcode) emit. The two callers resolve their own
// source types (the backend's domain payloads, the gateway's REST DTOs) into the
// neutral structs below and delegate the actual FlatBuffers construction here, so the
// shared tables (GameView, MoveRecord, StateView, AccountRef, Invitation) have a
// single encoding definition that cannot drift between the two paths.
//
// FlatBuffers is built bottom-up: every string and child vector is created before the
// table that references it, and no two tables/vectors are under construction at once.
// Each builder returns the offset of the table (or vector) it built; the caller embeds
// that offset in a parent table or finishes the buffer with it.
package wire

import (
	flatbuffers "github.com/google/flatbuffers/go"

	fb "scrabble/pkg/fbs/scrabblefb"
)

// SeatView is one seat's public standing in a GameView.
type SeatView struct {
	Seat        int
	AccountID   string
	Score       int
	HintsUsed   int
	IsWinner    bool
	DisplayName string
	// Deleted marks a seat whose account has been deleted (tombstoned): the client hides
	// every social control aimed at it — add-friend, block, chat and nudge.
	Deleted bool
}

// GameView is the shared, non-private game summary.
type GameView struct {
	ID                   string
	Variant              string
	DictVersion          string
	Status               string
	Players              int
	ToMove               int
	TurnTimeoutSecs      int
	MultipleWordsPerTurn bool
	MoveCount            int
	EndReason            string
	Seats                []SeatView
	LastActivityUnix     int64
	// VsAI marks an honest-AI game (the opponent is shown as 🤖, chat/nudge/add-friend off).
	VsAI bool
	// UnreadChat is a per-viewer flag: the requesting player has unread chat in this game.
	UnreadChat bool
	// UnreadMessages is a per-viewer flag: at least one unread entry is a real message (not just
	// a nudge), so the badge can be coloured apart from the nudge-only case.
	UnreadMessages bool
	// Kind is the game's origin for the active-game limits: 0 unknown (a pre-existing game), 1 vs_ai,
	// 2 random, 3 friends. The lobby counts active games per kind to lock a capped New-Game start.
	Kind int
	// NoRepeatWords forbids laying a word that is already on the board (the "Эрудит" rule). Pinned
	// per game, so a game started before the rule existed keeps playing without it.
	NoRepeatWords bool
}

// TileRecord is one tile in a decoded MoveRecord (the concrete letter, "?" for a blank
// read from a hand).
type TileRecord struct {
	Row    int
	Col    int
	Letter string
	Blank  bool
}

// MoveRecord is one decoded move (a committed play, a hint preview). Action and Dir are
// the already-stringified move kind and direction.
type MoveRecord struct {
	Player  int
	Action  string
	Dir     string
	MainRow int
	MainCol int
	Tiles   []TileRecord
	Words   []string
	Count   int
	Score   int
	Total   int
}

// AlphabetEntry is one letter of a variant's alphabet (Index is the wire alphabet-index
// byte; a value of 255 is the blank sentinel elsewhere).
type AlphabetEntry struct {
	Index  int
	Letter string
	Value  int
}

// StateView is a player's view of a game: the shared summary plus their private rack
// (wire alphabet indices), bag size and hint budget. Alphabet is set only when the
// recipient may not have cached the variant's display table yet.
type StateView struct {
	Game                  GameView
	Seat                  int
	Rack                  []int
	BagLen                int
	HintsRemaining        int
	HintUnlockLeftSeconds int
	Alphabet              []AlphabetEntry
}

// AccountRef is a referenced account with its display name resolved.
type AccountRef struct {
	AccountID   string
	DisplayName string
}

// InvitationInvitee is one invited player's seat and response inside an Invitation.
type InvitationInvitee struct {
	AccountID   string
	DisplayName string
	Seat        int
	Response    string
}

// Invitation is a friend-game invitation with its settings and invitees.
type Invitation struct {
	ID                   string
	Inviter              AccountRef
	Invitees             []InvitationInvitee
	Variant              string
	TurnTimeoutSecs      int
	HintsAllowed         bool
	HintsPerPlayer       int
	MultipleWordsPerTurn bool
	DropoutTiles         string
	Status               string
	GameID               string
	ExpiresAtUnix        int64
}

// BuildGameView builds a GameView table from g and returns its offset.
func BuildGameView(b *flatbuffers.Builder, g GameView) flatbuffers.UOffsetT {
	seatOffs := make([]flatbuffers.UOffsetT, len(g.Seats))
	for i, s := range g.Seats {
		aid := b.CreateString(s.AccountID)
		dname := b.CreateString(s.DisplayName)
		fb.SeatViewStart(b)
		fb.SeatViewAddSeat(b, int32(s.Seat))
		fb.SeatViewAddAccountId(b, aid)
		fb.SeatViewAddScore(b, int32(s.Score))
		fb.SeatViewAddHintsUsed(b, int32(s.HintsUsed))
		fb.SeatViewAddIsWinner(b, s.IsWinner)
		fb.SeatViewAddDisplayName(b, dname)
		fb.SeatViewAddDeleted(b, s.Deleted)
		seatOffs[i] = fb.SeatViewEnd(b)
	}
	fb.GameViewStartSeatsVector(b, len(seatOffs))
	for i := len(seatOffs) - 1; i >= 0; i-- {
		b.PrependUOffsetT(seatOffs[i])
	}
	seats := b.EndVector(len(seatOffs))

	id := b.CreateString(g.ID)
	variant := b.CreateString(g.Variant)
	dictVer := b.CreateString(g.DictVersion)
	status := b.CreateString(g.Status)
	endReason := b.CreateString(g.EndReason)

	fb.GameViewStart(b)
	fb.GameViewAddId(b, id)
	fb.GameViewAddVariant(b, variant)
	fb.GameViewAddDictVersion(b, dictVer)
	fb.GameViewAddStatus(b, status)
	fb.GameViewAddPlayers(b, int32(g.Players))
	fb.GameViewAddToMove(b, int32(g.ToMove))
	fb.GameViewAddTurnTimeoutSecs(b, int32(g.TurnTimeoutSecs))
	fb.GameViewAddMultipleWordsPerTurn(b, g.MultipleWordsPerTurn)
	fb.GameViewAddMoveCount(b, int32(g.MoveCount))
	fb.GameViewAddEndReason(b, endReason)
	fb.GameViewAddSeats(b, seats)
	fb.GameViewAddLastActivityUnix(b, g.LastActivityUnix)
	fb.GameViewAddVsAi(b, g.VsAI)
	fb.GameViewAddUnreadChat(b, g.UnreadChat)
	fb.GameViewAddUnreadMessages(b, g.UnreadMessages)
	fb.GameViewAddKind(b, int32(g.Kind))
	fb.GameViewAddNoRepeatWords(b, g.NoRepeatWords)
	return fb.GameViewEnd(b)
}

// BuildMoveRecord builds a MoveRecord table from m and returns its offset.
func BuildMoveRecord(b *flatbuffers.Builder, m MoveRecord) flatbuffers.UOffsetT {
	tileOffs := make([]flatbuffers.UOffsetT, len(m.Tiles))
	for i, t := range m.Tiles {
		letter := b.CreateString(t.Letter)
		fb.TileRecordStart(b)
		fb.TileRecordAddRow(b, int32(t.Row))
		fb.TileRecordAddCol(b, int32(t.Col))
		fb.TileRecordAddLetter(b, letter)
		fb.TileRecordAddBlank(b, t.Blank)
		tileOffs[i] = fb.TileRecordEnd(b)
	}
	fb.MoveRecordStartTilesVector(b, len(tileOffs))
	for i := len(tileOffs) - 1; i >= 0; i-- {
		b.PrependUOffsetT(tileOffs[i])
	}
	tiles := b.EndVector(len(tileOffs))

	wordOffs := make([]flatbuffers.UOffsetT, len(m.Words))
	for i, w := range m.Words {
		wordOffs[i] = b.CreateString(w)
	}
	fb.MoveRecordStartWordsVector(b, len(wordOffs))
	for i := len(wordOffs) - 1; i >= 0; i-- {
		b.PrependUOffsetT(wordOffs[i])
	}
	words := b.EndVector(len(wordOffs))

	action := b.CreateString(m.Action)
	dir := b.CreateString(m.Dir)
	fb.MoveRecordStart(b)
	fb.MoveRecordAddPlayer(b, int32(m.Player))
	fb.MoveRecordAddAction(b, action)
	fb.MoveRecordAddDir(b, dir)
	fb.MoveRecordAddMainRow(b, int32(m.MainRow))
	fb.MoveRecordAddMainCol(b, int32(m.MainCol))
	fb.MoveRecordAddTiles(b, tiles)
	fb.MoveRecordAddWords(b, words)
	fb.MoveRecordAddCount(b, int32(m.Count))
	fb.MoveRecordAddScore(b, int32(m.Score))
	fb.MoveRecordAddTotal(b, int32(m.Total))
	return fb.MoveRecordEnd(b)
}

// BuildStateViewAlphabet builds the AlphabetEntry vector embedded in a StateView and
// returns its offset.
func BuildStateViewAlphabet(b *flatbuffers.Builder, entries []AlphabetEntry) flatbuffers.UOffsetT {
	offs := make([]flatbuffers.UOffsetT, len(entries))
	for i, e := range entries {
		letter := b.CreateString(e.Letter)
		fb.AlphabetEntryStart(b)
		fb.AlphabetEntryAddIndex(b, byte(e.Index))
		fb.AlphabetEntryAddLetter(b, letter)
		fb.AlphabetEntryAddValue(b, int32(e.Value))
		offs[i] = fb.AlphabetEntryEnd(b)
	}
	fb.StateViewStartAlphabetVector(b, len(offs))
	for i := len(offs) - 1; i >= 0; i-- {
		b.PrependUOffsetT(offs[i])
	}
	return b.EndVector(len(offs))
}

// BuildStateView builds a StateView table from s and returns its offset. The alphabet
// table is embedded only when s.Alphabet is non-empty.
func BuildStateView(b *flatbuffers.Builder, s StateView) flatbuffers.UOffsetT {
	game := BuildGameView(b, s.Game)
	rackBytes := make([]byte, len(s.Rack))
	for i, v := range s.Rack {
		rackBytes[i] = byte(v)
	}
	rack := b.CreateByteVector(rackBytes)
	hasAlphabet := len(s.Alphabet) > 0
	var alphabet flatbuffers.UOffsetT
	if hasAlphabet {
		alphabet = BuildStateViewAlphabet(b, s.Alphabet)
	}
	fb.StateViewStart(b)
	fb.StateViewAddGame(b, game)
	fb.StateViewAddSeat(b, int32(s.Seat))
	fb.StateViewAddRack(b, rack)
	fb.StateViewAddBagLen(b, int32(s.BagLen))
	fb.StateViewAddHintsRemaining(b, int32(s.HintsRemaining))
	fb.StateViewAddHintUnlockLeftSeconds(b, int32(s.HintUnlockLeftSeconds))
	if hasAlphabet {
		fb.StateViewAddAlphabet(b, alphabet)
	}
	return fb.StateViewEnd(b)
}

// BuildAccountRef builds an AccountRef table from a and returns its offset.
func BuildAccountRef(b *flatbuffers.Builder, a AccountRef) flatbuffers.UOffsetT {
	aid := b.CreateString(a.AccountID)
	name := b.CreateString(a.DisplayName)
	fb.AccountRefStart(b)
	fb.AccountRefAddAccountId(b, aid)
	fb.AccountRefAddDisplayName(b, name)
	return fb.AccountRefEnd(b)
}

// BuildInvitation builds an Invitation table from inv and returns its offset.
func BuildInvitation(b *flatbuffers.Builder, inv Invitation) flatbuffers.UOffsetT {
	inviteeOffs := make([]flatbuffers.UOffsetT, len(inv.Invitees))
	for i, iv := range inv.Invitees {
		aid := b.CreateString(iv.AccountID)
		name := b.CreateString(iv.DisplayName)
		resp := b.CreateString(iv.Response)
		fb.InvitationInviteeStart(b)
		fb.InvitationInviteeAddAccountId(b, aid)
		fb.InvitationInviteeAddDisplayName(b, name)
		fb.InvitationInviteeAddSeat(b, int32(iv.Seat))
		fb.InvitationInviteeAddResponse(b, resp)
		inviteeOffs[i] = fb.InvitationInviteeEnd(b)
	}
	fb.InvitationStartInviteesVector(b, len(inviteeOffs))
	for i := len(inviteeOffs) - 1; i >= 0; i-- {
		b.PrependUOffsetT(inviteeOffs[i])
	}
	invitees := b.EndVector(len(inviteeOffs))

	inviter := BuildAccountRef(b, inv.Inviter)
	id := b.CreateString(inv.ID)
	variant := b.CreateString(inv.Variant)
	dropout := b.CreateString(inv.DropoutTiles)
	status := b.CreateString(inv.Status)
	gameID := b.CreateString(inv.GameID)
	fb.InvitationStart(b)
	fb.InvitationAddId(b, id)
	fb.InvitationAddInviter(b, inviter)
	fb.InvitationAddInvitees(b, invitees)
	fb.InvitationAddVariant(b, variant)
	fb.InvitationAddTurnTimeoutSecs(b, int32(inv.TurnTimeoutSecs))
	fb.InvitationAddHintsAllowed(b, inv.HintsAllowed)
	fb.InvitationAddHintsPerPlayer(b, int32(inv.HintsPerPlayer))
	fb.InvitationAddMultipleWordsPerTurn(b, inv.MultipleWordsPerTurn)
	fb.InvitationAddDropoutTiles(b, dropout)
	fb.InvitationAddStatus(b, status)
	fb.InvitationAddGameId(b, gameID)
	fb.InvitationAddExpiresAtUnix(b, inv.ExpiresAtUnix)
	return fb.InvitationEnd(b)
}
