package mail

import (
	"context"
	"errors"
	"time"

	"galaxy/backend/internal/config"

	"github.com/google/uuid"
	"go.uber.org/zap"
)

// SMTPSender is the wire-level boundary the worker uses to deliver an
// outbox row through SMTP. Implementations are expected to be
// concurrency-safe and to honour ctx cancellation: the worker passes a
// per-row context bounded by the configured operation timeout.
//
// `Send` is the single point where transient-vs-permanent classification
// happens; the returned error carries IsPermanent to let the worker
// decide between schedule-a-retry and dead-letter.
type SMTPSender interface {
	Send(ctx context.Context, msg OutboundMessage) error
}

// OutboundMessage is the rendered, recipient-addressed payload handed
// to SMTPSender. From is taken from BACKEND_SMTP_FROM at construction
// time, so producers and the worker never set it directly.
type OutboundMessage struct {
	To []string
	Subject string
	ContentType string
	Body []byte
}

// SendError augments a regular error with a permanence classification.
// Permanent errors (RFC 5321 5xx, malformed addresses, oversize body)
// dead-letter the row immediately on the next attempt; transient ones
// (4xx, network) trigger the backoff schedule.
type SendError struct {
	Err error
	Permanent bool
}

// Error returns the underlying error string.
func (e *SendError) Error() string {
	if e == nil || e.Err == nil {
		return ""
	}
	return e.Err.Error()
}

// Unwrap exposes the underlying error for errors.Is / errors.As.
func (e *SendError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Err
}

// IsPermanent reports whether err is a *SendError marked Permanent.
// Non-SendError values are treated as transient by default — the
// worker will retry until MaxAttempts.
func IsPermanent(err error) bool {
	if err == nil {
		return false
	}
	var se *SendError
	if errors.As(err, &se) && se != nil {
		return se.Permanent
	}
	return false
}

// AdminNotifier is the outbound surface mail uses to flag a dead-letter
// to operators. The canonical notification wiring lives in `cmd/backend/main.go` and publisher; until
// then NewNoopAdminNotifier ships a logger-only stub matching the
// pattern used elsewhere in `backend/internal/*`.
type AdminNotifier interface {
	OnDeadLetter(ctx context.Context, deliveryID uuid.UUID, templateID, reason string)
}

// Deps aggregates every collaborator the Service depends on.
//
// Store and SMTP must be non-nil. Admin defaults to a no-op publisher
// when omitted; Now defaults to time.Now; Logger defaults to
// zap.NewNop. Config carries the worker interval and max-attempts
// derived from `BACKEND_MAIL_*`.
type Deps struct {
	Store *Store
	SMTP SMTPSender
	Admin AdminNotifier
	Config config.MailConfig
	// Now overrides time.Now for deterministic tests. A nil Now defaults
	// to time.Now in NewService.
	Now func() time.Time
	// Logger is named under "mail" by NewService. Nil falls back to
	// zap.NewNop.
	Logger *zap.Logger
}

// NewNoopAdminNotifier returns an AdminNotifier that logs every
// dead-letter event at warn level and never blocks. The canonical implementation replaces // it with the real notification publisher.
func NewNoopAdminNotifier(logger *zap.Logger) AdminNotifier {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &noopAdminNotifier{logger: logger.Named("notify.noop")}
}

type noopAdminNotifier struct {
	logger *zap.Logger
}

func (n *noopAdminNotifier) OnDeadLetter(_ context.Context, deliveryID uuid.UUID, templateID, reason string) {
	n.logger.Warn("mail dead-letter (noop publisher)",
		zap.String("delivery_id", deliveryID.String()),
		zap.String("template_id", templateID),
		zap.String("reason", reason),
	)
}
