package server

import (
	"net/http"

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

	"scrabble/backend/internal/payments"
)

// walletSegmentDTO is one chip balance in the wallet view: the funding source, the chip count,
// and whether it can be spent in the current execution context (false for a frozen VK-iOS
// balance or an untrusted platform).
type walletSegmentDTO struct {
	Source    string `json:"source"`
	Chips     int    `json:"chips"`
	Spendable bool   `json:"spendable"`
}

// walletDTO is the user-facing wallet: the context-visible chip segments and the
// context-applicable benefits (the no-ads term or forever flag, and the available hints).
// RewardChips is the chips a rewarded-video view earns in the current context (0 when rewarded is
// unavailable here — outside VK, or unconfigured); the client shows the "watch for chips" button
// only when it is positive.
type walletDTO struct {
	Segments     []walletSegmentDTO `json:"segments"`
	AdsForever   bool               `json:"ads_forever"`
	AdsPaidUntil int64              `json:"ads_paid_until_ms"` // unix millis; 0 = no active term
	Hints        int                `json:"hints"`
	RewardChips  int                `json:"reward_chips"`
}

// walletBuyRequest is the POST body of a chip spend: the product to buy with chips.
type walletBuyRequest struct {
	ProductID string `json:"product_id"`
}

// walletDTOFrom projects a payments wallet view into the wire DTO.
func walletDTOFrom(v payments.WalletView) walletDTO {
	out := walletDTO{AdsForever: v.Benefits.AdsForever, Hints: v.Benefits.Hints}
	if v.Benefits.AdsPaidUntil != nil {
		out.AdsPaidUntil = v.Benefits.AdsPaidUntil.UnixMilli()
	}
	out.Segments = make([]walletSegmentDTO, 0, len(v.Segments))
	for _, seg := range v.Segments {
		out.Segments = append(out.Segments, walletSegmentDTO{Source: string(seg.Source), Chips: seg.Chips, Spendable: seg.Spendable})
	}
	return out
}

// catalogAtomDTO is one atom line of a storefront product: the base value type it grants and how
// many of it the product carries.
type catalogAtomDTO struct {
	AtomType string `json:"atom_type"`
	Quantity int    `json:"quantity"`
}

// catalogProductDTO is one storefront product for the caller's context: a chip-priced value
// (chips set, no money price) or a chip pack priced in the context's payment method
// (money_amount minor units + money_currency, no chips), plus what it grants.
type catalogProductDTO struct {
	Kind          string           `json:"kind"`
	ProductID     string           `json:"product_id"`
	Title         string           `json:"title"`
	Chips         int              `json:"chips"`
	MoneyAmount   int64            `json:"money_amount"`
	MoneyCurrency string           `json:"money_currency"`
	Atoms         []catalogAtomDTO `json:"atoms"`
}

// catalogDTO is the storefront: the products visible and purchasable in the caller's context.
type catalogDTO struct {
	Products []catalogProductDTO `json:"products"`
}

// catalogDTOFrom projects a payments catalog view into the wire DTO.
func catalogDTOFrom(v payments.CatalogView) catalogDTO {
	out := catalogDTO{Products: make([]catalogProductDTO, 0, len(v.Products))}
	for _, p := range v.Products {
		atoms := make([]catalogAtomDTO, 0, len(p.Atoms))
		for _, a := range p.Atoms {
			atoms = append(atoms, catalogAtomDTO{AtomType: a.AtomType, Quantity: a.Quantity})
		}
		out.Products = append(out.Products, catalogProductDTO{
			Kind: p.Kind, ProductID: p.ProductID, Title: p.Title,
			Chips: p.Chips, MoneyAmount: p.MoneyAmount, MoneyCurrency: p.MoneyCurrency, Atoms: atoms,
		})
	}
	return out
}

// handleWalletCatalog returns the storefront for the caller's trusted execution context: the
// chip-priced values and the chip packs priced in the context's payment method.
func (s *Server) handleWalletCatalog(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		return
	}
	ctx := c.Request.Context()
	cxt, _, err := s.walletGate(ctx, uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	view, err := s.payments.Catalog(ctx, cxt)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, catalogDTOFrom(view))
}

// handleOfferPricing serves the public-offer price list (§4.4) as markdown — the two catalog tables
// projected from the active products. The render sidecar fetches it and splices it into the offer
// markdown before rendering the /offer/ page. Internal, non-public: the /api/v1/internal group is
// off the edge allow-list, and the value is served from the payments cache (no per-request query in
// the steady state). Called by the renderer, not the gateway.
func (s *Server) handleOfferPricing(c *gin.Context) {
	md, err := s.payments.OfferPricing(c.Request.Context())
	if err != nil {
		s.log.Error("offer pricing projection failed", zap.Error(err))
		c.String(http.StatusInternalServerError, "offer pricing unavailable")
		return
	}
	c.Header("Content-Type", "text/markdown; charset=utf-8")
	c.String(http.StatusOK, md)
}

// handleWallet returns the caller's wallet — the segments and benefits visible in the current
// trusted execution context, plus the rewarded-video payout available here (0 outside VK or when
// unconfigured), which gates the client's "watch for chips" button.
func (s *Server) handleWallet(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		return
	}
	ctx := c.Request.Context()
	cxt, present, err := s.walletGate(ctx, uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	view, err := s.payments.Wallet(ctx, uid, cxt, present)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	dto := walletDTOFrom(view)
	if payout, perr := s.payments.RewardPayout(ctx, cxt, present); perr != nil {
		s.log.Warn("wallet: reward payout read failed", zap.String("account", uid.String()), zap.Error(perr))
	} else {
		dto.RewardChips = payout
	}
	c.JSON(http.StatusOK, dto)
}

// handleWalletBuy spends chips on a chip-priced value and returns the updated wallet. It is
// fail-closed: an untrusted or frozen context, or an insufficient balance, is refused.
func (s *Server) handleWalletBuy(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		return
	}
	var req walletBuyRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, errorResponse{Error: errorBody{Code: "invalid_request", Message: "invalid request body"}})
		return
	}
	productID, err := uuid.Parse(req.ProductID)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, errorResponse{Error: errorBody{Code: "invalid_request", Message: "invalid product id"}})
		return
	}
	ctx := c.Request.Context()
	cxt, present, err := s.walletGate(ctx, uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	if err := s.payments.Spend(ctx, uid, cxt, present, productID); err != nil {
		s.abortErr(c, err)
		return
	}
	view, err := s.payments.Wallet(ctx, uid, cxt, present)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	c.JSON(http.StatusOK, walletDTOFrom(view))
}

// walletRewardRequest is the POST body of a rewarded-video credit: a client nonce, the idempotency
// key for a single watched view (a retry credits once).
type walletRewardRequest struct {
	Nonce string `json:"nonce"`
}

// handleWalletReward credits a rewarded-video view's chips to the VK segment, client-attested and
// bounded by the config daily cap. It is VK-only and idempotent on the nonce; a reached cap answers
// reward_capped, and an unconfigured payout answers reward_unavailable. On success it returns the
// updated wallet (like a spend).
func (s *Server) handleWalletReward(c *gin.Context) {
	uid, ok := userID(c)
	if !ok {
		return
	}
	var req walletRewardRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.Nonce == "" {
		abortBadRequest(c, "nonce is required")
		return
	}
	ctx := c.Request.Context()
	cxt, present, err := s.walletGate(ctx, uid)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	outcome, err := s.payments.CreditReward(ctx, uid, cxt, present, req.Nonce)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	if outcome.Capped {
		c.AbortWithStatusJSON(http.StatusConflict, errorResponse{Error: errorBody{Code: "reward_capped", Message: "daily reward limit reached"}})
		return
	}
	if outcome.Chips == 0 && !outcome.AlreadyCredited {
		c.AbortWithStatusJSON(http.StatusConflict, errorResponse{Error: errorBody{Code: "reward_unavailable", Message: "rewarded video is not available"}})
		return
	}
	view, err := s.payments.Wallet(ctx, uid, cxt, present)
	if err != nil {
		s.abortErr(c, err)
		return
	}
	view2 := walletDTOFrom(view)
	if payout, perr := s.payments.RewardPayout(ctx, cxt, present); perr == nil {
		view2.RewardChips = payout
	}
	c.JSON(http.StatusOK, view2)
}
