package server

import (
	"errors"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"go.uber.org/zap"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/notify"
	"scrabble/backend/internal/session"
)

// The /api/v1/internal/sessions/* endpoints are gateway-only: the gateway has
// already validated the originating credential (Telegram initData, an email
// code, or a guest bootstrap) and forwards the result here to provision the
// account and mint the opaque session. The backend trusts the gateway on this
// segment (docs/ARCHITECTURE.md §12).

// telegramAuthRequest carries the identity the connector extracted from a
// validated initData payload. Username, FirstName and LanguageCode seed a
// brand-new account's display name and language; BrowserTZ (the client's detected
// "±HH:MM" UTC offset) seeds its time zone; StartParam is the validated launch
// deep-link payload, which may widen the new account's variant preferences (first
// contact only) — LanguageCode decides whether English Scrabble is part of that.
// Subtype is the client-reported device family (ios/android/web); Telegram's initData
// does not sign it, so it is recorded best-effort and the payments gate never relies
// on it.
type telegramAuthRequest struct {
	ExternalID   string `json:"external_id"`
	Username     string `json:"username"`
	FirstName    string `json:"first_name"`
	LanguageCode string `json:"language_code"`
	BrowserTZ    string `json:"browser_tz"`
	StartParam   string `json:"start_param"`
	Subtype      string `json:"subtype"`
}

// handleTelegramAuth provisions (or finds) the account bound to a Telegram
// identity and mints a session for it, seeding a new account's display name and
// language from the supplied Telegram fields (first contact only).
func (s *Server) handleTelegramAuth(c *gin.Context) {
	var req telegramAuthRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.ExternalID == "" {
		abortBadRequest(c, "external_id is required")
		return
	}
	acc, created, err := s.accounts.ProvisionTelegram(c.Request.Context(), req.ExternalID, req.LanguageCode, req.Username, req.FirstName, req.BrowserTZ)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	if created {
		// First registration: re-evaluate moderated-chat write access, so a user who
		// joined the chat before registering is granted on the spot (no chat_member
		// event fires on registration).
		s.publishChatAccessChange(acc.ID)
		// A promo deep-link may widen this brand-new account's variant preferences beyond
		// the default (English Scrabble for a non-Russian-speaking arrival — see
		// account.MergeVariantSeed, which gates that variant on req.LanguageCode).
		// Best-effort: an absent or malformed payload leaves the account on its defaults,
		// and a write failure must not block the session mint.
		seed := account.SeedVariantsFromStartParam(req.StartParam)
		if merged := account.MergeVariantSeed(acc.VariantPreferences, seed, req.LanguageCode); len(merged) > 0 {
			if _, err := s.accounts.SetVariantPreferences(c.Request.Context(), acc.ID, merged); err != nil {
				s.log.Warn("telegram: seed variant preferences failed",
					zap.String("account", acc.ID.String()), zap.Error(err))
			}
		}
	}
	s.mintSession(c, acc, session.Platform{Kind: session.PlatformKindTelegram, Subtype: session.NormalizeSubtype(req.Subtype)})
}

// vkAuthRequest carries the identity the gateway extracted from verified VK launch
// params. LanguageCode (vk_language) and DisplayName (read client-side via
// VKWebAppGetUserInfo, since VK omits the name from the signed params) seed a brand-new
// account's language and display name; BrowserTZ (the client's detected "±HH:MM" UTC
// offset) seeds its time zone. All seeds apply on first contact only. Subtype is the
// device family the gateway derived from the signed vk_platform param — trusted,
// since it rides inside the verified launch signature.
type vkAuthRequest struct {
	ExternalID   string `json:"external_id"`
	LanguageCode string `json:"language_code"`
	DisplayName  string `json:"display_name"`
	BrowserTZ    string `json:"browser_tz"`
	Subtype      string `json:"subtype"`
}

// handleVKAuth provisions (or finds) the account bound to a VK identity and mints a
// session for it, seeding a new account's language and display name from the supplied VK
// fields (first contact only). Unlike Telegram there is no moderated-chat re-evaluation
// or deep-link variant seed: a fresh VK account has no Telegram chat eligibility and the
// MVP carries no launch deep link.
func (s *Server) handleVKAuth(c *gin.Context) {
	var req vkAuthRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.ExternalID == "" {
		abortBadRequest(c, "external_id is required")
		return
	}
	acc, _, err := s.accounts.ProvisionVK(c.Request.Context(), req.ExternalID, req.LanguageCode, req.DisplayName, req.BrowserTZ)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	s.mintSession(c, acc, session.Platform{Kind: session.PlatformKindVK, Subtype: session.NormalizeSubtype(req.Subtype)})
}

// pushTargetRequest asks for a user's out-of-app push routing data by account id.
type pushTargetRequest struct {
	UserID string `json:"user_id"`
}

// pushTargetResponse carries what the gateway needs to route an out-of-app push:
// the recipient's Telegram external_id (empty when they have no Telegram
// identity, e.g. a guest or email-only account), the language the single bot renders
// the message in (the account's interface language), and whether they confined
// notifications to the in-app stream.
type pushTargetResponse struct {
	ExternalID             string `json:"external_id"`
	Language               string `json:"language"`
	NotificationsInAppOnly bool   `json:"notifications_in_app_only"`
}

// handlePushTarget resolves a user id to the data the gateway needs to deliver an
// out-of-app Telegram notification — the gateway-only internal counterpart of the
// in-app push stream. A user with no Telegram identity yields an empty external_id,
// which the gateway treats as "no out-of-app channel".
func (s *Server) handlePushTarget(c *gin.Context) {
	var req pushTargetRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.UserID == "" {
		abortBadRequest(c, "user_id is required")
		return
	}
	uid, err := uuid.Parse(req.UserID)
	if err != nil {
		abortBadRequest(c, "user_id must be a uuid")
		return
	}
	acc, err := s.accounts.GetByID(c.Request.Context(), uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	ext, err := s.accounts.IdentityExternalID(c.Request.Context(), uid, account.KindTelegram)
	if err != nil && !errors.Is(err, account.ErrNotFound) {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, pushTargetResponse{
		ExternalID:             ext,
		Language:               acc.PreferredLanguage,
		NotificationsInAppOnly: acc.NotificationsInAppOnly,
	})
}

// guestAuthRequest carries the guest bootstrap's optional time-zone seed: BrowserTZ
// (the client's detected "±HH:MM" UTC offset) is written to the new guest account's
// time zone, so robot timing is anchored to the player's zone from the first game.
type guestAuthRequest struct {
	BrowserTZ string `json:"browser_tz"`
	// Subtype is the client-reported device family (ios/android/web) of this direct
	// (web/native) session; there is no external signer, so it is recorded best-effort.
	Subtype string `json:"subtype"`
	// Language is the client's detected interface locale, seeding the fresh guest's
	// interface language (best-effort; an unsupported/absent value keeps the 'en' default).
	Language string `json:"language"`
}

// handleGuestAuth provisions a fresh ephemeral guest account and mints a session,
// seeding its time zone from the optional detected browser offset and its interface
// language from the optional detected locale.
func (s *Server) handleGuestAuth(c *gin.Context) {
	// The body is optional: an absent or malformed one simply yields no time-zone/language seed
	// (the account keeps the UTC / 'en' defaults), so a bind error must not fail the bootstrap.
	var req guestAuthRequest
	_ = c.ShouldBindJSON(&req)
	acc, err := s.accounts.ProvisionGuest(c.Request.Context(), req.BrowserTZ, req.Language)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	s.mintSession(c, acc, session.Platform{Kind: session.PlatformKindDirect, Subtype: session.NormalizeSubtype(req.Subtype)})
}

// emailRequest is an email-login code request. BrowserTZ (the client's detected
// "±HH:MM" UTC offset) seeds the time zone of an account provisioned here on first
// contact (the email account is created at the request step, not at login).
type emailRequest struct {
	Email     string `json:"email"`
	BrowserTZ string `json:"browser_tz"`
	Language  string `json:"language"`
	// Pwa marks a request from an installed PWA (standalone display mode): the login email then
	// omits the one-tap confirm link so the code is typed in the same window (the link would open
	// in a separate browser whose session cannot reach the PWA). See EmailService.RequestLoginCode.
	Pwa bool `json:"pwa"`
}

// handleEmailRequest issues a login confirm-code to the email. It always reports
// success once the address is well-formed, so the response does not reveal
// whether an account already exists.
func (s *Server) handleEmailRequest(c *gin.Context) {
	var req emailRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.Email == "" {
		abortBadRequest(c, "email is required")
		return
	}
	if _, err := s.emails.RequestLoginCode(c.Request.Context(), req.Email, req.BrowserTZ, req.Language, req.Pwa); err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, okResponse{OK: true})
}

// emailLoginRequest verifies an email login code. Subtype is the client-reported
// device family (ios/android/web) of this direct session; recorded best-effort.
type emailLoginRequest struct {
	Email   string `json:"email"`
	Code    string `json:"code"`
	Subtype string `json:"subtype"`
}

// handleEmailLogin verifies the code and mints a session for the owning account.
func (s *Server) handleEmailLogin(c *gin.Context) {
	var req emailLoginRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.Email == "" || req.Code == "" {
		abortBadRequest(c, "email and code are required")
		return
	}
	acc, err := s.emails.LoginWithCode(c.Request.Context(), req.Email, req.Code)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	s.mintSession(c, acc, session.Platform{Kind: session.PlatformKindDirect, Subtype: session.NormalizeSubtype(req.Subtype)})
}

// confirmLinkResponse is the outcome of a one-tap deeplink confirmation. For a login,
// Session carries the freshly minted credential (the deeplink page signs in with it);
// for a link, Status is "confirmed" or "merge_required" (the app completes the merge).
type confirmLinkResponse struct {
	Purpose string           `json:"purpose"`
	Status  string           `json:"status"`
	Session *sessionResponse `json:"session,omitempty"`
}

// handleEmailConfirmLink verifies a one-tap deeplink token. A login mints a session
// (the deeplink page signs in with it); a link attaches the confirmed email, reporting
// "merge_required" when the address is owned by another account so the app drives the
// interactive merge. The token, not a request session, is the authorization — the page
// need not be signed in.
func (s *Server) handleEmailConfirmLink(c *gin.Context) {
	var req tokenRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.Token == "" {
		abortBadRequest(c, "token is required")
		return
	}
	res, err := s.emails.ConfirmByToken(c.Request.Context(), req.Token)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	if res.IsLogin() {
		acc, err := s.accounts.GetByID(c.Request.Context(), res.Account)
		if err != nil {
			s.abortErr(c, err)
			return
		}
		// A magic-link login always opens in a browser, so its session is direct/web.
		token, _, err := s.sessions.Create(c.Request.Context(), acc.ID, session.Platform{Kind: session.PlatformKindDirect, Subtype: session.SubtypeWeb})
		if err != nil {
			s.abortErr(c, err)
			return
		}
		sess := sessionResponseFor(token, acc)
		c.JSON(http.StatusOK, confirmLinkResponse{Purpose: "login", Status: "confirmed", Session: &sess})
		return
	}
	if res.NeedsMerge {
		c.JSON(http.StatusOK, confirmLinkResponse{Purpose: "link", Status: "merge_required"})
		return
	}
	// A free link attached the email; nudge the account's in-app session(s) to re-fetch
	// the profile, so a link confirmed in another browser reflects at once.
	s.notifier.Publish(notify.ProfileChanged(res.Account))
	c.JSON(http.StatusOK, confirmLinkResponse{Purpose: "link", Status: "confirmed"})
}

// tokenRequest carries an opaque session token.
type tokenRequest struct {
	Token string `json:"token"`
}

// handleResolveSession resolves a token to its account id. The gateway calls it
// on a session-cache miss.
func (s *Server) handleResolveSession(c *gin.Context) {
	var req tokenRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.Token == "" {
		abortBadRequest(c, "token is required")
		return
	}
	sess, err := s.sessions.Resolve(c.Request.Context(), req.Token)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	// is_guest is best-effort: a transient account read must not fail an otherwise
	// valid resolve (the auth hot path), so a read error falls back to false; the
	// per-operation backend gate remains the authoritative guest check.
	resp := resolveResponse{
		UserID:          sess.AccountID.String(),
		PlatformKind:    sess.Platform.Kind,
		PlatformSubtype: sess.Platform.Subtype,
	}
	if acc, err := s.accounts.GetByID(c.Request.Context(), sess.AccountID); err == nil {
		resp.IsGuest = acc.IsGuest
	}
	c.JSON(http.StatusOK, resp)
}

// handleRevokeSession revokes the session for a token (idempotent).
func (s *Server) handleRevokeSession(c *gin.Context) {
	var req tokenRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.Token == "" {
		abortBadRequest(c, "token is required")
		return
	}
	if err := s.sessions.Revoke(c.Request.Context(), req.Token); err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, okResponse{OK: true})
}

// mintSession creates a session for acc carrying the captured platform and writes
// the credential response.
func (s *Server) mintSession(c *gin.Context, acc account.Account, platform session.Platform) {
	token, _, err := s.sessions.Create(c.Request.Context(), acc.ID, platform)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, sessionResponseFor(token, acc))
}
