package server

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

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

// The /api/v1/user/friends/* handlers wire the social friend graph: the
// befriend-an-opponent request flow, the one-time friend-code path, and the
// friends/incoming lists. They follow handlers_user.go: X-User-ID identity, a domain
// call, a JSON DTO. Account ids are projected to {id, display_name} refs resolved
// from the account store, mirroring fillSeatNames.

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

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

// incomingListDTO is the friend requests awaiting the caller's response.
type incomingListDTO struct {
	Requests []accountRefDTO `json:"requests"`
}

// outgoingListDTO is the addressees the caller has already requested (a live pending
// request or one the addressee declined) and therefore cannot re-request, plus the
// per-game disguised-robot requests (not real accounts), which keep the in-game
// "request sent" state pinned to a seat.
type outgoingListDTO struct {
	Requests []accountRefDTO         `json:"requests"`
	Robots   []robotFriendRequestDTO `json:"robots"`
}

// robotFriendRequestDTO 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 (so the in-game card can
// re-mark that seat as already requested).
type robotFriendRequestDTO struct {
	ID          string `json:"id"`
	DisplayName string `json:"display_name"`
	GameID      string `json:"game_id"`
	Seat        int    `json:"seat"`
}

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

// redeemResultDTO reports the new friend gained by redeeming a code.
type redeemResultDTO struct {
	Friend accountRefDTO `json:"friend"`
}

// targetIDRequest carries a single counterpart account id. GameID is set only by an
// in-game block (so a disguised-robot opponent can be recorded as a per-game block); the
// other target paths leave it empty.
type targetIDRequest struct {
	AccountID string `json:"account_id"`
	GameID    string `json:"game_id"`
}

// friendRespondRequest accepts or declines a pending request from a requester.
type friendRespondRequest struct {
	RequesterID string `json:"requester_id"`
	Accept      bool   `json:"accept"`
}

// redeemCodeRequest carries a friend code to redeem.
type redeemCodeRequest struct {
	Code string `json:"code"`
}

// namedRef resolves a single account id into its display-name ref, caching the
// lookup in memo so a caller can share it across many refs in one response.
func (s *Server) namedRef(ctx context.Context, id uuid.UUID, memo map[string]string) accountRefDTO {
	key := id.String()
	name, ok := memo[key]
	if !ok {
		if acc, err := s.accounts.GetByID(ctx, id); err == nil {
			name = acc.DisplayName
		}
		memo[key] = name
	}
	return accountRefDTO{AccountID: key, DisplayName: name}
}

// accountRefs resolves a list of account ids into display-name refs, memoising
// lookups within the call.
func (s *Server) accountRefs(ctx context.Context, ids []uuid.UUID) []accountRefDTO {
	memo := map[string]string{}
	out := make([]accountRefDTO, 0, len(ids))
	for _, id := range ids {
		out = append(out, s.namedRef(ctx, id, memo))
	}
	return out
}

// accountRef resolves a single account id into its display-name ref.
func (s *Server) accountRef(ctx context.Context, id uuid.UUID) accountRefDTO {
	return s.namedRef(ctx, id, map[string]string{})
}

// parseUUIDField parses a body-supplied account id, trimming whitespace.
func parseUUIDField(s string) (uuid.UUID, bool) {
	id, err := uuid.Parse(strings.TrimSpace(s))
	if err != nil {
		return uuid.UUID{}, false
	}
	return id, true
}

// handleFriendRequest sends a friend request to an opponent the caller has played.
func (s *Server) handleFriendRequest(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req targetIDRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	target, ok := parseUUIDField(req.AccountID)
	if !ok {
		abortBadRequest(c, "invalid account id")
		return
	}
	// An in-game request carries the game id, so a disguised-robot opponent is recorded as a
	// per-game request (RequestInGame); a request without a game (or to a human) goes through
	// SendFriendRequest directly.
	var err error
	if strings.TrimSpace(req.GameID) == "" {
		err = s.social.SendFriendRequest(c.Request.Context(), uid, target)
	} else if gameID, ok := parseUUIDField(req.GameID); !ok {
		abortBadRequest(c, "invalid game id")
		return
	} else {
		err = s.social.RequestInGame(c.Request.Context(), uid, target, gameID)
	}
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, okResponse{OK: true})
}

// handleFriendRespond accepts or declines a pending incoming request.
func (s *Server) handleFriendRespond(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req friendRespondRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	requester, ok := parseUUIDField(req.RequesterID)
	if !ok {
		abortBadRequest(c, "invalid requester id")
		return
	}
	if err := s.social.RespondFriendRequest(c.Request.Context(), uid, requester, req.Accept); err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, okResponse{OK: true})
}

// handleFriendCancel withdraws the caller's own pending request.
func (s *Server) handleFriendCancel(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req targetIDRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	target, ok := parseUUIDField(req.AccountID)
	if !ok {
		abortBadRequest(c, "invalid account id")
		return
	}
	if err := s.social.CancelFriendRequest(c.Request.Context(), uid, target); err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, okResponse{OK: true})
}

// handleUnfriend removes a friendship with the :id account.
func (s *Server) handleUnfriend(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	other, err := uuid.Parse(c.Param("id"))
	if err != nil {
		abortBadRequest(c, "invalid account id")
		return
	}
	if err := s.social.Unfriend(c.Request.Context(), uid, other); err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, okResponse{OK: true})
}

// handleListFriends returns the caller's accepted friends.
func (s *Server) handleListFriends(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	ids, err := s.social.ListFriends(c.Request.Context(), uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, friendListDTO{Friends: s.accountRefs(c.Request.Context(), ids)})
}

// handleIncomingRequests returns the friend requests awaiting the caller.
func (s *Server) handleIncomingRequests(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	ids, err := s.social.ListIncomingRequests(c.Request.Context(), uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, incomingListDTO{Requests: s.accountRefs(c.Request.Context(), ids)})
}

// handleOutgoingRequests returns the addressees the caller has already requested
// (pending or declined) and cannot re-request.
func (s *Server) handleOutgoingRequests(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	ctx := c.Request.Context()
	ids, err := s.social.ListOutgoingRequests(ctx, uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	robots, err := s.social.ListRobotFriendRequests(ctx, uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	dto := outgoingListDTO{Requests: s.accountRefs(ctx, ids)}
	for _, r := range robots {
		dto.Robots = append(dto.Robots, robotFriendRequestDTO{ID: r.ID.String(), DisplayName: r.DisplayName, GameID: r.GameID.String(), Seat: r.Seat})
	}
	c.JSON(http.StatusOK, dto)
}

// handleIssueFriendCode issues a one-time add-a-friend code for the caller.
func (s *Server) handleIssueFriendCode(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	code, err := s.social.IssueFriendCode(c.Request.Context(), uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, friendCodeDTO{Code: code.Code, ExpiresAtUnix: code.ExpiresAt.Unix()})
}

// handleRedeemFriendCode redeems a friend code, befriending its issuer.
func (s *Server) handleRedeemFriendCode(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req redeemCodeRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	issuer, err := s.social.RedeemFriendCode(c.Request.Context(), uid, strings.TrimSpace(req.Code))
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, redeemResultDTO{Friend: s.accountRef(c.Request.Context(), issuer)})
}
