package main

import (
	"context"
	"sort"
	"time"

	"github.com/go-telegram/bot/models"
)

// albumFlushWindow is the debounce delay before a buffered Telegram media_group
// is flushed as a single coalesced user turn. 1s matches the de-facto community
// standard across the dominant third-party album plugins (aiogram-media-group,
// DieTime/telegraf-media-group) and sits above the sub-100ms values documented
// as lossy under network jitter (openclaw#1811). Telegram has no official
// "album complete" signal, so timeout-based flush is the only option.
const albumFlushWindow = 1 * time.Second

// pendingAlbum holds a Telegram media_group as its items arrive, plus the
// per-user metadata captured from the first item. All items in an album share
// the same chat/user, so we record metadata once and reuse it at flush time.
type pendingAlbum struct {
	items []*models.Message
	// Metadata captured from the first arriving item. Albums always come from
	// the same user/chat, so these are stable across the buffering window.
	chatID, userID                              int64
	username, firstName, lastName, languageCode string
	isPremium                                   bool
	messageTime                                 int
	businessConnectionID                        string
	// timer flushes the album after albumFlushWindow with no further arrivals.
	// Each new arrival stops the previous timer (best-effort) and installs a
	// fresh one — the standard debounce pattern.
	timer *time.Timer
}

// bufferAlbumItem appends an incoming Telegram album item to the per-MediaGroupID
// buffer. On first arrival it captures the user/chat metadata and starts the
// flush timer; on subsequent arrivals it appends the item and extends the timer.
// The 1s debounce gives the rest of the album time to arrive over the network.
func (b *Bot) bufferAlbumItem(
	ctx context.Context,
	msg *models.Message,
	chatID, userID int64,
	username, firstName, lastName string,
	isPremium bool,
	languageCode string,
	messageTime int,
	businessConnectionID string,
) {
	b.albumBuffersMu.Lock()
	defer b.albumBuffersMu.Unlock()

	album, exists := b.albumBuffers[msg.MediaGroupID]
	if !exists {
		album = &pendingAlbum{
			chatID:               chatID,
			userID:               userID,
			username:             username,
			firstName:            firstName,
			lastName:             lastName,
			isPremium:            isPremium,
			languageCode:         languageCode,
			messageTime:          messageTime,
			businessConnectionID: businessConnectionID,
		}
		b.albumBuffers[msg.MediaGroupID] = album
	}
	album.items = append(album.items, msg)

	// Stop the previous timer best-effort; even if it already fired the race
	// is benign because flushAlbum removes the map entry under the lock — a
	// late arrival would simply seed a fresh album.
	if album.timer != nil {
		album.timer.Stop()
	}
	mediaGroupID := msg.MediaGroupID
	album.timer = time.AfterFunc(albumFlushWindow, func() {
		b.flushAlbum(ctx, mediaGroupID)
	})
}

// flushAlbum is called by the flush timer (or by code that needs to force-flush
// during shutdown). It removes the album from the buffer, sorts items by
// message_id (Telegram does not guarantee in-order arrival), runs the rate-limit
// check once, and dispatches to handlePhotoMessage.
func (b *Bot) flushAlbum(ctx context.Context, mediaGroupID string) {
	b.albumBuffersMu.Lock()
	album, exists := b.albumBuffers[mediaGroupID]
	if !exists {
		b.albumBuffersMu.Unlock()
		return
	}
	delete(b.albumBuffers, mediaGroupID)
	items := album.items
	captured := *album // copy fields for use after unlock
	b.albumBuffersMu.Unlock()

	// Sort by Telegram message_id: items in an album arrive as separate Updates
	// over the network and may interleave. Sorting restores the user's intended
	// order before we hand them to Claude.
	sort.Slice(items, func(i, j int) bool { return items[i].ID < items[j].ID })

	// Rate-limit fires once per coalesced album, not once per item.
	if !b.checkRateLimits(captured.userID) {
		b.sendRateLimitExceededMessage(ctx, captured.chatID, captured.businessConnectionID)
		return
	}

	b.handlePhotoMessage(
		ctx, items,
		captured.chatID, captured.userID,
		captured.username, captured.firstName, captured.lastName,
		captured.isPremium, captured.languageCode, captured.messageTime,
		captured.businessConnectionID,
	)
}
