package server

import (
	"context"
	"net/http"

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

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/link"
)

// The /api/v1/user/link handlers drive account linking & merge. The
// request step always mails a code (no pre-send "taken" signal, so a probe cannot
// enumerate registered emails); confirm reveals a required merge only after the
// code is verified; merge performs the irreversible consolidation behind an
// explicit step. A merge into a guest initiator's durable counterpart switches the
// active session — the new token rides back in the result for the client to adopt.

// linkEmailRequestBody starts a link/merge by mailing a code to email.
type linkEmailRequestBody struct {
	Email string `json:"email"`
}

// linkEmailConfirmBody carries the email and its confirm code.
type linkEmailConfirmBody struct {
	Email string `json:"email"`
	Code  string `json:"code"`
}

// linkTelegramBody carries a gateway-validated Telegram identity.
type linkTelegramBody struct {
	ExternalID string `json:"external_id"`
}

// linkVKBody carries a gateway-validated VK identity (the trusted vk user id the
// gateway resolved from the VK ID code exchange).
type linkVKBody struct {
	ExternalID string `json:"external_id"`
}

// linkResultResponse is the unified result of a confirm or merge step. Status is
// "linked" (bound to the caller), "merge_required" (the identity belongs to another
// account — the secondary_* fields summarise it for the irreversible confirmation),
// or "merged" (done; token is non-empty when the active account switched).
type linkResultResponse struct {
	Status           string           `json:"status"`
	SecondaryUserID  string           `json:"secondary_user_id,omitempty"`
	SecondaryName    string           `json:"secondary_display_name,omitempty"`
	SecondaryGames   int              `json:"secondary_games"`
	SecondaryFriends int              `json:"secondary_friends"`
	Token            string           `json:"token,omitempty"`
	Profile          *profileResponse `json:"profile,omitempty"`
}

// handleLinkEmailRequest mails a confirm-code to email for a later link or merge.
func (s *Server) handleLinkEmailRequest(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req linkEmailRequestBody
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	if err := s.links.RequestEmail(c.Request.Context(), uid, req.Email); err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, okResponse{OK: true})
}

// unlinkBody carries the provider kind to detach.
type unlinkBody struct {
	Kind string `json:"kind"`
}

// handleUnlink detaches a platform identity (telegram or vk) from the caller's
// account, refusing to remove the last identity (ErrLastIdentity). Email is never
// unlinked — it is replaced through the change-email flow — so an email kind is
// rejected. It returns the refreshed profile so the client updates its controls.
func (s *Server) handleUnlink(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req unlinkBody
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	if req.Kind != account.KindTelegram && req.Kind != account.KindVK {
		abortBadRequest(c, "only telegram or vk can be unlinked")
		return
	}
	ctx := c.Request.Context()
	if err := s.accounts.RemoveIdentity(ctx, uid, req.Kind); err != nil {
		s.abortErr(c, err)
		return
	}
	acc, err := s.accounts.GetByID(ctx, uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	r := s.profileResponse(ctx, acc)
	c.JSON(http.StatusOK, linkResultResponse{Status: "unlinked", Profile: &r})
}

// handleChangeEmailRequest mails a confirm-code to a new address for an authenticated
// email change. Like the link request it never signals "taken" up front — a conflict is
// only revealed (non-disclosingly) at confirm.
func (s *Server) handleChangeEmailRequest(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req linkEmailRequestBody
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	if err := s.emails.RequestChangeCode(c.Request.Context(), uid, req.Email); err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, okResponse{OK: true})
}

// handleChangeEmailConfirm verifies the code and atomically switches the account to the
// new address, returning the refreshed profile. A new address confirmed by another
// account is refused (ErrEmailTaken → the non-disclosing message), never merged.
func (s *Server) handleChangeEmailConfirm(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req linkEmailConfirmBody
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	ctx := c.Request.Context()
	acc, err := s.emails.ConfirmChange(ctx, uid, req.Email, req.Code)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	r := s.profileResponse(ctx, acc)
	c.JSON(http.StatusOK, linkResultResponse{Status: "changed", Profile: &r})
}

// handleLinkEmailConfirm verifies the code and binds a free email or reports a
// required merge.
func (s *Server) handleLinkEmailConfirm(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req linkEmailConfirmBody
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	res, err := s.links.ConfirmEmail(c.Request.Context(), uid, req.Email, req.Code)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, s.confirmResultResponse(c, uid, res))
}

// handleLinkEmailMerge re-verifies the code and performs the merge.
func (s *Server) handleLinkEmailMerge(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req linkEmailConfirmBody
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	res, err := s.links.MergeEmail(c.Request.Context(), uid, req.Email, req.Code)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, s.mergeResultResponse(c, res))
}

// handleLinkTelegram attaches a gateway-validated Telegram identity to the caller
// or reports a required merge.
func (s *Server) handleLinkTelegram(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req linkTelegramBody
	if err := c.ShouldBindJSON(&req); err != nil || req.ExternalID == "" {
		abortBadRequest(c, "missing external_id")
		return
	}
	res, err := s.links.ConfirmTelegram(c.Request.Context(), uid, req.ExternalID)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, s.confirmResultResponse(c, uid, res))
}

// handleLinkTelegramMerge merges the account owning a gateway-validated Telegram
// identity into the caller's.
func (s *Server) handleLinkTelegramMerge(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req linkTelegramBody
	if err := c.ShouldBindJSON(&req); err != nil || req.ExternalID == "" {
		abortBadRequest(c, "missing external_id")
		return
	}
	res, err := s.links.MergeTelegram(c.Request.Context(), uid, req.ExternalID)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, s.mergeResultResponse(c, res))
}

// handleLinkVK attaches a gateway-validated VK identity to the caller or reports a
// required merge.
func (s *Server) handleLinkVK(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req linkVKBody
	if err := c.ShouldBindJSON(&req); err != nil || req.ExternalID == "" {
		abortBadRequest(c, "missing external_id")
		return
	}
	res, err := s.links.ConfirmVK(c.Request.Context(), uid, req.ExternalID)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, s.confirmResultResponse(c, uid, res))
}

// handleLinkVKMerge merges the account owning a gateway-validated VK identity into
// the caller's.
func (s *Server) handleLinkVKMerge(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req linkVKBody
	if err := c.ShouldBindJSON(&req); err != nil || req.ExternalID == "" {
		abortBadRequest(c, "missing external_id")
		return
	}
	res, err := s.links.MergeVK(c.Request.Context(), uid, req.ExternalID)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, s.mergeResultResponse(c, res))
}

// confirmResultResponse renders a confirm step: a merge preview (secondary summary)
// or a completed link (the active account's refreshed profile).
func (s *Server) confirmResultResponse(c *gin.Context, activeID uuid.UUID, res link.ConfirmResult) linkResultResponse {
	ctx := c.Request.Context()
	if res.Merged {
		// A guest initiator's merge ran inline (the guest-primary rule; no confirmation step),
		// so render the completed merge — the client adopts the switched session and lands on
		// the surviving durable account as if it simply logged in.
		return s.mergeResultResponse(c, res.Merge)
	}
	if res.MergeRequired {
		out := linkResultResponse{Status: "merge_required", SecondaryUserID: res.SecondaryID.String()}
		if acc, err := s.accounts.GetByID(ctx, res.SecondaryID); err == nil {
			out.SecondaryName = acc.DisplayName
		}
		out.SecondaryGames, out.SecondaryFriends = s.secondaryCounts(ctx, res.SecondaryID)
		return out
	}
	return linkResultResponse{Status: "linked", Profile: s.profileFor(ctx, activeID)}
}

// mergeResultResponse renders a completed merge: the surviving account's profile
// plus a switched-session token when the active account changed.
func (s *Server) mergeResultResponse(c *gin.Context, res link.MergeResult) linkResultResponse {
	return linkResultResponse{
		Status:  "merged",
		Token:   res.SwitchedToken,
		Profile: s.profileFor(c.Request.Context(), res.PrimaryID),
	}
}

// profileFor loads an account's profile DTO, or nil when it cannot be read.
func (s *Server) profileFor(ctx context.Context, id uuid.UUID) *profileResponse {
	acc, err := s.accounts.GetByID(ctx, id)
	if err != nil {
		return nil
	}
	p := s.profileResponse(ctx, acc)
	return &p
}

// secondaryCounts summarises the to-be-retired account for the merge confirmation.
func (s *Server) secondaryCounts(ctx context.Context, id uuid.UUID) (games, friends int) {
	if s.games != nil {
		if gs, err := s.games.ListForAccount(ctx, id); err == nil {
			games = len(gs)
		}
	}
	if s.social != nil {
		if fs, err := s.social.ListFriends(ctx, id); err == nil {
			friends = len(fs)
		}
	}
	return games, friends
}
