package bot

import (
	"context"
	"time"

	tgbot "github.com/go-telegram/bot"
	"github.com/go-telegram/bot/models"
	"go.uber.org/zap"

	"scrabble/platform/telegram/internal/outbox"
)

// starsCurrency is the Telegram Stars currency code for createInvoiceLink and the invoice line.
const starsCurrency = "XTR"

// paymentDrainInterval is how often the bot re-drives undelivered outbox payments (the backstop for a
// gateway or backend outage, and the restart re-drive); the happy path forwards immediately on receipt.
const paymentDrainInterval = 30 * time.Second

// PreCheckoutValidator validates a Stars pre_checkout order over the bot-link and returns whether it
// may be charged plus a short, already-localised decline reason for the payer. The bot-link client
// backs it.
type PreCheckoutValidator func(ctx context.Context, orderID string, amount int64, currency string) (ok bool, reason string, err error)

// PaymentForwarder delivers a completed Stars payment over the bot-link for crediting and reports
// whether it was credited (or already had been). A non-nil error is transient and retried. The
// bot-link client backs it.
type PaymentForwarder func(ctx context.Context, orderID, chargeID string, amount, telegramUserID int64) (credited bool, err error)

// SetPaymentHandlers wires the Telegram Stars runtime dependencies after construction: the
// pre_checkout validator and payment forwarder (both over the bot-link, built after the bot) and the
// durable outbox. It is the late binding that breaks the bot <-> bot-link construction cycle.
func (t *Bot) SetPaymentHandlers(precheck PreCheckoutValidator, forward PaymentForwarder, store *outbox.Store) {
	t.precheck = precheck
	t.forward = forward
	t.outbox = store
}

// CreateInvoiceLink mints a Telegram Stars invoice link (XTR) for amountStars, tagged with payload
// (the order id, echoed back in pre_checkout and successful_payment), and returns the link. The
// provider token is empty for Stars. Only the bot reaches Telegram, so the gateway calls this over
// the bot-link on the wallet-order path.
func (t *Bot) CreateInvoiceLink(ctx context.Context, title, description, payload string, amountStars int64) (string, error) {
	return t.api.CreateInvoiceLink(ctx, &tgbot.CreateInvoiceLinkParams{
		Title:       title,
		Description: description,
		Payload:     payload,
		Currency:    starsCurrency,
		Prices:      []models.LabeledPrice{{Label: title, Amount: int(amountStars)}},
	})
}

// handlePreCheckout answers a Stars pre_checkout_query. It validates the order against the backend
// (over the bot-link) and approves only on a positive answer; a not-ok answer or a validation
// failure declines the charge (fail-closed), before any star moves. The decline reason from the
// backend is already localised to the payer's account language.
func (t *Bot) handlePreCheckout(ctx context.Context, q *models.PreCheckoutQuery) {
	ok, reason := false, ""
	if t.precheck == nil {
		t.log.Warn("pre_checkout received but the validator is not wired; declining", zap.String("order", q.InvoicePayload))
		reason = fallbackDeclineText(q.From)
	} else if v, r, err := t.precheck(ctx, q.InvoicePayload, int64(q.TotalAmount), q.Currency); err != nil {
		t.log.Warn("pre_checkout validation failed; declining", zap.String("order", q.InvoicePayload), zap.Error(err))
		reason = fallbackDeclineText(q.From)
	} else {
		ok, reason = v, r
	}
	params := &tgbot.AnswerPreCheckoutQueryParams{PreCheckoutQueryID: q.ID, OK: ok}
	if !ok {
		params.ErrorMessage = reason
	}
	if _, err := t.api.AnswerPreCheckoutQuery(ctx, params); err != nil {
		t.log.Warn("answer pre_checkout failed", zap.String("order", q.InvoicePayload), zap.Error(err))
	}
}

// handleSuccessfulPayment records a completed Stars payment in the durable outbox and forwards it to
// the gateway. Persisting first is the durability point: a crash before forwarding still re-drives
// the payment on restart. The immediate forward is best-effort; the periodic drainer covers a
// gateway or backend outage.
func (t *Bot) handleSuccessfulPayment(ctx context.Context, msg *models.Message) {
	sp := msg.SuccessfulPayment
	if t.outbox == nil {
		t.log.Error("successful_payment received but the outbox is not wired; the payment is at risk",
			zap.String("charge", sp.TelegramPaymentChargeID))
		return
	}
	var tgUserID int64
	if msg.From != nil {
		tgUserID = msg.From.ID
	}
	rec := outbox.Record{
		ChargeID: sp.TelegramPaymentChargeID,
		OrderID:  sp.InvoicePayload,
		Amount:   int64(sp.TotalAmount),
		UserID:   tgUserID,
	}
	if err := t.outbox.Add(ctx, rec); err != nil {
		t.log.Error("outbox persist failed; the drainer cannot recover this payment",
			zap.String("charge", rec.ChargeID), zap.Error(err))
		return
	}
	t.log.Info("stars payment received", zap.String("charge", rec.ChargeID), zap.String("order", rec.OrderID))
	t.forwardOne(ctx, rec)
}

// RunPaymentDrainer re-drives undelivered outbox payments: once at startup (recovering any left by a
// restart or a past outage) and then on paymentDrainInterval, until ctx is cancelled. It is a no-op
// when the Stars rail is not wired.
func (t *Bot) RunPaymentDrainer(ctx context.Context) {
	if t.outbox == nil || t.forward == nil {
		return
	}
	t.drainOutbox(ctx)
	ticker := time.NewTicker(paymentDrainInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			t.drainOutbox(ctx)
		}
	}
}

// drainOutbox forwards a batch of pending payments to the gateway.
func (t *Bot) drainOutbox(ctx context.Context) {
	recs, err := t.outbox.Pending(ctx, 50)
	if err != nil {
		t.log.Warn("outbox drain read failed", zap.Error(err))
		return
	}
	for _, rec := range recs {
		t.forwardOne(ctx, rec)
	}
}

// forwardOne delivers one payment to the gateway and, on a durable response (credited or not), marks
// it forwarded so it is not re-sent. A transport error leaves the row for the next drain. Crediting is
// idempotent at the backend, so a re-forward after a lost ack never double-credits.
func (t *Bot) forwardOne(ctx context.Context, rec outbox.Record) {
	if t.forward == nil {
		return
	}
	credited, err := t.forward(ctx, rec.OrderID, rec.ChargeID, rec.Amount, rec.UserID)
	if err != nil {
		t.log.Warn("forward payment failed; will retry", zap.String("charge", rec.ChargeID), zap.Error(err))
		return
	}
	if err := t.outbox.MarkForwarded(ctx, rec.ChargeID); err != nil {
		t.log.Error("mark forwarded failed", zap.String("charge", rec.ChargeID), zap.Error(err))
		return
	}
	t.log.Info("stars payment forwarded", zap.String("charge", rec.ChargeID), zap.String("order", rec.OrderID), zap.Bool("credited", credited))
}

// fallbackDeclineText is the pre_checkout decline message used only when the backend cannot be
// reached to form a localised one; it falls back to the payer's Telegram client language.
func fallbackDeclineText(from *models.User) string {
	if from != nil && from.LanguageCode == "ru" {
		return "Оплата временно недоступна. Попробуйте позже."
	}
	return "Payment is temporarily unavailable. Please try again later."
}
