package email

import (
	"context"
	"fmt"
	"strings"
	"sync"

	"github.com/zeromicro/go-zero/core/logx"
)

// MockRedisHook is the minimal Redis surface needed to persist the last mock
// email body for dev/k6 inspection. *github.com/zeromicro/go-zero/core/stores/redis.Redis
// satisfies it (SetexCtx).
type MockRedisHook interface {
	SetexCtx(ctx context.Context, key, value string, seconds int) error
}

// MockSender records calls and returns configurable results (for tests and local dev).
type MockSender struct {
	name  string
	sort  int
	mu    sync.Mutex
	calls []*Message

	Err       error
	MessageID string
	SendHook  func(ctx context.Context, msg *Message) (string, error)

	// Optional Redis hook for dev/k6: every successful Send writes the body
	// to "dev:notification:last:email:<recipient>" for each To address.
	redis       MockRedisHook
	redisKeyTTL int
}

type MockSenderOption func(*MockSender)

func WithMockName(name string) MockSenderOption {
	return func(m *MockSender) { m.name = name }
}

func WithMockSort(sort int) MockSenderOption {
	return func(m *MockSender) { m.sort = sort }
}

func WithMockError(err error) MockSenderOption {
	return func(m *MockSender) { m.Err = err }
}

func WithMockMessageID(id string) MockSenderOption {
	return func(m *MockSender) { m.MessageID = id }
}

// WithMockRedis mirrors every outbound mock email body into Redis at key
// "dev:notification:last:email:<recipient>" (one key per To address).
// Primary OTP transport for k6 is still MailHog HTTP API; this hook is a
// fallback so the SMTP-disabled mock mode is also k6-friendly.
func WithMockRedis(r MockRedisHook, ttlSeconds int) MockSenderOption {
	return func(m *MockSender) {
		m.redis = r
		if ttlSeconds <= 0 {
			ttlSeconds = 600
		}
		m.redisKeyTTL = ttlSeconds
	}
}

func NewMockSender(opts ...MockSenderOption) *MockSender {
	m := &MockSender{
		name:        "mock",
		sort:        0,
		MessageID:   "mock-email-id",
		redisKeyTTL: 600,
	}
	for _, opt := range opts {
		opt(m)
	}
	return m
}

func (m *MockSender) Name() string { return m.name }
func (m *MockSender) Sort() int    { return m.sort }

// MockEmailRedisKeyPrefix is the key prefix written by the Redis hook.
const MockEmailRedisKeyPrefix = "dev:notification:last:email:"

func (m *MockSender) Send(ctx context.Context, msg *Message) (string, error) {
	m.mu.Lock()
	m.calls = append(m.calls, msg)
	m.mu.Unlock()

	if m.SendHook != nil {
		return m.SendHook(ctx, msg)
	}
	if m.Err != nil {
		return "", m.Err
	}
	if msg != nil {
		logx.Infof("[notification mock email] from=%s to=%s subject=%q body=%s message_id=%s",
			msg.From, strings.Join(msg.To, ","), msg.Subject, truncateForLog(msg.Body, 500), m.MessageID)
		if m.redis != nil {
			for _, to := range msg.To {
				if to == "" {
					continue
				}
				key := MockEmailRedisKeyPrefix + to
				if err := m.redis.SetexCtx(ctx, key, msg.Body, m.redisKeyTTL); err != nil {
					logx.Errorf("[notification mock email] redis hook setex %s: %v", key, err)
				}
			}
		}
	}
	return m.MessageID, nil
}

func truncateForLog(s string, maxLen int) string {
	if maxLen <= 0 || len(s) <= maxLen {
		return s
	}
	return s[:maxLen] + "…(truncated)"
}

func (m *MockSender) Calls() []*Message {
	m.mu.Lock()
	defer m.mu.Unlock()
	out := make([]*Message, len(m.calls))
	copy(out, m.calls)
	return out
}

func (m *MockSender) Reset() {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.calls = nil
}

// ErrSender is a Sender that always fails (helper for tests).
func ErrSender(name string, sort int, err error) Sender {
	return &staticErrSender{name: name, sort: sort, err: err}
}

type staticErrSender struct {
	name string
	sort int
	err  error
}

func (s *staticErrSender) Name() string { return s.name }
func (s *staticErrSender) Sort() int    { return s.sort }
func (s *staticErrSender) Send(context.Context, *Message) (string, error) {
	if s.err != nil {
		return "", s.err
	}
	return "", fmt.Errorf("%s: send failed", s.name)
}
