// Package adminalert emails the operator when new player feedback or word complaints
// arrive, coalescing a burst into a single digest per interval so a flood is one email,
// not N. It is inert unless an admin sender and recipient are configured. The sender is
// distinct from the user-facing confirm-code From, and the recipient may be several
// comma-separated addresses (the mailer splits them).
package adminalert

import (
	"context"
	"fmt"
	"strings"
	"time"

	"go.uber.org/zap"

	"scrabble/backend/internal/account"
)

// FeedbackCounter counts feedback created since a time (satisfied by feedback.Service).
type FeedbackCounter interface {
	CountSince(ctx context.Context, since time.Time) (int, error)
}

// ComplaintCounter counts word complaints filed since a time (satisfied by game.Service).
type ComplaintCounter interface {
	CountComplaintsSince(ctx context.Context, since time.Time) (int, error)
}

// Notifier polls for new feedback and complaints and emails the operator a digest.
type Notifier struct {
	mailer     account.Mailer
	feedback   FeedbackCounter
	complaints ComplaintCounter
	from       string
	to         string
	clock      func() time.Time
	log        *zap.Logger
	last       time.Time
}

// New constructs a Notifier. from and to are the alert sender and recipient(s); log may be
// nil. The watermark starts at "now", so only items arriving after start-up are reported. The
// digest deliberately carries no admin-console link — an admin URL must never travel in an
// email, where a mail provider could cache or index it.
func New(mailer account.Mailer, fb FeedbackCounter, cp ComplaintCounter, from, to string, log *zap.Logger) *Notifier {
	if log == nil {
		log = zap.NewNop()
	}
	return &Notifier{
		mailer: mailer, feedback: fb, complaints: cp, from: from, to: to,
		clock: func() time.Time { return time.Now().UTC() }, log: log, last: time.Now().UTC(),
	}
}

// Run polls on each tick until ctx is cancelled.
func (n *Notifier) Run(ctx context.Context, interval time.Duration) {
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			n.tick(ctx)
		}
	}
}

// tick counts what arrived since the last watermark and, if anything did, emails one
// digest. The watermark only advances after a successful send (or a quiet tick), so a
// transient send failure is retried on the next tick — the counts simply grow.
func (n *Notifier) tick(ctx context.Context) {
	now := n.clock()
	fb, err := n.feedback.CountSince(ctx, n.last)
	if err != nil {
		n.log.Warn("admin alert: count feedback failed", zap.Error(err))
		return
	}
	cp, err := n.complaints.CountComplaintsSince(ctx, n.last)
	if err != nil {
		n.log.Warn("admin alert: count complaints failed", zap.Error(err))
		return
	}
	if fb == 0 && cp == 0 {
		n.last = now
		return
	}
	if err := n.mailer.Send(ctx, n.digest(fb, cp)); err != nil {
		n.log.Warn("admin alert: send failed", zap.Error(err))
		return
	}
	n.log.Info("admin alert sent", zap.Int("feedback", fb), zap.Int("complaints", cp))
	n.last = now
}

// digest builds the operator alert email for fb new feedback and cp new complaints.
func (n *Notifier) digest(fb, cp int) account.Message {
	var parts []string
	if fb > 0 {
		parts = append(parts, fmt.Sprintf("%d new feedback message(s)", fb))
	}
	if cp > 0 {
		parts = append(parts, fmt.Sprintf("%d new word complaint(s)", cp))
	}
	summary := strings.Join(parts, ", ")
	// No admin-console link in the body: an admin URL must never travel in an email (a mail
	// provider could cache or index it). The operator opens the console directly.
	text := summary + "."
	return account.Message{
		From:    n.from,
		To:      n.to,
		Subject: "Erudit — " + summary,
		Text:    text,
	}
}
