package server

import (
	"github.com/google/uuid"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/engine"
	"scrabble/backend/internal/game"
	"scrabble/backend/internal/lobby"
	"scrabble/backend/internal/social"
)

// The JSON DTOs below are the gateway<->backend REST contract. They are explicit
// (the domain/engine structs are never serialised directly) and mirror the
// FlatBuffers edge tables (pkg/fbs) the gateway transcodes to and from.

// sessionResponse is the credential returned by every auth endpoint.
type sessionResponse struct {
	Token       string `json:"token"`
	UserID      string `json:"user_id"`
	IsGuest     bool   `json:"is_guest"`
	DisplayName string `json:"display_name"`
}

// okResponse is a simple success acknowledgement.
type okResponse struct {
	OK bool `json:"ok"`
}

// resolveResponse maps a session token to its account. IsGuest lets the gateway
// gate guest-forbidden operations without an extra round-trip. PlatformKind and
// PlatformSubtype carry the session's trusted execution platform so the gateway can
// inject X-Platform; both are empty for an untrusted (pre-capture) session.
type resolveResponse struct {
	UserID          string `json:"user_id"`
	IsGuest         bool   `json:"is_guest"`
	PlatformKind    string `json:"platform_kind"`
	PlatformSubtype string `json:"platform_subtype"`
}

// profileResponse is the authenticated account's own profile. AwayStart and AwayEnd
// are the daily away window's "HH:MM" local-time bounds (in TimeZone).
type profileResponse struct {
	UserID                 string `json:"user_id"`
	DisplayName            string `json:"display_name"`
	PreferredLanguage      string `json:"preferred_language"`
	TimeZone               string `json:"time_zone"`
	AwayStart              string `json:"away_start"`
	AwayEnd                string `json:"away_end"`
	HintBalance            int    `json:"hint_balance"`
	BlockChat              bool   `json:"block_chat"`
	BlockFriendRequests    bool   `json:"block_friend_requests"`
	IsGuest                bool   `json:"is_guest"`
	NotificationsInAppOnly bool   `json:"notifications_in_app_only"`
	// VariantPreferences is the set of game variants the player allows themselves to
	// be matched into (engine.Variant stable labels), Erudit-first. It gates the New
	// Game picker and the matchmaker; never empty.
	VariantPreferences []string `json:"variant_preferences"`
	// Banner is the advertising-banner block: present only for a viewer eligible to
	// see the banner (a free account with an empty hint wallet and without the
	// no_banner role), absent otherwise. See banner.go.
	Banner *bannerDTO `json:"banner,omitempty"`
	// Ads carries the post-move interstitial config for the client's client-mirrored gate: the
	// cooldowns (seconds) and whether ads are suppressed in this context (no-ads / no_banner role).
	// The client shows a VK interstitial after a confirmed move / hint when not suppressed and the
	// cooldown has elapsed. Always present (the client also gates VK-only + online itself).
	Ads *adsDTO `json:"ads,omitempty"`
	// Email is the account's confirmed email address ("" when none); TelegramLinked and
	// VkLinked report whether a platform identity is attached. They drive the profile's
	// link / unlink / change-email controls, and are filled outside the pure projection
	// (they read the account's identities). See Server.profileResponse.
	Email          string `json:"email"`
	TelegramLinked bool   `json:"telegram_linked"`
	VkLinked       bool   `json:"vk_linked"`
	// DictVersions lists the current dictionary version per game variant (engine.Variant
	// stable labels). An installed PWA preparing for offline play reads it to preload the
	// right dawg per enabled variant off this cold-start response, without an extra request.
	// Filled outside the pure projection (it reads the dictionary registry), so it is empty
	// for callers that build the DTO without a Server. See Server.profileResponse.
	DictVersions []dictVersion `json:"dict_versions,omitempty"`
	// GameLimits carries the caller's tier active-game caps per kind (-1 = unlimited); the client
	// counts its active games per kind from the lobby and locks a capped New-Game start. Filled
	// outside the pure projection (it reads the limits config). See Server.profileResponse.
	GameLimits *gameLimitsDTO `json:"game_limits,omitempty"`
}

// gameLimitsDTO is the caller's tier active-game caps per kind (-1 = unlimited), for the client's
// per-kind New-Game lock. profileResponse.GameLimits carries it.
type gameLimitsDTO struct {
	VsAI    int `json:"vs_ai"`
	Random  int `json:"random"`
	Friends int `json:"friends"`
}

// dictVersion pairs a game variant's stable label (engine.Variant.String) with its current
// dictionary version. profileResponse.DictVersions carries the set so an offline-capable
// client preloads the matching dawg per variant.
type dictVersion struct {
	Variant string `json:"variant"`
	Version string `json:"version"`
}

// tileDTO is one placed (or to-place) tile.
type tileDTO struct {
	Row    int    `json:"row"`
	Col    int    `json:"col"`
	Letter string `json:"letter"`
	Blank  bool   `json:"blank"`
}

// moveRecordDTO is a decoded move (a committed play, or a hint preview).
type moveRecordDTO struct {
	Player  int       `json:"player"`
	Action  string    `json:"action"`
	Dir     string    `json:"dir"`
	MainRow int       `json:"main_row"`
	MainCol int       `json:"main_col"`
	Tiles   []tileDTO `json:"tiles"`
	Words   []string  `json:"words"`
	Count   int       `json:"count"`
	Score   int       `json:"score"`
	Total   int       `json:"total"`
}

// seatDTO is one seat's public standing. DisplayName is the seat's display-name snapshot
// (the per-game name, frozen when the seat was taken); fillSeatNames fills it from the
// account store only when a seat has no snapshot yet (a pre-snapshot legacy row).
type seatDTO struct {
	Seat        int    `json:"seat"`
	AccountID   string `json:"account_id"`
	DisplayName string `json:"display_name"`
	Score       int    `json:"score"`
	HintsUsed   int    `json:"hints_used"`
	IsWinner    bool   `json:"is_winner"`
	// Deleted marks a seat whose account has been deleted: the client hides every social
	// control aimed at it (add-friend, block, chat, nudge). gameDTOFromGame leaves it false
	// (it does not reach the account store); fillSeatDeleted fills it.
	Deleted bool `json:"deleted"`
}

// gameDTO is the shared game summary.
type gameDTO struct {
	ID                   string `json:"id"`
	Variant              string `json:"variant"`
	DictVersion          string `json:"dict_version"`
	Status               string `json:"status"`
	Players              int    `json:"players"`
	ToMove               int    `json:"to_move"`
	TurnTimeoutSecs      int    `json:"turn_timeout_secs"`
	MultipleWordsPerTurn bool   `json:"multiple_words_per_turn"`
	// NoRepeatWords forbids laying a word that is already on the board (the "Эрудит" rule, pinned
	// per game). The client needs it to score its local move preview the way the server does.
	NoRepeatWords bool `json:"no_repeat_words"`
	// VsAI marks an honest-AI game: the opponent is shown as 🤖 and chat/nudge/add-friend
	// are suppressed in the client.
	VsAI bool `json:"vs_ai"`
	// Kind is the game's origin for the active-game limits (game.Kind): 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    `json:"kind"`
	MoveCount int    `json:"move_count"`
	EndReason string `json:"end_reason"`
	// LastActivityUnix is the lobby sort key: the current turn's start for an active
	// game, the finish time once finished.
	LastActivityUnix int64     `json:"last_activity_unix"`
	Seats            []seatDTO `json:"seats"`
	// UnreadChat is a per-viewer flag: the requesting player has at least one unread chat
	// entry (message or nudge) in this game. It seeds the lobby and in-game unread badge.
	// gameDTOFromGame leaves it false (it has no viewer); the handlers fill it.
	UnreadChat bool `json:"unread_chat"`
	// UnreadMessages is a per-viewer flag: at least one of the unread entries is a real chat
	// message (not just a nudge). It colours the badge — set with UnreadChat it is the regular
	// badge, UnreadChat alone is the softer nudge-only badge. gameDTOFromGame leaves it false.
	UnreadMessages bool `json:"unread_messages"`
}

// moveResultDTO is the outcome of a committed move. Rack carries the actor's refilled rack as
// wire alphabet indices and BagLen the bag size after the draw, so the mover renders the
// next state from the response without a follow-up state fetch.
type moveResultDTO struct {
	Move   moveRecordDTO `json:"move"`
	Game   gameDTO       `json:"game"`
	Rack   []int         `json:"rack"`
	BagLen int           `json:"bag_len"`
}

// alphabetEntryDTO is one letter of a variant's alphabet (its index, concrete letter and
// tile value), embedded in the state view for display only when the client requests it.
type alphabetEntryDTO struct {
	Index  int    `json:"index"`
	Letter string `json:"letter"`
	Value  int    `json:"value"`
}

// stateDTO is a player's view of a game. Rack carries wire alphabet indices (a
// blank is engine.BlankIndex). Alphabet is present only when the request asked for it.
type stateDTO struct {
	Game           gameDTO `json:"game"`
	Seat           int     `json:"seat"`
	Rack           []int   `json:"rack"`
	BagLen         int     `json:"bag_len"`
	HintsRemaining int     `json:"hints_remaining"`
	// HintUnlockLeftSeconds is the vs_ai idle-hint gate: seconds until the hint unlocks (0 for a human
	// game / first move / not your turn). The client anchors a monotonic countdown to it.
	HintUnlockLeftSeconds int                `json:"hint_unlock_left_seconds"`
	Alphabet              []alphabetEntryDTO `json:"alphabet,omitempty"`
}

// matchDTO reports whether the caller has been paired into a game.
type matchDTO struct {
	Matched bool     `json:"matched"`
	Game    *gameDTO `json:"game,omitempty"`
}

// chatDTO is one stored chat message or nudge.
type chatDTO struct {
	ID            string `json:"id"`
	GameID        string `json:"game_id"`
	SenderID      string `json:"sender_id"`
	Kind          string `json:"kind"`
	Body          string `json:"body"`
	CreatedAtUnix int64  `json:"created_at_unix"`
}

// errorResponse is the uniform error envelope.
type errorResponse struct {
	Error errorBody `json:"error"`
}

type errorBody struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

// sessionResponseFor builds the credential payload for a minted session.
func sessionResponseFor(token string, acc account.Account) sessionResponse {
	return sessionResponse{
		Token:       token,
		UserID:      acc.ID.String(),
		IsGuest:     acc.IsGuest,
		DisplayName: acc.DisplayName,
	}
}

// profileResponseFor projects an account into its profile DTO.
func profileResponseFor(acc account.Account) profileResponse {
	return profileResponse{
		UserID:            acc.ID.String(),
		DisplayName:       acc.DisplayName,
		PreferredLanguage: acc.PreferredLanguage,
		TimeZone:          acc.TimeZone,
		AwayStart:         acc.AwayStart.Format(awayTimeLayout),
		AwayEnd:           acc.AwayEnd.Format(awayTimeLayout),
		// The hint balance comes from the payments benefit; profileResponse overrides this
		// with the context-aware count. This zero is the fallback when that read fails.
		HintBalance:            0,
		BlockChat:              acc.BlockChat,
		BlockFriendRequests:    acc.BlockFriendRequests,
		IsGuest:                acc.IsGuest,
		NotificationsInAppOnly: acc.NotificationsInAppOnly,
		VariantPreferences:     acc.VariantPreferences,
	}
}

// awayTimeLayout is the "HH:MM" wire form of the daily away-window bounds.
const awayTimeLayout = "15:04"

// gameDTOFromGame projects a game.Game into its DTO.
func gameDTOFromGame(g game.Game) gameDTO {
	seats := make([]seatDTO, 0, len(g.Seats))
	for _, s := range g.Seats {
		// An open game's still-empty opponent seat has no account: emit an empty id (and an
		// empty display name) so the client shows "searching for opponent" rather than the
		// nil-UUID. DisplayName carries the seat's per-game snapshot; fillSeatNames fills
		// only the seats still without one from the account store.
		accountID := ""
		if s.AccountID != uuid.Nil {
			accountID = s.AccountID.String()
		}
		seats = append(seats, seatDTO{
			Seat:        s.Seat,
			AccountID:   accountID,
			DisplayName: s.DisplayName,
			Score:       s.Score,
			HintsUsed:   s.HintsUsed,
			IsWinner:    s.IsWinner,
		})
	}
	last := g.TurnStartedAt
	if g.FinishedAt != nil {
		last = *g.FinishedAt
	}
	return gameDTO{
		ID:                   g.ID.String(),
		Variant:              g.Variant.String(),
		DictVersion:          g.DictVersion,
		Status:               g.Status,
		Players:              g.Players,
		ToMove:               g.ToMove,
		TurnTimeoutSecs:      int(g.TurnTimeout.Seconds()),
		MultipleWordsPerTurn: g.MultipleWordsPerTurn,
		NoRepeatWords:        g.NoRepeatWords,
		VsAI:                 g.VsAI,
		Kind:                 int(g.Kind),
		MoveCount:            g.MoveCount,
		EndReason:            g.EndReason,
		LastActivityUnix:     last.Unix(),
		Seats:                seats,
	}
}

// moveRecordDTOFrom projects an engine move record into its DTO.
func moveRecordDTOFrom(m engine.MoveRecord) moveRecordDTO {
	tiles := make([]tileDTO, 0, len(m.Tiles))
	for _, t := range m.Tiles {
		tiles = append(tiles, tileDTO{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank})
	}
	return moveRecordDTO{
		Player:  m.Player,
		Action:  m.Action.String(),
		Dir:     m.Dir.String(),
		MainRow: m.MainRow,
		MainCol: m.MainCol,
		Tiles:   tiles,
		Words:   m.Words,
		Count:   m.Count,
		Score:   m.Score,
		Total:   m.Total,
	}
}

// moveResultDTOFrom projects a committed move result into its DTO, encoding the actor's rack as
// wire alphabet indices.
func moveResultDTOFrom(r game.MoveResult) (moveResultDTO, error) {
	rack, err := engine.EncodeRack(r.Game.Variant, r.Rack)
	if err != nil {
		return moveResultDTO{}, err
	}
	return moveResultDTO{
		Move:   moveRecordDTOFrom(r.Move),
		Game:   gameDTOFromGame(r.Game),
		Rack:   rack,
		BagLen: r.BagLen,
	}, nil
}

// stateDTOFrom projects a player's state view into its DTO, encoding the rack as wire
// alphabet indices. When includeAlphabet is set it also embeds the variant's
// display table, which the client caches per variant and renders the rack with.
func stateDTOFrom(v game.StateView, includeAlphabet bool) (stateDTO, error) {
	rack, err := engine.EncodeRack(v.Game.Variant, v.Rack)
	if err != nil {
		return stateDTO{}, err
	}
	dto := stateDTO{
		Game:                  gameDTOFromGame(v.Game),
		Seat:                  v.Seat,
		Rack:                  rack,
		BagLen:                v.BagLen,
		HintsRemaining:        v.HintsRemaining,
		HintUnlockLeftSeconds: v.HintUnlockLeftSeconds,
	}
	if includeAlphabet {
		tab, err := engine.AlphabetTable(v.Game.Variant)
		if err != nil {
			return stateDTO{}, err
		}
		dto.Alphabet = make([]alphabetEntryDTO, len(tab))
		for i, e := range tab {
			dto.Alphabet[i] = alphabetEntryDTO{Index: int(e.Index), Letter: e.Letter, Value: e.Value}
		}
	}
	return dto, nil
}

// matchDTOFrom projects an enqueue result into its DTO. Enqueue always lands the
// caller in a game (freshly opened or joined), so the game is always present; Matched
// reports whether it already had an opponent.
func matchDTOFrom(r lobby.EnqueueResult) matchDTO {
	g := gameDTOFromGame(r.Game)
	return matchDTO{Matched: r.Matched, Game: &g}
}

// chatDTOFrom projects a chat message into its DTO.
func chatDTOFrom(m social.Message) chatDTO {
	return chatDTO{
		ID:            m.ID.String(),
		GameID:        m.GameID.String(),
		SenderID:      m.SenderID.String(),
		Kind:          m.Kind,
		Body:          m.Body,
		CreatedAtUnix: m.CreatedAt.Unix(),
	}
}
