package sms

import (
	"context"
	"fmt"
	"sync"

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

// MockRedisHook is the minimal Redis surface needed to persist the last mock
// SMS body for dev/k6 inspection. *github.com/zeromicro/go-zero/core/stores/redis.Redis
// already satisfies it (SetexCtx).
//
// Production SMS senders never receive this hook; it is only wired when
// SMSConfig.Provider=="mock" and a Redis client is available.
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: on every successful Send, the message
	// body is written to "dev:notification:last:sms:<phone>" with the given TTL.
	// nil hook → behaviour identical to original implementation.
	redis       MockRedisHook
	redisKeyTTL int // seconds, defaults to 600 when redis hook set
}

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 enables dev/k6 OTP inspection by mirroring every outbound
// mock SMS body into Redis at key "dev:notification:last:sms:<phone>".
// ttlSeconds <= 0 → defaults to 600 (10m).
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-sms-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 }

// MockSMSRedisKeyPrefix is the key prefix written by the Redis hook.
// Exposed so k6 / dev tooling can resolve the key for a given phone.
const MockSMSRedisKeyPrefix = "dev:notification:last:sms:"

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 sms] to=%s recipient=%s body=%q message_id=%s",
			msg.PhoneNumber, msg.RecipientName, msg.Body, m.MessageID)
		if m.redis != nil && msg.PhoneNumber != "" {
			key := MockSMSRedisKeyPrefix + msg.PhoneNumber
			if err := m.redis.SetexCtx(ctx, key, msg.Body, m.redisKeyTTL); err != nil {
				logx.Errorf("[notification mock sms] redis hook setex %s: %v", key, err)
			}
		}
	}
	return m.MessageID, nil
}

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)
}
