package server

import (
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"scrabble/backend/internal/account"
)

// The /api/v1/user account handlers wire profile editing, email binding and the
// statistics read. They follow handlers_user.go: X-User-ID identity, a
// domain call, a JSON DTO. Profile editing overwrites the full editable set, so the
// client sends the complete desired profile.

// updateProfileRequest is the full editable profile. away_start/away_end are
// "HH:MM" local-time bounds of the daily away window.
type updateProfileRequest struct {
	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"`
	BlockChat              bool     `json:"block_chat"`
	BlockFriendRequests    bool     `json:"block_friend_requests"`
	NotificationsInAppOnly bool     `json:"notifications_in_app_only"`
	VariantPreferences     []string `json:"variant_preferences"`
}

// statsDTO is a durable account's lifetime statistics (the derived games-played and
// win-rate are computed client-side). BestMoves breaks the best move down per variant,
// carrying the word itself; it is absent for an account with no recorded play and never
// lists a variant the account has not played.
type statsDTO 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     []bestMoveDTO `json:"best_moves,omitempty"`
}

// bestMoveDTO 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), so the
// client renders it as game tiles without the variant's alphabet table.
type bestMoveDTO struct {
	Variant string                 `json:"variant"`
	Score   int                    `json:"score"`
	Word    []account.BestMoveTile `json:"word"`
}

// parseAwayTime parses an "HH:MM" away-window bound.
func parseAwayTime(s string) (time.Time, bool) {
	t, err := time.Parse(awayTimeLayout, strings.TrimSpace(s))
	if err != nil {
		return time.Time{}, false
	}
	return t, true
}

// handleUpdateProfile overwrites the caller's editable profile fields.
func (s *Server) handleUpdateProfile(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	var req updateProfileRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		abortBadRequest(c, "invalid request body")
		return
	}
	awayStart, ok := parseAwayTime(req.AwayStart)
	if !ok {
		abortBadRequest(c, "away_start must be HH:MM")
		return
	}
	awayEnd, ok := parseAwayTime(req.AwayEnd)
	if !ok {
		abortBadRequest(c, "away_end must be HH:MM")
		return
	}
	acc, err := s.accounts.UpdateProfile(c.Request.Context(), uid, account.ProfileUpdate{
		DisplayName:            req.DisplayName,
		PreferredLanguage:      req.PreferredLanguage,
		TimeZone:               req.TimeZone,
		AwayStart:              awayStart,
		AwayEnd:                awayEnd,
		BlockChat:              req.BlockChat,
		BlockFriendRequests:    req.BlockFriendRequests,
		NotificationsInAppOnly: req.NotificationsInAppOnly,
		VariantPreferences:     req.VariantPreferences,
	})
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, s.profileResponse(c.Request.Context(), acc))
}

// blockStatusResponse reports the caller's current block to the client. Until is an RFC3339 UTC
// instant for a temporary block and empty for a permanent one (or when not blocked); Reason is
// the operator-set reason resolved to the account's language, empty when none was cited. It is
// the one payload a blocked client can still fetch, behind the suspension gate's exemption.
type blockStatusResponse struct {
	Blocked   bool   `json:"blocked"`
	Permanent bool   `json:"permanent"`
	Until     string `json:"until,omitempty"`
	Reason    string `json:"reason,omitempty"`
}

// handleBlockStatus reports whether the caller is blocked and, if so, the block's expiry and the
// reason resolved to the account's language. The suspension gate exempts this route so a blocked
// client can render the terminal blocked screen.
func (s *Server) handleBlockStatus(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	susp, blocked, err := s.accounts.CurrentSuspension(c.Request.Context(), uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	resp := blockStatusResponse{Blocked: blocked}
	if blocked {
		resp.Permanent = susp.Permanent()
		if susp.BlockedUntil != nil {
			resp.Until = susp.BlockedUntil.UTC().Format(time.RFC3339)
		}
		// Resolve the reason snapshot to the account's interface language; fall back to English
		// if the account row cannot be read for some reason.
		lang := "en"
		if acc, err := s.accounts.GetByID(c.Request.Context(), uid); err == nil {
			lang = acc.PreferredLanguage
		}
		resp.Reason = susp.LocalizedReason(lang)
	}
	c.JSON(http.StatusOK, resp)
}

// handleStats returns the caller's lifetime statistics.
func (s *Server) handleStats(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		abortBadRequest(c, "missing identity")
		return
	}
	st, err := s.accounts.GetStats(c.Request.Context(), uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	var best []bestMoveDTO
	if len(st.BestMoves) > 0 {
		best = make([]bestMoveDTO, len(st.BestMoves))
		for i, b := range st.BestMoves {
			best[i] = bestMoveDTO{Variant: b.Variant, Score: b.Score, Word: b.Tiles}
		}
	}
	c.JSON(http.StatusOK, statsDTO{
		Wins:          st.Wins,
		Losses:        st.Losses,
		Draws:         st.Draws,
		MaxGamePoints: st.MaxGamePoints,
		MaxWordPoints: st.MaxWordPoints,
		Moves:         st.Moves,
		HintsUsed:     st.HintsUsed,
		BestMoves:     best,
	})
}
