// Package rocketmqx hides the Apache RocketMQ client behind the small surface
// this codebase needs: synchronous publish, delayed publish by millisecond
// timestamp, and clustering push consume.
package rocketmqx

import (
	"context"
	"errors"
	"fmt"
	"net"
	"strings"
	"time"

	rocketmq "github.com/apache/rocketmq-client-go/v2"
	"github.com/apache/rocketmq-client-go/v2/consumer"
	"github.com/apache/rocketmq-client-go/v2/primitive"
	"github.com/apache/rocketmq-client-go/v2/producer"
)

const (
	// Tencent Cloud RocketMQ accepts this property as an absolute Unix epoch
	// millisecond delivery timestamp. Open-source RocketMQ delay levels remain
	// available through the native client, but the rocket launch timer needs an
	// exact backend-configured launch_at_ms.
	PropertyStartDeliverTime = "__STARTDELIVERTIME"
)

// EndpointConfig is the shared RocketMQ connection identity.
type EndpointConfig struct {
	NameServers      []string
	NameServerDomain string
	AccessKey        string
	SecretKey        string
	SecurityToken    string
	Namespace        string
	VIPChannel       bool
}

// ProducerConfig configures one producer group.
type ProducerConfig struct {
	EndpointConfig
	GroupName   string
	SendTimeout time.Duration
	Retry       int
}

// ConsumerConfig configures one clustering push consumer group.
type ConsumerConfig struct {
	EndpointConfig
	GroupName          string
	MaxReconsumeTimes  int32
	ConsumeRetryDelay  time.Duration
	ConsumePullBatch   int32
	ConsumePullTimeout time.Duration
	ConsumeGoroutines  int
}

// Message is the stable publish shape used by services.
type Message struct {
	Topic        string
	Tag          string
	Keys         []string
	Body         []byte
	Properties   map[string]string
	DeliveryAtMS int64
}

// ConsumedMessage is the stable consume shape used by service handlers.
type ConsumedMessage struct {
	Topic          string
	Tag            string
	Keys           string
	MsgID          string
	Body           []byte
	Properties     map[string]string
	ReconsumeTimes int32
}

// Handler returns nil after the message has been durably processed. A non-nil
// error asks RocketMQ to retry the message according to the consumer group
// retry policy.
type Handler func(context.Context, ConsumedMessage) error

// Producer wraps a started or startable RocketMQ producer.
type Producer struct {
	client rocketmq.Producer
}

// NewProducer creates a RocketMQ producer. Call Start before SendSync.
func NewProducer(cfg ProducerConfig) (*Producer, error) {
	if err := validateEndpoint(cfg.EndpointConfig); err != nil {
		return nil, err
	}
	group := strings.TrimSpace(cfg.GroupName)
	if group == "" {
		return nil, errors.New("rocketmq producer group_name is required")
	}
	options := []producer.Option{producer.WithGroupName(group)}
	endpointOptions, err := producerEndpointOptions(cfg.EndpointConfig)
	if err != nil {
		return nil, err
	}
	options = append(options, endpointOptions...)
	if cfg.SendTimeout > 0 {
		options = append(options, producer.WithSendMsgTimeout(cfg.SendTimeout))
	}
	if cfg.Retry > 0 {
		options = append(options, producer.WithRetry(cfg.Retry))
	}
	client, err := rocketmq.NewProducer(options...)
	if err != nil {
		return nil, err
	}
	return &Producer{client: client}, nil
}

// Start registers the producer group and opens broker connections.
func (p *Producer) Start() error {
	if p == nil || p.client == nil {
		return errors.New("rocketmq producer is not configured")
	}
	return p.client.Start()
}

// Shutdown releases the underlying RocketMQ producer.
func (p *Producer) Shutdown() error {
	if p == nil || p.client == nil {
		return nil
	}
	return p.client.Shutdown()
}

// SendSync publishes one message and only returns nil when the broker accepts it.
func (p *Producer) SendSync(ctx context.Context, message Message) error {
	if p == nil || p.client == nil {
		return errors.New("rocketmq producer is not configured")
	}
	topic := strings.TrimSpace(message.Topic)
	if topic == "" {
		return errors.New("rocketmq topic is required")
	}
	msg := primitive.NewMessage(topic, message.Body)
	if tag := strings.TrimSpace(message.Tag); tag != "" {
		msg.WithTag(tag)
	}
	keys := make([]string, 0, len(message.Keys))
	for _, key := range message.Keys {
		if key = strings.TrimSpace(key); key != "" {
			keys = append(keys, key)
		}
	}
	if len(keys) > 0 {
		msg.WithKeys(keys)
	}
	for key, value := range message.Properties {
		msg.WithProperty(strings.TrimSpace(key), strings.TrimSpace(value))
	}
	if message.DeliveryAtMS > 0 {
		msg.WithProperty(PropertyStartDeliverTime, fmt.Sprintf("%d", message.DeliveryAtMS))
	}
	result, err := p.client.SendSync(ctx, msg)
	if err != nil {
		return err
	}
	if result == nil || result.Status != primitive.SendOK {
		if result == nil {
			return errors.New("rocketmq send failed: empty result")
		}
		return fmt.Errorf("rocketmq send failed: status=%d msg_id=%s", result.Status, result.MsgID)
	}
	return nil
}

// Consumer wraps a clustering push consumer.
type Consumer struct {
	client rocketmq.PushConsumer
}

// NewConsumer creates a RocketMQ push consumer. Subscribe before Start.
func NewConsumer(cfg ConsumerConfig) (*Consumer, error) {
	if err := validateEndpoint(cfg.EndpointConfig); err != nil {
		return nil, err
	}
	group := strings.TrimSpace(cfg.GroupName)
	if group == "" {
		return nil, errors.New("rocketmq consumer group_name is required")
	}
	options := []consumer.Option{consumer.WithGroupName(group)}
	endpointOptions, err := consumerEndpointOptions(cfg.EndpointConfig)
	if err != nil {
		return nil, err
	}
	options = append(options, endpointOptions...)
	if cfg.MaxReconsumeTimes > 0 {
		options = append(options, consumer.WithMaxReconsumeTimes(cfg.MaxReconsumeTimes))
	}
	if cfg.ConsumeRetryDelay > 0 {
		options = append(options, consumer.WithSuspendCurrentQueueTimeMillis(cfg.ConsumeRetryDelay))
	}
	if cfg.ConsumePullBatch > 0 {
		options = append(options, consumer.WithPullBatchSize(cfg.ConsumePullBatch))
	}
	if cfg.ConsumePullTimeout > 0 {
		options = append(options, consumer.WithConsumeTimeout(cfg.ConsumePullTimeout))
	}
	if cfg.ConsumeGoroutines > 0 {
		options = append(options, consumer.WithConsumeGoroutineNums(cfg.ConsumeGoroutines))
	}
	client, err := rocketmq.NewPushConsumer(options...)
	if err != nil {
		return nil, err
	}
	return &Consumer{client: client}, nil
}

// Subscribe binds a topic/tag selector to a handler. Empty tag subscribes all.
func (c *Consumer) Subscribe(topic string, tag string, handler Handler) error {
	if c == nil || c.client == nil {
		return errors.New("rocketmq consumer is not configured")
	}
	topic = strings.TrimSpace(topic)
	if topic == "" {
		return errors.New("rocketmq topic is required")
	}
	if handler == nil {
		return errors.New("rocketmq handler is required")
	}
	selector := consumer.MessageSelector{}
	if tag = strings.TrimSpace(tag); tag != "" {
		selector = consumer.MessageSelector{Type: consumer.TAG, Expression: tag}
	}
	return c.client.Subscribe(topic, selector, func(ctx context.Context, messages ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
		for _, message := range messages {
			if message == nil {
				continue
			}
			err := handler(ctx, ConsumedMessage{
				Topic:          message.Topic,
				Tag:            message.GetTags(),
				Keys:           message.GetKeys(),
				MsgID:          message.MsgId,
				Body:           message.Body,
				Properties:     message.GetProperties(),
				ReconsumeTimes: message.ReconsumeTimes,
			})
			if err != nil {
				return consumer.ConsumeRetryLater, err
			}
		}
		return consumer.ConsumeSuccess, nil
	})
}

// Start begins pulling messages.
func (c *Consumer) Start() error {
	if c == nil || c.client == nil {
		return errors.New("rocketmq consumer is not configured")
	}
	return c.client.Start()
}

// Shutdown releases the underlying consumer.
func (c *Consumer) Shutdown() error {
	if c == nil || c.client == nil {
		return nil
	}
	return c.client.Shutdown()
}

func validateEndpoint(cfg EndpointConfig) error {
	nameservers := normalizeStringSlice(cfg.NameServers)
	if len(nameservers) == 0 && strings.TrimSpace(cfg.NameServerDomain) == "" {
		return errors.New("rocketmq name_servers or name_server_domain is required")
	}
	return nil
}

func producerEndpointOptions(cfg EndpointConfig) ([]producer.Option, error) {
	options := make([]producer.Option, 0, 5)
	if nameservers := normalizeStringSlice(cfg.NameServers); len(nameservers) > 0 {
		var err error
		nameservers, err = resolveNameServers(nameservers)
		if err != nil {
			return nil, err
		}
		options = append(options, producer.WithNsResolver(primitive.NewPassthroughResolver(nameservers)))
	} else if domain := strings.TrimSpace(cfg.NameServerDomain); domain != "" {
		options = append(options, producer.WithNameServerDomain(domain))
	}
	if namespace := strings.TrimSpace(cfg.Namespace); namespace != "" {
		options = append(options, producer.WithNamespace(namespace))
	}
	options = append(options, producer.WithVIPChannel(cfg.VIPChannel))
	if strings.TrimSpace(cfg.AccessKey) != "" || strings.TrimSpace(cfg.SecretKey) != "" {
		options = append(options, producer.WithCredentials(credentials(cfg)))
	}
	return options, nil
}

func consumerEndpointOptions(cfg EndpointConfig) ([]consumer.Option, error) {
	options := make([]consumer.Option, 0, 5)
	if nameservers := normalizeStringSlice(cfg.NameServers); len(nameservers) > 0 {
		var err error
		nameservers, err = resolveNameServers(nameservers)
		if err != nil {
			return nil, err
		}
		options = append(options, consumer.WithNsResolver(primitive.NewPassthroughResolver(nameservers)))
	} else if domain := strings.TrimSpace(cfg.NameServerDomain); domain != "" {
		options = append(options, consumer.WithNameServerDomain(domain))
	}
	if namespace := strings.TrimSpace(cfg.Namespace); namespace != "" {
		options = append(options, consumer.WithNamespace(namespace))
	}
	options = append(options, consumer.WithVIPChannel(cfg.VIPChannel))
	if strings.TrimSpace(cfg.AccessKey) != "" || strings.TrimSpace(cfg.SecretKey) != "" {
		options = append(options, consumer.WithCredentials(credentials(cfg)))
	}
	return options, nil
}

func resolveNameServers(nameservers []string) ([]string, error) {
	resolved := make([]string, 0, len(nameservers))
	for _, nameserver := range nameservers {
		host, port, err := net.SplitHostPort(nameserver)
		if err != nil {
			return nil, fmt.Errorf("invalid rocketmq name_server %q: %w", nameserver, err)
		}
		if net.ParseIP(host) != nil {
			resolved = append(resolved, net.JoinHostPort(host, port))
			continue
		}
		ips, err := net.LookupHost(host)
		if err != nil {
			return nil, fmt.Errorf("resolve rocketmq name_server %q: %w", nameserver, err)
		}
		if len(ips) == 0 {
			return nil, fmt.Errorf("resolve rocketmq name_server %q: empty result", nameserver)
		}
		resolved = append(resolved, net.JoinHostPort(ips[0], port))
	}
	return resolved, nil
}

func credentials(cfg EndpointConfig) primitive.Credentials {
	return primitive.Credentials{
		AccessKey:     strings.TrimSpace(cfg.AccessKey),
		SecretKey:     strings.TrimSpace(cfg.SecretKey),
		SecurityToken: strings.TrimSpace(cfg.SecurityToken),
	}
}

func normalizeStringSlice(input []string) []string {
	out := make([]string, 0, len(input))
	for _, value := range input {
		if value = strings.TrimSpace(value); value != "" {
			out = append(out, value)
		}
	}
	return out
}
