package backendclient

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

	"galaxy/gateway/internal/downstream"
	lobbymodel "galaxy/model/lobby"
	"galaxy/transcoder"
)

const (
	lobbyResultCodeOK            = "ok"
	defaultLobbyErrorCodeInvalid = "invalid_request"
	defaultLobbyErrorCodeNoSubj  = "subject_not_found"
	defaultLobbyErrorCodeForbid  = "forbidden"
	defaultLobbyErrorCodeConfl   = "conflict"
	defaultLobbyErrorCodeIntErr  = "internal_error"
)

var stableLobbyErrorMessages = map[string]string{
	defaultLobbyErrorCodeInvalid: "request is invalid",
	defaultLobbyErrorCodeNoSubj:  "subject not found",
	defaultLobbyErrorCodeForbid:  "operation is forbidden for the calling user",
	defaultLobbyErrorCodeConfl:   "request conflicts with current state",
	defaultLobbyErrorCodeIntErr:  "internal server error",
}

// ExecuteLobbyCommand routes one authenticated lobby command into
// backend's `/api/v1/user/lobby/*` endpoints.
func (c *RESTClient) ExecuteLobbyCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) {
	if c == nil || c.httpClient == nil {
		return downstream.UnaryResult{}, errors.New("backendclient: execute lobby command: nil client")
	}
	if ctx == nil {
		return downstream.UnaryResult{}, errors.New("backendclient: execute lobby 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 lobby command: user_id must not be empty")
	}

	switch command.MessageType {
	case lobbymodel.MessageTypeMyGamesList:
		if _, err := transcoder.PayloadToMyGamesListRequest(command.PayloadBytes); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
		}
		return c.executeLobbyMyGames(ctx, command.UserID)
	case lobbymodel.MessageTypePublicGamesList:
		req, err := transcoder.PayloadToPublicGamesListRequest(command.PayloadBytes)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
		}
		return c.executeLobbyPublicGames(ctx, command.UserID, req)
	case lobbymodel.MessageTypeMyApplicationsList:
		if _, err := transcoder.PayloadToMyApplicationsListRequest(command.PayloadBytes); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
		}
		return c.executeLobbyMyApplications(ctx, command.UserID)
	case lobbymodel.MessageTypeMyInvitesList:
		if _, err := transcoder.PayloadToMyInvitesListRequest(command.PayloadBytes); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
		}
		return c.executeLobbyMyInvites(ctx, command.UserID)
	case lobbymodel.MessageTypeOpenEnrollment:
		req, err := transcoder.PayloadToOpenEnrollmentRequest(command.PayloadBytes)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
		}
		return c.executeLobbyOpenEnrollment(ctx, command.UserID, req)
	case lobbymodel.MessageTypeGameCreate:
		req, err := transcoder.PayloadToGameCreateRequest(command.PayloadBytes)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
		}
		return c.executeLobbyGameCreate(ctx, command.UserID, req)
	case lobbymodel.MessageTypeApplicationSubmit:
		req, err := transcoder.PayloadToApplicationSubmitRequest(command.PayloadBytes)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
		}
		return c.executeLobbyApplicationSubmit(ctx, command.UserID, req)
	case lobbymodel.MessageTypeInviteRedeem:
		req, err := transcoder.PayloadToInviteRedeemRequest(command.PayloadBytes)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
		}
		return c.executeLobbyInviteRedeem(ctx, command.UserID, req)
	case lobbymodel.MessageTypeInviteDecline:
		req, err := transcoder.PayloadToInviteDeclineRequest(command.PayloadBytes)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
		}
		return c.executeLobbyInviteDecline(ctx, command.UserID, req)
	default:
		return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command: unsupported message type %q", command.MessageType)
	}
}

func (c *RESTClient) executeLobbyMyGames(ctx context.Context, userID string) (downstream.UnaryResult, error) {
	body, status, err := c.do(ctx, http.MethodGet, c.baseURL+"/api/v1/user/lobby/my/games", userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute lobby.my.games.list: %w", err)
	}
	if status == http.StatusOK {
		var response lobbymodel.MyGamesListResponse
		if err := decodeStrictJSON(body, &response); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("decode success response: %w", err)
		}
		payloadBytes, err := transcoder.MyGamesListResponseToPayload(&response)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   lobbyResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	}
	return projectLobbyErrorResponse(status, body)
}

func (c *RESTClient) executeLobbyPublicGames(ctx context.Context, userID string, req *lobbymodel.PublicGamesListRequest) (downstream.UnaryResult, error) {
	page := req.Page
	if page <= 0 {
		page = 1
	}
	pageSize := req.PageSize
	if pageSize <= 0 {
		pageSize = 50
	}
	target := fmt.Sprintf("%s/api/v1/user/lobby/games?page=%d&page_size=%d", c.baseURL, page, pageSize)
	body, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute lobby.public.games.list: %w", err)
	}
	if status == http.StatusOK {
		page, err := decodePublicGamesPage(body)
		if err != nil {
			return downstream.UnaryResult{}, err
		}
		payloadBytes, err := transcoder.PublicGamesListResponseToPayload(page)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   lobbyResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	}
	return projectLobbyErrorResponse(status, body)
}

func (c *RESTClient) executeLobbyMyApplications(ctx context.Context, userID string) (downstream.UnaryResult, error) {
	body, status, err := c.do(ctx, http.MethodGet, c.baseURL+"/api/v1/user/lobby/my/applications", userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute lobby.my.applications.list: %w", err)
	}
	if status == http.StatusOK {
		response, err := decodeApplicationsList(body)
		if err != nil {
			return downstream.UnaryResult{}, err
		}
		payloadBytes, err := transcoder.MyApplicationsListResponseToPayload(response)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   lobbyResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	}
	return projectLobbyErrorResponse(status, body)
}

func (c *RESTClient) executeLobbyMyInvites(ctx context.Context, userID string) (downstream.UnaryResult, error) {
	body, status, err := c.do(ctx, http.MethodGet, c.baseURL+"/api/v1/user/lobby/my/invites", userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute lobby.my.invites.list: %w", err)
	}
	if status == http.StatusOK {
		response, err := decodeInvitesList(body)
		if err != nil {
			return downstream.UnaryResult{}, err
		}
		payloadBytes, err := transcoder.MyInvitesListResponseToPayload(response)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   lobbyResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	}
	return projectLobbyErrorResponse(status, body)
}

func (c *RESTClient) executeLobbyOpenEnrollment(ctx context.Context, userID string, req *lobbymodel.OpenEnrollmentRequest) (downstream.UnaryResult, error) {
	if req == nil || strings.TrimSpace(req.GameID) == "" {
		return downstream.UnaryResult{}, errors.New("execute lobby.game.open-enrollment: game_id must not be empty")
	}
	target := c.baseURL + "/api/v1/user/lobby/games/" + url.PathEscape(req.GameID) + "/open-enrollment"
	body, status, err := c.do(ctx, http.MethodPost, target, userID, struct{}{})
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute lobby.game.open-enrollment: %w", err)
	}
	if status == http.StatusOK {
		// Backend returns the full LobbyGameDetail; gateway projects the
		// minimal {game_id, status} pair onto the existing wire shape.
		var detail struct {
			GameID string `json:"game_id"`
			Status string `json:"status"`
		}
		if err := json.NewDecoder(bytes.NewReader(body)).Decode(&detail); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("decode success response: %w", err)
		}
		payloadBytes, err := transcoder.OpenEnrollmentResponseToPayload(&lobbymodel.OpenEnrollmentResponse{
			GameID: detail.GameID,
			Status: detail.Status,
		})
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   lobbyResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	}
	return projectLobbyErrorResponse(status, body)
}

func (c *RESTClient) executeLobbyGameCreate(ctx context.Context, userID string, req *lobbymodel.GameCreateRequest) (downstream.UnaryResult, error) {
	if req == nil || strings.TrimSpace(req.GameName) == "" {
		return downstream.UnaryResult{}, errors.New("execute lobby.game.create: game_name must not be empty")
	}
	if strings.TrimSpace(req.TurnSchedule) == "" {
		return downstream.UnaryResult{}, errors.New("execute lobby.game.create: turn_schedule must not be empty")
	}
	if strings.TrimSpace(req.TargetEngineVersion) == "" {
		return downstream.UnaryResult{}, errors.New("execute lobby.game.create: target_engine_version must not be empty")
	}
	if req.MinPlayers <= 0 || req.MaxPlayers <= 0 {
		return downstream.UnaryResult{}, errors.New("execute lobby.game.create: min_players and max_players must be positive")
	}
	if req.MinPlayers > req.MaxPlayers {
		return downstream.UnaryResult{}, errors.New("execute lobby.game.create: min_players must not exceed max_players")
	}
	if req.EnrollmentEndsAt.IsZero() {
		return downstream.UnaryResult{}, errors.New("execute lobby.game.create: enrollment_ends_at must be set")
	}

	body := map[string]any{
		"game_name":             req.GameName,
		"visibility":            "private",
		"description":           req.Description,
		"min_players":           int32(req.MinPlayers),
		"max_players":           int32(req.MaxPlayers),
		"start_gap_hours":       int32(req.StartGapHours),
		"start_gap_players":     int32(req.StartGapPlayers),
		"enrollment_ends_at":    req.EnrollmentEndsAt.UTC().Format(time.RFC3339Nano),
		"turn_schedule":         req.TurnSchedule,
		"target_engine_version": req.TargetEngineVersion,
	}
	payload, status, err := c.do(ctx, http.MethodPost, c.baseURL+"/api/v1/user/lobby/games", userID, body)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute lobby.game.create: %w", err)
	}
	if status == http.StatusOK || status == http.StatusCreated {
		summary, err := decodeGameSummaryFromGameDetail(payload)
		if err != nil {
			return downstream.UnaryResult{}, err
		}
		payloadBytes, err := transcoder.GameCreateResponseToPayload(&lobbymodel.GameCreateResponse{Game: summary})
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   lobbyResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	}
	return projectLobbyErrorResponse(status, payload)
}

func (c *RESTClient) executeLobbyApplicationSubmit(ctx context.Context, userID string, req *lobbymodel.ApplicationSubmitRequest) (downstream.UnaryResult, error) {
	if req == nil || strings.TrimSpace(req.GameID) == "" {
		return downstream.UnaryResult{}, errors.New("execute lobby.application.submit: game_id must not be empty")
	}
	if strings.TrimSpace(req.RaceName) == "" {
		return downstream.UnaryResult{}, errors.New("execute lobby.application.submit: race_name must not be empty")
	}
	target := c.baseURL + "/api/v1/user/lobby/games/" + url.PathEscape(req.GameID) + "/applications"
	body := map[string]any{"race_name": req.RaceName}
	payload, status, err := c.do(ctx, http.MethodPost, target, userID, body)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute lobby.application.submit: %w", err)
	}
	if status == http.StatusOK || status == http.StatusCreated {
		app, err := decodeApplicationDetail(payload)
		if err != nil {
			return downstream.UnaryResult{}, err
		}
		payloadBytes, err := transcoder.ApplicationSubmitResponseToPayload(&lobbymodel.ApplicationSubmitResponse{Application: app})
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   lobbyResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	}
	return projectLobbyErrorResponse(status, payload)
}

func (c *RESTClient) executeLobbyInviteRedeem(ctx context.Context, userID string, req *lobbymodel.InviteRedeemRequest) (downstream.UnaryResult, error) {
	if req == nil || strings.TrimSpace(req.GameID) == "" || strings.TrimSpace(req.InviteID) == "" {
		return downstream.UnaryResult{}, errors.New("execute lobby.invite.redeem: game_id and invite_id must not be empty")
	}
	target := c.baseURL + "/api/v1/user/lobby/games/" + url.PathEscape(req.GameID) + "/invites/" + url.PathEscape(req.InviteID) + "/redeem"
	payload, status, err := c.do(ctx, http.MethodPost, target, userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute lobby.invite.redeem: %w", err)
	}
	if status == http.StatusOK {
		invite, err := decodeInviteDetail(payload)
		if err != nil {
			return downstream.UnaryResult{}, err
		}
		payloadBytes, err := transcoder.InviteRedeemResponseToPayload(&lobbymodel.InviteRedeemResponse{Invite: invite})
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   lobbyResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	}
	return projectLobbyErrorResponse(status, payload)
}

func (c *RESTClient) executeLobbyInviteDecline(ctx context.Context, userID string, req *lobbymodel.InviteDeclineRequest) (downstream.UnaryResult, error) {
	if req == nil || strings.TrimSpace(req.GameID) == "" || strings.TrimSpace(req.InviteID) == "" {
		return downstream.UnaryResult{}, errors.New("execute lobby.invite.decline: game_id and invite_id must not be empty")
	}
	target := c.baseURL + "/api/v1/user/lobby/games/" + url.PathEscape(req.GameID) + "/invites/" + url.PathEscape(req.InviteID) + "/decline"
	payload, status, err := c.do(ctx, http.MethodPost, target, userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute lobby.invite.decline: %w", err)
	}
	if status == http.StatusOK {
		invite, err := decodeInviteDetail(payload)
		if err != nil {
			return downstream.UnaryResult{}, err
		}
		payloadBytes, err := transcoder.InviteDeclineResponseToPayload(&lobbymodel.InviteDeclineResponse{Invite: invite})
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   lobbyResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	}
	return projectLobbyErrorResponse(status, payload)
}

// decodeGameSummaryFromGameDetail accepts the backend's full
// LobbyGameDetail wire shape and projects it onto the gateway's
// GameSummary contract. It uses non-strict JSON decoding so the
// gateway tolerates the runtime/engine fields it does not forward to
// the UI.
func decodeGameSummaryFromGameDetail(payload []byte) (lobbymodel.GameSummary, error) {
	var wire struct {
		GameID           string    `json:"game_id"`
		GameName         string    `json:"game_name"`
		GameType         string    `json:"game_type"`
		Status           string    `json:"status"`
		OwnerUserID      *string   `json:"owner_user_id"`
		MinPlayers       int       `json:"min_players"`
		MaxPlayers       int       `json:"max_players"`
		EnrollmentEndsAt time.Time `json:"enrollment_ends_at"`
		CreatedAt        time.Time `json:"created_at"`
		UpdatedAt        time.Time `json:"updated_at"`
		CurrentTurn      int32     `json:"current_turn"`
	}
	if err := json.Unmarshal(payload, &wire); err != nil {
		return lobbymodel.GameSummary{}, fmt.Errorf("decode success response: %w", err)
	}
	owner := ""
	if wire.OwnerUserID != nil {
		owner = *wire.OwnerUserID
	}
	return lobbymodel.GameSummary{
		GameID:           wire.GameID,
		GameName:         wire.GameName,
		GameType:         wire.GameType,
		Status:           wire.Status,
		OwnerUserID:      owner,
		MinPlayers:       wire.MinPlayers,
		MaxPlayers:       wire.MaxPlayers,
		EnrollmentEndsAt: wire.EnrollmentEndsAt.UTC(),
		CreatedAt:        wire.CreatedAt.UTC(),
		UpdatedAt:        wire.UpdatedAt.UTC(),
		CurrentTurn:      wire.CurrentTurn,
	}, nil
}

func decodePublicGamesPage(payload []byte) (*lobbymodel.PublicGamesListResponse, error) {
	var wire struct {
		Items []struct {
			GameID           string    `json:"game_id"`
			GameName         string    `json:"game_name"`
			GameType         string    `json:"game_type"`
			Status           string    `json:"status"`
			OwnerUserID      *string   `json:"owner_user_id"`
			MinPlayers       int       `json:"min_players"`
			MaxPlayers       int       `json:"max_players"`
			EnrollmentEndsAt time.Time `json:"enrollment_ends_at"`
			CreatedAt        time.Time `json:"created_at"`
			UpdatedAt        time.Time `json:"updated_at"`
			CurrentTurn      int32     `json:"current_turn"`
		} `json:"items"`
		Page     int `json:"page"`
		PageSize int `json:"page_size"`
		Total    int `json:"total"`
	}
	if err := json.Unmarshal(payload, &wire); err != nil {
		return nil, fmt.Errorf("decode success response: %w", err)
	}
	out := &lobbymodel.PublicGamesListResponse{
		Items:    make([]lobbymodel.GameSummary, 0, len(wire.Items)),
		Page:     wire.Page,
		PageSize: wire.PageSize,
		Total:    wire.Total,
	}
	for _, w := range wire.Items {
		owner := ""
		if w.OwnerUserID != nil {
			owner = *w.OwnerUserID
		}
		out.Items = append(out.Items, lobbymodel.GameSummary{
			GameID:           w.GameID,
			GameName:         w.GameName,
			GameType:         w.GameType,
			Status:           w.Status,
			OwnerUserID:      owner,
			MinPlayers:       w.MinPlayers,
			MaxPlayers:       w.MaxPlayers,
			EnrollmentEndsAt: w.EnrollmentEndsAt.UTC(),
			CreatedAt:        w.CreatedAt.UTC(),
			UpdatedAt:        w.UpdatedAt.UTC(),
			CurrentTurn:      w.CurrentTurn,
		})
	}
	return out, nil
}

func decodeApplicationsList(payload []byte) (*lobbymodel.MyApplicationsListResponse, error) {
	var wire struct {
		Items []applicationDetailWire `json:"items"`
	}
	if err := json.Unmarshal(payload, &wire); err != nil {
		return nil, fmt.Errorf("decode success response: %w", err)
	}
	out := &lobbymodel.MyApplicationsListResponse{
		Items: make([]lobbymodel.ApplicationSummary, 0, len(wire.Items)),
	}
	for _, w := range wire.Items {
		out.Items = append(out.Items, w.toModel())
	}
	return out, nil
}

func decodeApplicationDetail(payload []byte) (lobbymodel.ApplicationSummary, error) {
	var wire applicationDetailWire
	if err := json.Unmarshal(payload, &wire); err != nil {
		return lobbymodel.ApplicationSummary{}, fmt.Errorf("decode success response: %w", err)
	}
	return wire.toModel(), nil
}

func decodeInvitesList(payload []byte) (*lobbymodel.MyInvitesListResponse, error) {
	var wire struct {
		Items []inviteDetailWire `json:"items"`
	}
	if err := json.Unmarshal(payload, &wire); err != nil {
		return nil, fmt.Errorf("decode success response: %w", err)
	}
	out := &lobbymodel.MyInvitesListResponse{
		Items: make([]lobbymodel.InviteSummary, 0, len(wire.Items)),
	}
	for _, w := range wire.Items {
		out.Items = append(out.Items, w.toModel())
	}
	return out, nil
}

func decodeInviteDetail(payload []byte) (lobbymodel.InviteSummary, error) {
	var wire inviteDetailWire
	if err := json.Unmarshal(payload, &wire); err != nil {
		return lobbymodel.InviteSummary{}, fmt.Errorf("decode success response: %w", err)
	}
	return wire.toModel(), nil
}

type applicationDetailWire struct {
	ApplicationID   string     `json:"application_id"`
	GameID          string     `json:"game_id"`
	ApplicantUserID string     `json:"applicant_user_id"`
	RaceName        string     `json:"race_name"`
	Status          string     `json:"status"`
	CreatedAt       time.Time  `json:"created_at"`
	DecidedAt       *time.Time `json:"decided_at,omitempty"`
}

func (w applicationDetailWire) toModel() lobbymodel.ApplicationSummary {
	out := lobbymodel.ApplicationSummary{
		ApplicationID:   w.ApplicationID,
		GameID:          w.GameID,
		ApplicantUserID: w.ApplicantUserID,
		RaceName:        w.RaceName,
		Status:          w.Status,
		CreatedAt:       w.CreatedAt.UTC(),
	}
	if w.DecidedAt != nil {
		t := w.DecidedAt.UTC()
		out.DecidedAt = &t
	}
	return out
}

type inviteDetailWire struct {
	InviteID      string     `json:"invite_id"`
	GameID        string     `json:"game_id"`
	InviterUserID string     `json:"inviter_user_id"`
	InvitedUserID *string    `json:"invited_user_id,omitempty"`
	Code          *string    `json:"code,omitempty"`
	RaceName      string     `json:"race_name"`
	Status        string     `json:"status"`
	CreatedAt     time.Time  `json:"created_at"`
	ExpiresAt     time.Time  `json:"expires_at"`
	DecidedAt     *time.Time `json:"decided_at,omitempty"`
}

func (w inviteDetailWire) toModel() lobbymodel.InviteSummary {
	out := lobbymodel.InviteSummary{
		InviteID:      w.InviteID,
		GameID:        w.GameID,
		InviterUserID: w.InviterUserID,
		RaceName:      w.RaceName,
		Status:        w.Status,
		CreatedAt:     w.CreatedAt.UTC(),
		ExpiresAt:     w.ExpiresAt.UTC(),
	}
	if w.InvitedUserID != nil {
		out.InvitedUserID = *w.InvitedUserID
	}
	if w.Code != nil {
		out.Code = *w.Code
	}
	if w.DecidedAt != nil {
		t := w.DecidedAt.UTC()
		out.DecidedAt = &t
	}
	return out
}

func projectLobbyErrorResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
	switch {
	case statusCode == http.StatusServiceUnavailable:
		return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
	case statusCode >= 400 && statusCode <= 599:
		errResp, err := decodeLobbyError(statusCode, payload)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("decode error response: %w", err)
		}
		payloadBytes, err := transcoder.LobbyErrorResponseToPayload(errResp)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode error response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   errResp.Error.Code,
			PayloadBytes: payloadBytes,
		}, nil
	default:
		return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
	}
}

func decodeLobbyError(statusCode int, payload []byte) (*lobbymodel.ErrorResponse, error) {
	var response lobbymodel.ErrorResponse
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&response); err != nil {
		return nil, err
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		if err == nil {
			return nil, errors.New("unexpected trailing JSON input")
		}
		return nil, err
	}
	response.Error.Code = normalizeLobbyErrorCode(statusCode, response.Error.Code)
	response.Error.Message = normalizeLobbyErrorMessage(response.Error.Code, response.Error.Message)
	if strings.TrimSpace(response.Error.Code) == "" {
		return nil, errors.New("missing error code")
	}
	if strings.TrimSpace(response.Error.Message) == "" {
		return nil, errors.New("missing error message")
	}
	return &response, nil
}

func normalizeLobbyErrorCode(statusCode int, code string) string {
	if trimmed := strings.TrimSpace(code); trimmed != "" {
		return trimmed
	}
	switch statusCode {
	case http.StatusBadRequest:
		return defaultLobbyErrorCodeInvalid
	case http.StatusForbidden:
		return defaultLobbyErrorCodeForbid
	case http.StatusNotFound:
		return defaultLobbyErrorCodeNoSubj
	case http.StatusConflict:
		return defaultLobbyErrorCodeConfl
	default:
		return defaultLobbyErrorCodeIntErr
	}
}

func normalizeLobbyErrorMessage(code, message string) string {
	if trimmed := strings.TrimSpace(message); trimmed != "" {
		return trimmed
	}
	if stable, ok := stableLobbyErrorMessages[code]; ok {
		return stable
	}
	return stableLobbyErrorMessages[defaultLobbyErrorCodeIntErr]
}
