package backendclient

import (
	"context"
	"net/http"
	"net/url"
	"strings"
)

// The response structs and client methods mirror the backend's social,
// account and history JSON DTOs. The transcode layer maps them to FlatBuffers.

// AccountRefResp is a referenced account with its display name resolved.
type AccountRefResp struct {
	AccountID   string `json:"account_id"`
	DisplayName string `json:"display_name"`
}

// FriendListResp is the caller's accepted friends.
type FriendListResp struct {
	Friends []AccountRefResp `json:"friends"`
}

// IncomingListResp is the friend requests awaiting the caller.
type IncomingListResp struct {
	Requests []AccountRefResp `json:"requests"`
}

// OutgoingListResp is the addressees the caller has already requested (a live pending
// request or one the addressee declined) and cannot re-request, plus the per-game
// disguised-robot requests (not real accounts).
type OutgoingListResp struct {
	Requests []AccountRefResp     `json:"requests"`
	Robots   []RobotFriendReqResp `json:"robots"`
}

// RobotFriendReqResp is one per-game disguised-robot friend request: the row id, the game
// name the player saw, and the game + seat it was sent in.
type RobotFriendReqResp struct {
	ID          string `json:"id"`
	DisplayName string `json:"display_name"`
	GameID      string `json:"game_id"`
	Seat        int    `json:"seat"`
}

// FriendCodeResp is a freshly issued one-time friend code.
type FriendCodeResp struct {
	Code          string `json:"code"`
	ExpiresAtUnix int64  `json:"expires_at_unix"`
}

// RedeemResultResp reports the friend gained by redeeming a code.
type RedeemResultResp struct {
	Friend AccountRefResp `json:"friend"`
}

// BlockListResp is the accounts the caller has blocked, plus the per-game disguised-robot
// blocks (not real accounts).
type BlockListResp struct {
	Blocked []AccountRefResp `json:"blocked"`
	Robots  []RobotBlockResp `json:"robots"`
}

// RobotBlockResp is one per-game disguised-robot block: the row id (to unblock it), the game
// name the player saw, and the game + seat it was blocked in.
type RobotBlockResp struct {
	ID          string `json:"id"`
	DisplayName string `json:"display_name"`
	GameID      string `json:"game_id"`
	Seat        int    `json:"seat"`
}

// StatsResp is a durable account's lifetime statistics. BestMoves breaks the best move
// down per variant, carrying the word itself; it is absent for an account with no recorded
// play and lists only variants the account has played.
type StatsResp struct {
	Wins          int            `json:"wins"`
	Losses        int            `json:"losses"`
	Draws         int            `json:"draws"`
	MaxGamePoints int            `json:"max_game_points"`
	MaxWordPoints int            `json:"max_word_points"`
	Moves         int            `json:"moves"`
	HintsUsed     int            `json:"hints_used"`
	BestMoves     []BestMoveResp `json:"best_moves"`
}

// BestMoveResp is one variant's best play: the variant label, the play's total score and
// its main word as ordered tiles (letter, value, blank — value 0 for a blank).
type BestMoveResp struct {
	Variant string             `json:"variant"`
	Score   int                `json:"score"`
	Word    []BestMoveTileResp `json:"word"`
}

// BestMoveTileResp is one letter cell of a best-move word.
type BestMoveTileResp struct {
	Letter string `json:"letter"`
	Value  int    `json:"value"`
	Blank  bool   `json:"blank"`
}

// InvitationInviteeResp is one invitee's seat and response with their name.
type InvitationInviteeResp struct {
	AccountID   string `json:"account_id"`
	DisplayName string `json:"display_name"`
	Seat        int    `json:"seat"`
	Response    string `json:"response"`
}

// InvitationResp is a friend-game invitation with its settings and invitees.
type InvitationResp struct {
	ID                   string                  `json:"id"`
	Inviter              AccountRefResp          `json:"inviter"`
	Invitees             []InvitationInviteeResp `json:"invitees"`
	Variant              string                  `json:"variant"`
	TurnTimeoutSecs      int                     `json:"turn_timeout_secs"`
	HintsAllowed         bool                    `json:"hints_allowed"`
	HintsPerPlayer       int                     `json:"hints_per_player"`
	MultipleWordsPerTurn bool                    `json:"multiple_words_per_turn"`
	DropoutTiles         string                  `json:"dropout_tiles"`
	Status               string                  `json:"status"`
	GameID               string                  `json:"game_id"`
	ExpiresAtUnix        int64                   `json:"expires_at_unix"`
}

// InvitationListResp is the caller's open invitations.
type InvitationListResp struct {
	Invitations []InvitationResp `json:"invitations"`
}

// GcgResp is a finished game's GCG export.
type GcgResp struct {
	GameID   string `json:"game_id"`
	Filename string `json:"filename"`
	Content  string `json:"content"`
}

// InvitationParams are the settings the inviter chooses for a friend game.
type InvitationParams struct {
	InviteeIDs      []string
	Variant         string
	TurnTimeoutSecs int
	HintsAllowed    bool
	HintsPerPlayer  int
	DropoutTiles    string
	// MultipleWordsPerTurn true is standard Scrabble; false the single-word rule.
	MultipleWordsPerTurn bool
}

// --- friends ---

// SendFriendRequest sends a friend request to a played opponent. A non-empty gameID
// marks an in-game request, so a disguised-robot opponent is recorded as a per-game
// request; it is empty for any non-game path.
func (c *Client) SendFriendRequest(ctx context.Context, userID, targetID, gameID string) error {
	return c.do(ctx, http.MethodPost, "/api/v1/user/friends/request", userID, "",
		map[string]string{"account_id": targetID, "game_id": gameID}, nil)
}

// RespondFriendRequest accepts or declines an incoming request.
func (c *Client) RespondFriendRequest(ctx context.Context, userID, requesterID string, accept bool) error {
	return c.do(ctx, http.MethodPost, "/api/v1/user/friends/respond", userID, "",
		map[string]any{"requester_id": requesterID, "accept": accept}, nil)
}

// CancelFriendRequest withdraws the caller's own pending request.
func (c *Client) CancelFriendRequest(ctx context.Context, userID, targetID string) error {
	return c.do(ctx, http.MethodPost, "/api/v1/user/friends/cancel", userID, "",
		map[string]string{"account_id": targetID}, nil)
}

// Unfriend removes a friendship.
func (c *Client) Unfriend(ctx context.Context, userID, targetID string) error {
	return c.do(ctx, http.MethodDelete, "/api/v1/user/friends/"+url.PathEscape(targetID), userID, "", nil, nil)
}

// ListFriends returns the caller's accepted friends.
func (c *Client) ListFriends(ctx context.Context, userID string) (FriendListResp, error) {
	var out FriendListResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/friends", userID, "", nil, &out)
	return out, err
}

// ListIncoming returns the friend requests awaiting the caller.
func (c *Client) ListIncoming(ctx context.Context, userID string) (IncomingListResp, error) {
	var out IncomingListResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/friends/incoming", userID, "", nil, &out)
	return out, err
}

// ListOutgoing returns the addressees the caller has already requested (pending or
// declined) and cannot re-request.
func (c *Client) ListOutgoing(ctx context.Context, userID string) (OutgoingListResp, error) {
	var out OutgoingListResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/friends/outgoing", userID, "", nil, &out)
	return out, err
}

// IssueFriendCode issues a one-time friend code for the caller.
func (c *Client) IssueFriendCode(ctx context.Context, userID string) (FriendCodeResp, error) {
	var out FriendCodeResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/friends/code", userID, "", struct{}{}, &out)
	return out, err
}

// RedeemFriendCode redeems a friend code, befriending its issuer.
func (c *Client) RedeemFriendCode(ctx context.Context, userID, code string) (RedeemResultResp, error) {
	var out RedeemResultResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/friends/code/redeem", userID, "",
		map[string]string{"code": code}, &out)
	return out, err
}

// --- blocks ---

// Block blocks an account. A non-empty gameID marks an in-game block, so a disguised-robot
// opponent is recorded as a per-game block; it is empty for a settings-screen block.
func (c *Client) Block(ctx context.Context, userID, targetID, gameID string) error {
	return c.do(ctx, http.MethodPost, "/api/v1/user/blocks", userID, "",
		map[string]string{"account_id": targetID, "game_id": gameID}, nil)
}

// Unblock removes a block.
func (c *Client) Unblock(ctx context.Context, userID, targetID string) error {
	return c.do(ctx, http.MethodDelete, "/api/v1/user/blocks/"+url.PathEscape(targetID), userID, "", nil, nil)
}

// ListBlocks returns the accounts the caller has blocked.
func (c *Client) ListBlocks(ctx context.Context, userID string) (BlockListResp, error) {
	var out BlockListResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/blocks", userID, "", nil, &out)
	return out, err
}

// --- invitations ---

// CreateInvitation proposes a friend game to the named invitees.
func (c *Client) CreateInvitation(ctx context.Context, userID string, p InvitationParams) (InvitationResp, error) {
	var out InvitationResp
	body := map[string]any{
		"invitee_ids":       p.InviteeIDs,
		"variant":           p.Variant,
		"turn_timeout_secs": p.TurnTimeoutSecs,
		"hints_allowed":     p.HintsAllowed,
		"hints_per_player":  p.HintsPerPlayer,
		"dropout_tiles":     p.DropoutTiles,

		"multiple_words_per_turn": p.MultipleWordsPerTurn,
	}
	err := c.do(ctx, http.MethodPost, "/api/v1/user/invitations", userID, "", body, &out)
	return out, err
}

// RespondInvitation accepts or declines an invitation by id.
func (c *Client) RespondInvitation(ctx context.Context, userID, invitationID string, accept bool) (InvitationResp, error) {
	var out InvitationResp
	action := "/decline"
	if accept {
		action = "/accept"
	}
	err := c.do(ctx, http.MethodPost, "/api/v1/user/invitations/"+url.PathEscape(invitationID)+action, userID, "", struct{}{}, &out)
	return out, err
}

// CancelInvitation withdraws the caller's own invitation.
func (c *Client) CancelInvitation(ctx context.Context, userID, invitationID string) error {
	return c.do(ctx, http.MethodDelete, "/api/v1/user/invitations/"+url.PathEscape(invitationID), userID, "", nil, nil)
}

// ListInvitations returns the caller's open invitations.
func (c *Client) ListInvitations(ctx context.Context, userID string) (InvitationListResp, error) {
	var out InvitationListResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/invitations", userID, "", nil, &out)
	return out, err
}

// --- profile, email, stats, gcg ---

// UpdateProfile overwrites the caller's editable profile and returns it.
func (c *Client) UpdateProfile(ctx context.Context, userID string, p ProfileResp) (ProfileResp, error) {
	var out ProfileResp
	body := map[string]any{
		"display_name":              p.DisplayName,
		"preferred_language":        p.PreferredLanguage,
		"time_zone":                 p.TimeZone,
		"away_start":                p.AwayStart,
		"away_end":                  p.AwayEnd,
		"block_chat":                p.BlockChat,
		"block_friend_requests":     p.BlockFriendRequests,
		"notifications_in_app_only": p.NotificationsInAppOnly,
		"variant_preferences":       p.VariantPreferences,
	}
	err := c.do(ctx, http.MethodPut, "/api/v1/user/profile", userID, "", body, &out)
	return out, err
}

// LinkEmailRequest asks the backend to mail a confirm-code for a link or merge.
func (c *Client) LinkEmailRequest(ctx context.Context, userID, email string) error {
	return c.do(ctx, http.MethodPost, "/api/v1/user/link/email/request", userID, "",
		map[string]string{"email": email}, nil)
}

// LinkEmailConfirm verifies the code and binds a free email or reports a required
// merge.
func (c *Client) LinkEmailConfirm(ctx context.Context, userID, email, code string) (LinkResultResp, error) {
	var out LinkResultResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/link/email/confirm", userID, "",
		map[string]string{"email": email, "code": code}, &out)
	return out, err
}

// LinkEmailMerge re-verifies the code and performs the merge.
func (c *Client) LinkEmailMerge(ctx context.Context, userID, email, code string) (LinkResultResp, error) {
	var out LinkResultResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/link/email/merge", userID, "",
		map[string]string{"email": email, "code": code}, &out)
	return out, err
}

// LinkTelegram attaches a gateway-validated Telegram identity to the caller or
// reports a required merge.
func (c *Client) LinkTelegram(ctx context.Context, userID, externalID string) (LinkResultResp, error) {
	var out LinkResultResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/link/telegram", userID, "",
		map[string]string{"external_id": externalID}, &out)
	return out, err
}

// LinkTelegramMerge merges the account owning a gateway-validated Telegram identity
// into the caller's.
func (c *Client) LinkTelegramMerge(ctx context.Context, userID, externalID string) (LinkResultResp, error) {
	var out LinkResultResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/link/telegram/merge", userID, "",
		map[string]string{"external_id": externalID}, &out)
	return out, err
}

// LinkVK attaches a gateway-validated VK identity to the caller or reports a required
// merge. externalID is the trusted vk user id resolved from the VK ID code exchange.
func (c *Client) LinkVK(ctx context.Context, userID, externalID string) (LinkResultResp, error) {
	var out LinkResultResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/link/vk", userID, "",
		map[string]string{"external_id": externalID}, &out)
	return out, err
}

// LinkVKMerge merges the account owning a gateway-validated VK identity into the
// caller's.
func (c *Client) LinkVKMerge(ctx context.Context, userID, externalID string) (LinkResultResp, error) {
	var out LinkResultResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/link/vk/merge", userID, "",
		map[string]string{"external_id": externalID}, &out)
	return out, err
}

// ChangeEmailRequest asks the backend to mail a confirm-code to a new address for an
// authenticated email change.
func (c *Client) ChangeEmailRequest(ctx context.Context, userID, email string) error {
	return c.do(ctx, http.MethodPost, "/api/v1/user/link/email/change/request", userID, "",
		map[string]string{"email": email}, nil)
}

// ChangeEmailConfirm verifies the code and atomically switches the account's email,
// returning the refreshed profile in the result.
func (c *Client) ChangeEmailConfirm(ctx context.Context, userID, email, code string) (LinkResultResp, error) {
	var out LinkResultResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/link/email/change/confirm", userID, "",
		map[string]string{"email": email, "code": code}, &out)
	return out, err
}

// DeleteRequestResp reports which account-deletion step-up the account uses.
type DeleteRequestResp struct {
	Method string `json:"method"`
}

// DeleteRequest starts account deletion: the backend mails a delete code (email accounts)
// or reports the typed-phrase path.
func (c *Client) DeleteRequest(ctx context.Context, userID string) (DeleteRequestResp, error) {
	var out DeleteRequestResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/delete/request", userID, "", nil, &out)
	return out, err
}

// DeleteConfirm verifies the step-up (a mailed code or the typed phrase) and deletes the
// account.
func (c *Client) DeleteConfirm(ctx context.Context, userID, code, phrase string) error {
	return c.do(ctx, http.MethodPost, "/api/v1/user/delete/confirm", userID, "",
		map[string]string{"code": code, "phrase": phrase}, nil)
}

// LinkUnlink detaches a platform identity (kind = "telegram" | "vk") from the caller
// and returns the refreshed profile in the result.
func (c *Client) LinkUnlink(ctx context.Context, userID, kind string) (LinkResultResp, error) {
	var out LinkResultResp
	err := c.do(ctx, http.MethodPost, "/api/v1/user/link/unlink", userID, "",
		map[string]string{"kind": kind}, &out)
	return out, err
}

// Stats returns the caller's lifetime statistics.
func (c *Client) Stats(ctx context.Context, userID string) (StatsResp, error) {
	var out StatsResp
	err := c.do(ctx, http.MethodGet, "/api/v1/user/stats", userID, "", nil, &out)
	return out, err
}

// ExportGCG returns a finished game's GCG transcript.
func (c *Client) ExportGCG(ctx context.Context, userID, gameID string) (GcgResp, error) {
	var out GcgResp
	err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/gcg"), userID, "", nil, &out)
	return out, err
}

// ExportURLResp is the minted signed download link for an export artifact.
type ExportURLResp struct {
	Path     string `json:"path"`
	Filename string `json:"filename"`
}

// ExportURL mints a signed download URL for a finished game's artifact (kind
// "png" or "gcg"). dateLocale and the localized non-play labels ride the URL so
// the server render matches the player's presentation.
func (c *Client) ExportURL(ctx context.Context, userID, gameID, kind, dateLocale, timeZone string, labels []string) (ExportURLResp, error) {
	q := url.Values{"kind": {kind}}
	if dateLocale != "" {
		q.Set("dl", dateLocale)
	}
	if timeZone != "" {
		q.Set("tz", timeZone)
	}
	if len(labels) > 0 {
		q.Set("t", strings.Join(labels, ","))
	}
	var out ExportURLResp
	err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/export-url")+"?"+q.Encode(), userID, "", nil, &out)
	return out, err
}
