package auth

import (
	"context"
	"time"

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

// LoginCodeMailer is the publisher contract auth uses to deliver a
// one-time login code to a user's mailbox. The canonical
// implementation lives in `backend/internal/mail`; tests can use
// `NewNoopLoginCodeMailer` to record the outbound code without wiring
// SMTP.
type LoginCodeMailer interface {
	EnqueueLoginCode(ctx context.Context, email, code string, ttl time.Duration) error
}

// SessionInvalidator emits the gRPC push session_invalidation event
// when auth revokes one or more device sessions. The canonical
// implementation lives in `backend/internal/push`; tests can use
// `NewNoopSessionInvalidator` for an in-memory log-only fallback.
type SessionInvalidator interface {
	PublishSessionInvalidation(ctx context.Context, deviceSessionID, userID uuid.UUID, reason string)
}

// UserEnsurer binds a confirmed email to an `accounts.user_id`. The
// canonical implementation is `*user.Service`; tests can swap in a
// recording fake.
type UserEnsurer interface {
	EnsureByEmail(ctx context.Context, email, preferredLanguage, timeZone, declaredCountry string) (uuid.UUID, error)
}

// GeoService provides the geo helpers auth needs at confirm-email-code:
// a country lookup that backfills `accounts.declared_country` for newly
// registered accounts and a post-commit write of the same column. Both
// methods are best-effort — auth never blocks the registration flow on
// geo failures.
type GeoService interface {
	LookupCountry(sourceIP string) string
	SetDeclaredCountryAtRegistration(ctx context.Context, userID uuid.UUID, sourceIP string) error
}

// NewNoopLoginCodeMailer returns a LoginCodeMailer that logs the
// outbound code at info level and returns nil. The wiring code uses
// the real `mail.Service`; this constructor exists for tests and for
// local smoke runs that do not want to bring up an SMTP relay.
func NewNoopLoginCodeMailer(logger *zap.Logger) LoginCodeMailer {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &noopLoginCodeMailer{logger: logger.Named("auth.mail.noop")}
}

type noopLoginCodeMailer struct {
	logger *zap.Logger
}

func (m *noopLoginCodeMailer) EnqueueLoginCode(_ context.Context, email, code string, ttl time.Duration) error {
	m.logger.Info("auth login code (noop publisher)",
		zap.String("email", email),
		zap.String("code", code),
		zap.Duration("ttl", ttl),
	)
	return nil
}

// NewNoopSessionInvalidator returns a SessionInvalidator that logs
// every invalidation at info level and never blocks. The wiring code
// uses the real `push.Service`; this constructor exists for tests
// that need a callable surface without bringing up gRPC.
func NewNoopSessionInvalidator(logger *zap.Logger) SessionInvalidator {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &noopSessionInvalidator{logger: logger.Named("auth.push.noop")}
}

type noopSessionInvalidator struct {
	logger *zap.Logger
}

func (p *noopSessionInvalidator) PublishSessionInvalidation(_ context.Context, deviceSessionID, userID uuid.UUID, reason string) {
	p.logger.Info("session invalidation (noop publisher)",
		zap.String("device_session_id", deviceSessionID.String()),
		zap.String("user_id", userID.String()),
		zap.String("reason", reason),
	)
}
