// Package promobot is the standalone promo bot: a second Telegram bot in the bot
// container whose only job is to answer /start with a localized message and a button
// that opens the MAIN bot's Mini App. It is self-contained — it never calls the
// gateway or the game — so onboarding works even when the game is down. The button is
// a URL to the main bot's direct Mini App link: a web_app button would launch the Mini
// App under the promo bot's identity (its token would sign the initData), which the
// main bot's validator would reject, so the cross-bot launch must be a t.me link. It
// reuses the same link the UI builds invitation links from (VITE_TELEGRAM_LINK).
package promobot

import (
	"cmp"
	"context"
	"html"
	"net/url"
	"strings"

	tgbot "github.com/go-telegram/bot"
	"github.com/go-telegram/bot/models"
	"go.uber.org/zap"
	"golang.org/x/time/rate"
)

// Config configures the promo bot.
type Config struct {
	// Token is the promo bot's Bot API token.
	Token string
	// APIBaseURL overrides the Bot API host ("" uses https://api.telegram.org).
	APIBaseURL string
	// TestEnv routes requests to the Bot API test environment.
	TestEnv bool
	// BotUsername is the main bot's @username without the leading @, named in the
	// message text.
	BotUsername string
	// BotLinkURL is the main bot's Mini App direct link; the button appends
	// ?startapp=<payload> to it.
	BotLinkURL string
	// StartParam is the campaign payload appended as ?startapp=<StartParam> to the launch
	// button — e.g. a variant-seed deep link ("verudit_ru-scrabble_en") the backend
	// decodes to seed a brand-new user's variant preferences. Empty falls back to
	// forwarding the user's own /start payload.
	StartParam string
	// SendRatePerSecond caps outbound sends to respect the Bot API flood limits; 0
	// disables the limiter. The burst equals the per-second rate.
	SendRatePerSecond int
}

// Bot is the promo bot wrapper around a Telegram Bot API client.
type Bot struct {
	api        *tgbot.Bot
	username   string
	linkURL    string
	startParam string
	log        *zap.Logger
	limiter    *rate.Limiter
}

// New builds the promo bot, registering a /start (and default) handler that replies
// with the launch button. It does not start polling; call Run for that.
func New(cfg Config, log *zap.Logger) (*Bot, error) {
	if log == nil {
		log = zap.NewNop()
	}
	t := &Bot{username: cfg.BotUsername, linkURL: cfg.BotLinkURL, startParam: cfg.StartParam, log: log}
	if cfg.SendRatePerSecond > 0 {
		t.limiter = rate.NewLimiter(rate.Limit(cfg.SendRatePerSecond), cfg.SendRatePerSecond)
	}
	opts := []tgbot.Option{
		tgbot.WithDefaultHandler(t.handleStart),
		tgbot.WithMessageTextHandler("/start", tgbot.MatchTypePrefix, t.handleStart),
	}
	if cfg.TestEnv {
		opts = append(opts, tgbot.UseTestEnvironment())
	}
	if cfg.APIBaseURL != "" {
		opts = append(opts, tgbot.WithServerURL(cfg.APIBaseURL))
	}
	api, err := tgbot.New(cfg.Token, opts...)
	if err != nil {
		return nil, err
	}
	t.api = api
	return t, nil
}

// Run sets the bot command, then blocks on the long-poll update loop until ctx is
// cancelled.
func (t *Bot) Run(ctx context.Context) {
	if _, err := t.api.SetMyCommands(ctx, &tgbot.SetMyCommandsParams{
		Commands: []models.BotCommand{{Command: "start", Description: "Open Scrabble"}},
	}); err != nil {
		t.log.Warn("promo: set commands failed", zap.Error(err))
	}
	t.api.Start(ctx)
}

// handleStart replies to any message (typically /start) with the localized promo text
// and a button that opens the main bot's Mini App at the configured campaign payload
// (falling back to forwarding any /start payload the user arrived with).
func (t *Bot) handleStart(ctx context.Context, api *tgbot.Bot, update *models.Update) {
	if update.Message == nil {
		return
	}
	// Only respond to a private /start: the promo bot is a one-on-one onboarding entry
	// point and should never reply to group messages.
	if update.Message.Chat.Type != models.ChatTypePrivate {
		return
	}
	if err := t.throttle(ctx); err != nil {
		return
	}
	lang := ""
	if update.Message.From != nil {
		lang = update.Message.From.LanguageCode
	}
	// The configured campaign payload (a variant-seed deep link) takes precedence; absent
	// one, fall back to forwarding any /start payload the user arrived with. The same
	// payload backs both the inline button and the @username link in the body.
	param := cmp.Or(t.startParam, startPayload(update.Message.Text))
	text, button := promoText(lang, t.username, t.launchURL(param))
	if _, err := api.SendMessage(ctx, &tgbot.SendMessageParams{
		ChatID:      update.Message.Chat.ID,
		Text:        text,
		ParseMode:   models.ParseModeHTML,
		ReplyMarkup: t.launchMarkup(button, param),
	}); err != nil {
		t.log.Warn("promo: reply to start failed", zap.Error(err))
	}
}

// launchMarkup builds the single URL button that opens the main bot's Mini App at the
// optional startapp payload.
func (t *Bot) launchMarkup(buttonText, startParam string) *models.InlineKeyboardMarkup {
	return &models.InlineKeyboardMarkup{
		InlineKeyboard: [][]models.InlineKeyboardButton{{
			{Text: buttonText, URL: t.launchURL(startParam)},
		}},
	}
}

// launchURL appends the startapp payload to the main bot's Mini App link; an empty
// payload returns the base link unchanged.
func (t *Bot) launchURL(startParam string) string {
	if startParam == "" {
		return t.linkURL
	}
	u, err := url.Parse(t.linkURL)
	if err != nil {
		return t.linkURL
	}
	q := u.Query()
	q.Set("startapp", startParam)
	u.RawQuery = q.Encode()
	return u.String()
}

// throttle blocks until the rate limiter admits one send, or ctx is cancelled. It is
// a no-op when no limiter is configured.
func (t *Bot) throttle(ctx context.Context) error {
	if t.limiter == nil {
		return nil
	}
	return t.limiter.Wait(ctx)
}

// startPayload extracts the deep-link payload from a "/start <payload>" command; any
// other text yields an empty payload (open the lobby).
func startPayload(text string) string {
	const cmd = "/start"
	if !strings.HasPrefix(text, cmd) {
		return ""
	}
	return strings.TrimSpace(strings.TrimPrefix(text, cmd))
}

// promoText returns the localized message body and button label. The body names the main
// bot as a clickable @username whose link is the Mini App deep link (launchURL, the same
// target as the button), so tapping the mention opens the seeded Mini App rather than the
// bot profile. The body is sent with ParseMode HTML; only the link carries markup, so the
// static sentences need no escaping. Russian for a "ru" language code, English otherwise.
func promoText(lang, username, launchURL string) (text, button string) {
	mention := `<a href="` + html.EscapeString(launchURL) + `">@` + html.EscapeString(username) + `</a>`
	if strings.HasPrefix(strings.ToLower(lang), "ru") {
		return "Откройте " + mention + " и выберите в настройках профиля нужный вариант игры.", "🤩 Хочу играть!"
	}
	return "Open " + mention + " and choose your game variant in the profile settings.", "🤩 I want to play!"
}
