// Package pagination provides cursor-based (keyset) pagination utilities.
//
// Unlike offset-based pagination (OFFSET/LIMIT), cursor pagination uses
// a composite key (typically created_at + id) to locate the "position" in
// the result set, giving O(limit) performance regardless of how deep you page.
package pagination

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"time"
)

// Cursor represents an opaque position in a sorted result set.
// It is serialized as a URL-safe base64 string for transport.
type Cursor struct {
	// LastID is the primary key of the last item on the current page.
	LastID int64 `json:"last_id"`
	// LastValue is the sort column value of the last item (e.g. created_at).
	LastValue time.Time `json:"last_value"`
}

// Encode serializes a Cursor to a URL-safe base64 string suitable for query params.
func (c *Cursor) Encode() string {
	if c == nil || c.LastID == 0 {
		return ""
	}
	data, _ := json.Marshal(c)
	return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data)
}

// Decode parses a base64-encoded cursor string back into a Cursor.
// Returns nil for empty strings (meaning "first page").
func Decode(encoded string) (*Cursor, error) {
	if encoded == "" {
		return nil, nil
	}
	data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(encoded)
	if err != nil {
		return nil, fmt.Errorf("invalid cursor encoding: %w", err)
	}
	var c Cursor
	if err := json.Unmarshal(data, &c); err != nil {
		return nil, fmt.Errorf("invalid cursor data: %w", err)
	}
	return &c, nil
}

// PageResult wraps a paginated response with cursor navigation info.
type PageResult[T any] struct {
	Items      []T    `json:"items"`
	Total      int64  `json:"total"`       // Approximate or exact total (optional for pure cursor mode)
	NextCursor string `json:"next_cursor"` // Empty means no more pages
	HasMore    bool   `json:"has_more"`
	PageSize   int    `json:"page_size"`
}

// DefaultPageSize is the default number of items per page.
const DefaultPageSize = 20

// MaxPageSize caps the maximum allowed items per request to prevent abuse.
const MaxPageSize = 100

// ClampPageSize ensures size is within [1, MaxPageSize], falling back to DefaultPageSize.
func ClampPageSize(size int) int {
	if size <= 0 {
		return DefaultPageSize
	}
	if size > MaxPageSize {
		return MaxPageSize
	}
	return size
}

// BuildNextCursor creates a cursor from the last item's ID and timestamp.
// Returns empty string if there are no items.
func BuildNextCursor(lastID int64, lastTime time.Time) string {
	if lastID == 0 {
		return ""
	}
	return (&Cursor{LastID: lastID, LastValue: lastTime}).Encode()
}
