package backendclient

import (
	"context"
	"encoding/json"
	"net/http"
	"net/url"
	"strconv"
)

// The structs below mirror the backend's JSON DTOs (backend/internal/server
// /dto.go). The transcode layer maps them to and from the FlatBuffers edge
// payloads.

// SessionResp is the credential minted by an auth operation.
type SessionResp struct {
	Token       string `json:"token"`
	UserID      string `json:"user_id"`
	IsGuest     bool   `json:"is_guest"`
	DisplayName string `json:"display_name"`
}

// ProfileResp is an account's own profile.
type ProfileResp 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     []string `json:"variant_preferences"`
	// Banner is the advertising-banner block, present only for a viewer eligible to
	// see the banner. The gateway forwards it verbatim into the Profile payload.
	Banner *BannerResp `json:"banner,omitempty"`
	// Ads is the post-move interstitial config (cooldowns + suppressed) for the client-mirrored gate.
	Ads *AdsResp `json:"ads,omitempty"`
	// Email is the confirmed email ("" when none); TelegramLinked/VkLinked report an
	// attached platform identity — they drive the profile's link/unlink/change controls.
	Email          string `json:"email"`
	TelegramLinked bool   `json:"telegram_linked"`
	VkLinked       bool   `json:"vk_linked"`
	// DictVersions is the current dictionary version per game variant, forwarded verbatim
	// into the Profile payload so an offline-capable client preloads the matching dawg.
	DictVersions []DictVersion `json:"dict_versions,omitempty"`
	// GameLimits is the caller's tier active-game caps per kind (-1 = unlimited), forwarded verbatim
	// into the Profile payload for the client's per-kind New-Game lock.
	GameLimits *GameLimitsResp `json:"game_limits,omitempty"`
}

// GameLimitsResp is the caller's tier active-game caps per kind (-1 = unlimited) in the profile.
type GameLimitsResp struct {
	VsAI    int `json:"vs_ai"`
	Random  int `json:"random"`
	Friends int `json:"friends"`
}

// AdsResp is the post-move interstitial config in the profile: the client-mirrored cooldowns
// (seconds) and whether ads are suppressed in the caller's context.
type AdsResp struct {
	CooldownGlobalS int  `json:"cooldown_global_s"`
	CooldownVsAiS   int  `json:"cooldown_vs_ai_s"`
	CooldownHintS   int  `json:"cooldown_hint_s"`
	Suppressed      bool `json:"suppressed"`
}

// DictVersion pairs a game variant's stable label with its current dictionary version.
type DictVersion struct {
	Variant string `json:"variant"`
	Version string `json:"version"`
}

// BannerResp is the advertising-banner block of an eligible viewer's profile: the
// campaigns to rotate and the global display timings the client rotator reads.
type BannerResp struct {
	Campaigns []BannerCampaignResp `json:"campaigns"`
	Timings   BannerTimingsResp    `json:"timings"`
}

// BannerCampaignResp is one campaign in the rotation feed: a GCD-reduced show
// weight and its messages, in display order, already resolved to one language.
// The override_* colours are the optional per-campaign palette (empty when the
// campaign keeps the neutral theme tokens); they ride through to the client
// verbatim, mirroring the backend DTO.
type BannerCampaignResp struct {
	Weight           int      `json:"weight"`
	Messages         []string `json:"messages"`
	OverrideBg       string   `json:"override_bg,omitempty"`
	OverrideFg       string   `json:"override_fg,omitempty"`
	OverrideLink     string   `json:"override_link,omitempty"`
	OverrideBgDark   string   `json:"override_bg_dark,omitempty"`
	OverrideFgDark   string   `json:"override_fg_dark,omitempty"`
	OverrideLinkDark string   `json:"override_link_dark,omitempty"`
}

// BannerTimingsResp mirrors the backend's global display timings.
type BannerTimingsResp struct {
	HoldMs         int `json:"hold_ms"`
	EdgePauseMs    int `json:"edge_pause_ms"`
	ScrollPxPerSec int `json:"scroll_px_per_sec"`
	FadeOutMs      int `json:"fade_out_ms"`
	GapMs          int `json:"gap_ms"`
	FadeInMs       int `json:"fade_in_ms"`
}

// LinkResultResp is the result of an account link/merge step. Status is
// "linked", "merge_required" (the secondary_* fields summarise the other account) or
// "merged". Token is a switched-session token (a guest initiator's durable
// counterpart won); Profile is the surviving/active account's profile.
type LinkResultResp struct {
	Status           string       `json:"status"`
	SecondaryUserID  string       `json:"secondary_user_id"`
	SecondaryName    string       `json:"secondary_display_name"`
	SecondaryGames   int          `json:"secondary_games"`
	SecondaryFriends int          `json:"secondary_friends"`
	Token            string       `json:"token"`
	Profile          *ProfileResp `json:"profile"`
}

// TileJSON is one tile in a decoded move response (history, move result, hint); its Letter
// is a concrete character (the move journal is kept in letters).
type TileJSON struct {
	Row    int    `json:"row"`
	Col    int    `json:"col"`
	Letter string `json:"letter"`
	Blank  bool   `json:"blank"`
}

// PlayTileJSON is one inbound tile to place, addressed by alphabet index. For a
// blank, Letter is the designated letter's index and Blank is true.
type PlayTileJSON struct {
	Row    int  `json:"row"`
	Col    int  `json:"col"`
	Letter int  `json:"letter"`
	Blank  bool `json:"blank"`
}

// MoveRecordResp is a decoded move.
type MoveRecordResp struct {
	Player  int        `json:"player"`
	Action  string     `json:"action"`
	Dir     string     `json:"dir"`
	MainRow int        `json:"main_row"`
	MainCol int        `json:"main_col"`
	Tiles   []TileJSON `json:"tiles"`
	Words   []string   `json:"words"`
	Count   int        `json:"count"`
	Score   int        `json:"score"`
	Total   int        `json:"total"`
}

// SeatResp is one seat's public standing. Deleted marks a seat whose account has been
// deleted, so the client can hide the social controls aimed at it.
type SeatResp 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     bool   `json:"deleted"`
}

// GameResp is the shared game summary.
type GameResp 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"`
	VsAI                 bool       `json:"vs_ai"`
	MoveCount            int        `json:"move_count"`
	EndReason            string     `json:"end_reason"`
	LastActivityUnix     int64      `json:"last_activity_unix"`
	Seats                []SeatResp `json:"seats"`
	UnreadChat           bool       `json:"unread_chat"`
	UnreadMessages       bool       `json:"unread_messages"`
	Kind                 int        `json:"kind"`
	NoRepeatWords        bool       `json:"no_repeat_words"`
}

// MoveResultResp 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.
type MoveResultResp struct {
	Move   MoveRecordResp `json:"move"`
	Game   GameResp       `json:"game"`
	Rack   []int          `json:"rack"`
	BagLen int            `json:"bag_len"`
}

// AlphabetEntryJSON is one letter of a variant's alphabet (its index, concrete letter and
// tile value), present in StateResp only when the client requested it.
type AlphabetEntryJSON struct {
	Index  int    `json:"index"`
	Letter string `json:"letter"`
	Value  int    `json:"value"`
}

// StateResp is a player's view of a game. Rack carries wire alphabet indices;
// Alphabet is present only when the request asked for it.
type StateResp struct {
	Game                  GameResp            `json:"game"`
	Seat                  int                 `json:"seat"`
	Rack                  []int               `json:"rack"`
	BagLen                int                 `json:"bag_len"`
	HintsRemaining        int                 `json:"hints_remaining"`
	HintUnlockLeftSeconds int                 `json:"hint_unlock_left_seconds"`
	Alphabet              []AlphabetEntryJSON `json:"alphabet,omitempty"`
}

// MatchResp reports an auto-match outcome.
type MatchResp struct {
	Matched bool      `json:"matched"`
	Game    *GameResp `json:"game,omitempty"`
}

// ChatResp is a stored chat message.
type ChatResp 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"`
}

// TelegramAuth provisions/finds the Telegram account and mints a session, seeding a
// brand-new account's display name and language from the validated launch fields, its
// time zone from browserTz (the client's detected "±HH:MM" UTC offset) and, from the
// validated launch deep-link startParam, its variant preferences (first contact only).
func (c *Client) TelegramAuth(ctx context.Context, externalID, languageCode, username, firstName, browserTz, startParam, subtype string) (SessionResp, error) {
	var out SessionResp
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/telegram", "", "",
		map[string]string{
			"external_id":   externalID,
			"language_code": languageCode,
			"username":      username,
			"first_name":    firstName,
			"browser_tz":    browserTz,
			"start_param":   startParam,
			"subtype":       subtype,
		}, &out)
	return out, err
}

// VKAuth provisions/finds the VK account and mints a session, seeding a brand-new
// account's preferred language from languageCode (the vk_language hint), its display
// name from displayName (read client-side via VKWebAppGetUserInfo, since VK omits the
// name from the signed launch params) and its time zone from browserTz (the client's
// detected "±HH:MM" UTC offset). All seeds apply on first contact only.
func (c *Client) VKAuth(ctx context.Context, externalID, languageCode, displayName, browserTz, subtype string) (SessionResp, error) {
	var out SessionResp
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/vk", "", "",
		map[string]string{
			"external_id":   externalID,
			"language_code": languageCode,
			"display_name":  displayName,
			"browser_tz":    browserTz,
			"subtype":       subtype,
		}, &out)
	return out, err
}

// PushTargetResp is a recipient's out-of-app push routing data: their Telegram
// external_id (empty when they have no Telegram identity), preferred language, and
// whether they confined notifications to the in-app stream.
type PushTargetResp struct {
	ExternalID             string `json:"external_id"`
	Language               string `json:"language"`
	NotificationsInAppOnly bool   `json:"notifications_in_app_only"`
}

// PushTarget resolves a user id to their out-of-app Telegram routing data (the
// gateway uses it to decide whether to deliver an event over platform push).
func (c *Client) PushTarget(ctx context.Context, userID string) (PushTargetResp, error) {
	var out PushTargetResp
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/push-target", "", "",
		map[string]string{"user_id": userID}, &out)
	return out, err
}

// ChatAccessResp is a user's moderated-chat write eligibility: ExternalID is their
// Telegram identity (empty when they have none, so the gateway has nothing to gate),
// Registered whether an account was found, and Eligible the final gate the bot applies
// (registered and neither admin-suspended nor chat-muted).
type ChatAccessResp struct {
	ExternalID string `json:"external_id"`
	Registered bool   `json:"registered"`
	Eligible   bool   `json:"eligible"`
}

// ChatEligibility resolves a Telegram identity to its moderated-chat write
// eligibility — the join path, when the bot sees a user enter the chat.
func (c *Client) ChatEligibility(ctx context.Context, externalID string) (ChatAccessResp, error) {
	var out ChatAccessResp
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/chat-access", "", "",
		map[string]string{"external_id": externalID}, &out)
	return out, err
}

// ChatAccessByUser resolves an account id to its Telegram identity and current
// moderated-chat write eligibility — the change path, for a chat-access-changed event.
func (c *Client) ChatAccessByUser(ctx context.Context, userID string) (ChatAccessResp, error) {
	var out ChatAccessResp
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/chat-access", "", "",
		map[string]string{"user_id": userID}, &out)
	return out, err
}

// GuestAuth provisions a guest account and mints a session, seeding its time zone
// from browserTz (the client's detected "±HH:MM" UTC offset) and its interface
// language from language (the client's detected locale; best-effort).
func (c *Client) GuestAuth(ctx context.Context, browserTz, subtype, language string) (SessionResp, error) {
	var out SessionResp
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/guest", "", "",
		map[string]string{"browser_tz": browserTz, "subtype": subtype, "language": language}, &out)
	return out, err
}

// EmailRequest asks the backend to mail a login code, provisioning the account on
// first contact; browserTz (the client's detected "±HH:MM" UTC offset) seeds the new
// account's time zone, since the email account is created here, not at login. pwa marks a
// request from an installed PWA, so the backend omits the one-tap confirm link from the email.
func (c *Client) EmailRequest(ctx context.Context, email, browserTz, language string, pwa bool) error {
	return c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/email/request", "", "",
		map[string]any{"email": email, "browser_tz": browserTz, "language": language, "pwa": pwa}, nil)
}

// EmailLogin verifies a login code and mints a session.
func (c *Client) EmailLogin(ctx context.Context, email, code, subtype string) (SessionResp, error) {
	var out SessionResp
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/email/login", "", "",
		map[string]string{"email": email, "code": code, "subtype": subtype}, &out)
	return out, err
}

// ConfirmLinkResp is the backend's outcome of a one-tap deeplink confirmation: for a
// login Session carries the minted credential; for a link Status is "confirmed" or
// "merge_required".
type ConfirmLinkResp struct {
	Purpose string       `json:"purpose"`
	Status  string       `json:"status"`
	Session *SessionResp `json:"session,omitempty"`
}

// EmailConfirmLink verifies a one-tap deeplink token; the token is the authorization.
func (c *Client) EmailConfirmLink(ctx context.Context, token string) (ConfirmLinkResp, error) {
	var out ConfirmLinkResp
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/email/confirm-link", "", "",
		map[string]string{"token": token}, &out)
	return out, err
}

// ResolveSession maps a token to its account id, guest flag, and trusted platform
// (gateway session-cache miss). The guest flag lets the edge gate guest-forbidden
// ops; the platform ("<kind>/<subtype>", empty for an untrusted session) is injected
// as X-Platform on the caller's backend requests.
func (c *Client) ResolveSession(ctx context.Context, token string) (string, bool, string, error) {
	var out struct {
		UserID          string `json:"user_id"`
		IsGuest         bool   `json:"is_guest"`
		PlatformKind    string `json:"platform_kind"`
		PlatformSubtype string `json:"platform_subtype"`
	}
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/resolve", "", "",
		map[string]string{"token": token}, &out)
	platform := ""
	if out.PlatformKind != "" {
		platform = out.PlatformKind + "/" + out.PlatformSubtype
	}
	return out.UserID, out.IsGuest, platform, err
}

// Profile returns the authenticated account's profile.
func (c *Client) Profile(ctx context.Context, userID string) (ProfileResp, error) {
	var out ProfileResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/profile", userID, "", nil, &out)
	return out, err
}

// WalletSegmentResp is one chip balance in the wallet: the funding source, the chip count, and
// whether it is spendable in the caller's current context.
type WalletSegmentResp struct {
	Source    string `json:"source"`
	Chips     int    `json:"chips"`
	Spendable bool   `json:"spendable"`
}

// WalletResp is the caller's wallet: the context-visible chip segments and the context-applicable
// benefits (no-ads term end as unix millis / forever flag, and the available hints), plus the
// rewarded-video payout available in the current context (0 = unavailable — outside VK/unconfigured).
type WalletResp struct {
	Segments     []WalletSegmentResp `json:"segments"`
	AdsForever   bool                `json:"ads_forever"`
	AdsPaidUntil int64               `json:"ads_paid_until_ms"`
	Hints        int                 `json:"hints"`
	RewardChips  int                 `json:"reward_chips"`
}

// walletBuyBody is the chip-spend request body.
type walletBuyBody struct {
	ProductID string `json:"product_id"`
}

// walletRewardBody is the rewarded-video credit request: the per-view idempotency nonce.
type walletRewardBody struct {
	Nonce string `json:"nonce"`
}

// WalletReward credits a watched rewarded video and returns the updated wallet (client-attested + a
// backend daily/hourly cap). A reached cap or unconfigured payout surfaces as an APIError domain code.
func (c *Client) WalletReward(ctx context.Context, userID, nonce string) (WalletResp, error) {
	var out WalletResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/wallet/reward", userID, "", walletRewardBody{Nonce: nonce}, &out)
	return out, err
}

// Wallet fetches the caller's wallet in their current execution context.
func (c *Client) Wallet(ctx context.Context, userID string) (WalletResp, error) {
	var out WalletResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/wallet", userID, "", nil, &out)
	return out, err
}

// WalletBuy spends chips on a chip-priced value and returns the updated wallet.
func (c *Client) WalletBuy(ctx context.Context, userID, productID string) (WalletResp, error) {
	var out WalletResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/wallet/buy", userID, "", walletBuyBody{ProductID: productID}, &out)
	return out, err
}

// walletOrderBody is the POST body of an order: the chip pack to fund.
type walletOrderBody struct {
	ProductID string `json:"product_id"`
}

// WalletOrderResp is a created order: its id and the rail's launch details. RedirectURL is the
// provider's hosted-payment URL (direct); Rail names the settling rail; for the Telegram Stars rail
// InvoiceTitle and InvoiceAmount (whole stars) are the invoice the gateway mints via the bot.
type WalletOrderResp struct {
	OrderID       string `json:"order_id"`
	RedirectURL   string `json:"redirect_url"`
	Rail          string `json:"rail"`
	InvoiceTitle  string `json:"invoice_title"`
	InvoiceAmount int64  `json:"invoice_amount"`
}

// WalletOrder opens a pending order to fund a chip pack and returns the rail's launch details.
func (c *Client) WalletOrder(ctx context.Context, userID, productID string) (WalletOrderResp, error) {
	var out WalletOrderResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/wallet/order", userID, "", walletOrderBody{ProductID: productID}, &out)
	return out, err
}

// preCheckoutResp is the backend intake's pre_checkout answer.
type preCheckoutResp struct {
	OK     bool   `json:"ok"`
	Reason string `json:"reason"`
}

// ValidatePreCheckout asks the backend intake whether a Telegram Stars pre_checkout_query for
// orderID paying amount in currency may be approved, before the charge. It returns the approval and
// a short decline reason for the payer. It backs the bot's pre_checkout gate through the bot-link.
func (c *Client) ValidatePreCheckout(ctx context.Context, orderID string, amount int64, currency string) (bool, string, error) {
	var out preCheckoutResp
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/payments/telegram/precheckout", "", "",
		map[string]any{"order_id": orderID, "amount": amount, "currency": currency}, &out)
	return out.OK, out.Reason, err
}

// telegramPaymentResp is the backend intake's credit outcome.
type telegramPaymentResp struct {
	Credited bool `json:"credited"`
}

// TelegramPayment forwards a completed Telegram Stars payment from the bot's outbox to the backend
// intake, which credits the order idempotently on chargeID. It reports whether the order was
// credited (or already had been); a transport error is a transient failure the bot retries.
func (c *Client) TelegramPayment(ctx context.Context, orderID, chargeID string, amount, telegramUserID int64) (bool, error) {
	var out telegramPaymentResp
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/payments/telegram/payment", "", "",
		map[string]any{
			"order_id":                   orderID,
			"telegram_payment_charge_id": chargeID,
			"amount":                     amount,
			"telegram_user_id":           telegramUserID,
		}, &out)
	return out.Credited, err
}

// robokassaResultResp is the backend intake's reply: the body to echo back to Robokassa.
type robokassaResultResp struct {
	Response string `json:"response"`
}

// RobokassaResult forwards a Robokassa Result callback's parameters to the backend intake (the
// single writer, which verifies the signature and credits) and returns the body to echo to
// Robokassa ("OK<InvId>"). params carries the provider's raw form fields; channel selects the
// per-shop signature verifier (empty → the web shop).
func (c *Client) RobokassaResult(ctx context.Context, channel string, params map[string]string) (string, error) {
	var out robokassaResultResp
	path := "/api/v1/internal/payments/robokassa/result"
	if channel != "" {
		path += "?channel=" + url.QueryEscape(channel)
	}
	err := c.do(ctx, http.MethodPost, path, "", "", params, &out)
	return out.Response, err
}

// YooKassaNotify forwards a YooKassa webhook body verbatim to the backend intake (the single writer,
// which re-reads the payment from the provider before acting on it — YooKassa does not sign
// notifications). The backend answers 200 for anything durably decided and 5xx only for a transient
// failure, so a nil error means the provider may stop redelivering.
// clientIP is the true sender address, which only the gateway sees; the backend checks it against
// YooKassa's published sender ranges.
func (c *Client) YooKassaNotify(ctx context.Context, clientIP string, body json.RawMessage) error {
	return c.do(ctx, http.MethodPost, "/api/v1/internal/payments/yookassa/notify", "", clientIP, body, nil)
}

// VKCallback forwards a gateway-verified VK payment callback's parameters to the backend intake and
// returns the backend's raw VK response envelope (`{"response":…}` or `{"error":…}`) for the gateway
// to relay to VK verbatim. The backend answers 200 in every case (VK's protocol), so a transport
// error here is a genuine proxy failure.
func (c *Client) VKCallback(ctx context.Context, params map[string]string) (json.RawMessage, error) {
	var out json.RawMessage
	err := c.do(ctx, http.MethodPost, "/api/v1/internal/payments/vk/callback", "", "", params, &out)
	return out, err
}

// CatalogAtomResp is one atom line of a storefront product: the value type it grants and quantity.
type CatalogAtomResp struct {
	AtomType string `json:"atom_type"`
	Quantity int    `json:"quantity"`
}

// CatalogProductResp is one storefront product for the caller's context: a chip-priced value
// (chips set) or a chip pack priced in the context method (money_amount + money_currency).
type CatalogProductResp struct {
	Kind          string            `json:"kind"`
	ProductID     string            `json:"product_id"`
	Title         string            `json:"title"`
	Chips         int               `json:"chips"`
	MoneyAmount   int64             `json:"money_amount"`
	MoneyCurrency string            `json:"money_currency"`
	Atoms         []CatalogAtomResp `json:"atoms"`
}

// CatalogResp is the storefront: the products visible and purchasable in the caller's context.
type CatalogResp struct {
	Products []CatalogProductResp `json:"products"`
}

// Catalog fetches the storefront in the caller's current execution context.
func (c *Client) Catalog(ctx context.Context, userID string) (CatalogResp, error) {
	var out CatalogResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/wallet/catalog", userID, "", nil, &out)
	return out, err
}

// BlockStatusResp is the caller's current manual-block state. Until is an RFC3339 UTC instant for
// a temporary block, empty for a permanent one or when not blocked; Reason is resolved to the
// account's language, empty when none was cited.
type BlockStatusResp struct {
	Blocked   bool   `json:"blocked"`
	Permanent bool   `json:"permanent"`
	Until     string `json:"until"`
	Reason    string `json:"reason"`
}

// BlockStatus fetches the caller's current block. It is the one user endpoint a blocked account
// can still reach (the backend's suspension gate exempts it), so the UI can render the blocked
// screen.
func (c *Client) BlockStatus(ctx context.Context, userID string) (BlockStatusResp, error) {
	var out BlockStatusResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/block-status", userID, "", nil, &out)
	return out, err
}

// SubmitPlay commits a placement on the player's turn. The tiles are addressed by alphabet
// index.
func (c *Client) SubmitPlay(ctx context.Context, userID, gameID string, tiles []PlayTileJSON) (MoveResultResp, error) {
	var out MoveResultResp
	body := map[string]any{"tiles": tiles}
	err := c.do(ctx, http.MethodPost, "/api/v1/user/games/"+url.PathEscape(gameID)+"/play", userID, "", body, &out)
	return out, err
}

// GameState returns the player's view of a game. When includeAlphabet is set the backend
// embeds the variant's alphabet table; the client asks for it on a per-variant
// cache miss only.
func (c *Client) GameState(ctx context.Context, userID, gameID string, includeAlphabet bool) (StateResp, error) {
	var out StateResp
	path := "/api/v1/user/games/" + url.PathEscape(gameID) + "/state"
	if includeAlphabet {
		path += "?include_alphabet=true"
	}
	err := c.do(ctx, http.MethodGet, path, userID, "", nil, &out)
	return out, err
}

// Enqueue starts a quick game for a variant under a per-turn word rule (multipleWords
// true is standard Scrabble, false the single-word rule). vsAI true starts an honest-AI
// game against a robot instead of human auto-match.
func (c *Client) Enqueue(ctx context.Context, userID, variant string, multipleWords, vsAI bool) (MatchResp, error) {
	var out MatchResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/lobby/enqueue", userID, "",
		map[string]any{"variant": variant, "multiple_words_per_turn": multipleWords, "vs_ai": vsAI}, &out)
	return out, err
}

// Poll reports whether the caller has been paired since queueing.
func (c *Client) Poll(ctx context.Context, userID string) (MatchResp, error) {
	var out MatchResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/lobby/poll", userID, "", nil, &out)
	return out, err
}

// Cancel removes the caller from the auto-match pool (idempotent; 204 No Content).
func (c *Client) Cancel(ctx context.Context, userID string) error {
	return c.do(ctx, http.MethodPost, "/api/v1/user/lobby/cancel", userID, "", nil, nil)
}

// ChatPost stores a chat message, forwarding the client IP for moderation.
func (c *Client) ChatPost(ctx context.Context, userID, gameID, body, clientIP string) (ChatResp, error) {
	var out ChatResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/games/"+url.PathEscape(gameID)+"/chat", userID, clientIP,
		map[string]string{"body": body}, &out)
	return out, err
}

// HintResultResp is the top-ranked move plus the remaining hint budget.
type HintResultResp struct {
	Move           MoveRecordResp `json:"move"`
	HintsRemaining int            `json:"hints_remaining"`
	WalletBalance  int            `json:"wallet_balance"`
}

// EvalResultResp is an unlimited move preview. Dir is the orientation the backend
// inferred ("H"/"V", empty when illegal); Words lists the words formed, main word first.
type EvalResultResp struct {
	Legal bool     `json:"legal"`
	Score int      `json:"score"`
	Words []string `json:"words"`
	Dir   string   `json:"dir"`
}

// WordCheckResp is a dictionary lookup outcome.
type WordCheckResp struct {
	Word  string `json:"word"`
	Legal bool   `json:"legal"`
}

// HistoryResp is a game's decoded move journal.
type HistoryResp struct {
	GameID string           `json:"game_id"`
	Moves  []MoveRecordResp `json:"moves"`
}

// GameListResp is the caller's games for the lobby. AtGameLimit is true when the caller
// has reached the simultaneous quick-game cap, so the lobby disables "New Game".
type GameListResp struct {
	Games       []GameResp `json:"games"`
	AtGameLimit bool       `json:"at_game_limit"`
}

// ChatListResp is a game's chat history.
type ChatListResp struct {
	Messages []ChatResp `json:"messages"`
}

func (c *Client) gamePath(gameID, suffix string) string {
	return "/api/v1/user/games/" + url.PathEscape(gameID) + suffix
}

// Pass forfeits the player's turn.
func (c *Client) Pass(ctx context.Context, userID, gameID string) (MoveResultResp, error) {
	var out MoveResultResp
	err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/pass"), userID, "", struct{}{}, &out)
	return out, err
}

// Exchange swaps the chosen rack tiles back into the bag. Tiles are wire alphabet indices
// (a blank is engine.BlankIndex).
func (c *Client) Exchange(ctx context.Context, userID, gameID string, tiles []int) (MoveResultResp, error) {
	var out MoveResultResp
	err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/exchange"), userID, "",
		map[string]any{"tiles": tiles}, &out)
	return out, err
}

// Resign resigns the player from the game.
func (c *Client) Resign(ctx context.Context, userID, gameID string) (MoveResultResp, error) {
	var out MoveResultResp
	err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/resign"), userID, "", struct{}{}, &out)
	return out, err
}

// Hint reveals the top-ranked move and spends a hint.
func (c *Client) Hint(ctx context.Context, userID, gameID string) (HintResultResp, error) {
	var out HintResultResp
	err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/hint"), userID, "", struct{}{}, &out)
	return out, err
}

// GetDraft returns the player's saved composition for a game as the backend's
// raw JSON body. The gateway forwards it verbatim, never interpreting its shape.
func (c *Client) GetDraft(ctx context.Context, userID, gameID string) (json.RawMessage, error) {
	var out json.RawMessage
	err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/draft"), userID, "", nil, &out)
	return out, err
}

// SaveDraft upserts the player's composition for a game. body is the client's
// {rack_order, board_tiles} JSON, forwarded verbatim — a json.RawMessage marshals as-is, so
// there is no double-encode.
func (c *Client) SaveDraft(ctx context.Context, userID, gameID string, body json.RawMessage) error {
	return c.do(ctx, http.MethodPut, c.gamePath(gameID, "/draft"), userID, "", body, nil)
}

// HideGame hides a finished game from the caller's own games list. The action is
// per-account and irreversible; the game stays visible to the other players.
func (c *Client) HideGame(ctx context.Context, userID, gameID string) error {
	return c.do(ctx, http.MethodPost, c.gamePath(gameID, "/hide"), userID, "", struct{}{}, nil)
}

// Evaluate previews a tentative play's legality and score. The tiles are addressed by
// alphabet index.
func (c *Client) Evaluate(ctx context.Context, userID, gameID string, tiles []PlayTileJSON) (EvalResultResp, error) {
	var out EvalResultResp
	err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/evaluate"), userID, "",
		map[string]any{"tiles": tiles}, &out)
	return out, err
}

// DictBytes fetches the raw serialized dictionary for a (variant, version) pair,
// backing the client-side local move preview. It returns the blob and the
// backend's Cache-Control header, forwarded so the immutable blob is cached hard.
func (c *Client) DictBytes(ctx context.Context, userID, variant, version string) ([]byte, string, error) {
	return c.getRaw(ctx, "/api/v1/user/dict/"+url.PathEscape(variant)+"/"+url.PathEscape(version), userID, "Cache-Control")
}

// CheckWord looks a word up in the game's pinned dictionary. The word is carried as
// repeated ?idx= alphabet indices; the backend echoes the decoded concrete word.
func (c *Client) CheckWord(ctx context.Context, userID, gameID string, word []int) (WordCheckResp, error) {
	var out WordCheckResp
	q := url.Values{}
	for _, x := range word {
		q.Add("idx", strconv.Itoa(x))
	}
	err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/check_word")+"?"+q.Encode(), userID, "", nil, &out)
	return out, err
}

// Complaint disputes a word-check result.
func (c *Client) Complaint(ctx context.Context, userID, gameID, word, note string) error {
	return c.do(ctx, http.MethodPost, c.gamePath(gameID, "/complaint"), userID, "",
		map[string]string{"word": word, "note": note}, nil)
}

// History returns a game's decoded move journal.
func (c *Client) History(ctx context.Context, userID, gameID string) (HistoryResp, error) {
	var out HistoryResp
	err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/history"), userID, "", nil, &out)
	return out, err
}

// ChatList returns a game's chat history.
func (c *Client) ChatList(ctx context.Context, userID, gameID string) (ChatListResp, error) {
	var out ChatListResp
	err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/chat"), userID, "", nil, &out)
	return out, err
}

// Nudge posts a nudge to the player whose turn is awaited.
func (c *Client) Nudge(ctx context.Context, userID, gameID string) (ChatResp, error) {
	var out ChatResp
	err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/nudge"), userID, "", struct{}{}, &out)
	return out, err
}

// MarkChatRead acknowledges that the caller has read the game's chat (sent when they open
// the move history or chat), so the backend clears their unread bits and records the
// publish-to-read latency.
func (c *Client) MarkChatRead(ctx context.Context, userID, gameID string) error {
	return c.do(ctx, http.MethodPost, c.gamePath(gameID, "/chat/read"), userID, "", struct{}{}, nil)
}

// GamesList returns the caller's active and finished games.
func (c *Client) GamesList(ctx context.Context, userID string) (GameListResp, error) {
	var out GameListResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/games", userID, "", nil, &out)
	return out, err
}
