/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package pulsar

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

	"github.com/apache/pulsar-client-go/pulsar"
	lru "github.com/hashicorp/golang-lru"

	"github.com/dapr/components-contrib/pubsub"
	"github.com/dapr/kit/logger"
)

const (
	host                    = "host"
	consumerID              = "consumerID"
	enableTLS               = "enableTLS"
	deliverAt               = "deliverAt"
	deliverAfter            = "deliverAfter"
	disableBatching         = "disableBatching"
	batchingMaxPublishDelay = "batchingMaxPublishDelay"
	batchingMaxSize         = "batchingMaxSize"
	batchingMaxMessages     = "batchingMaxMessages"
	tenant                  = "tenant"
	namespace               = "namespace"
	persistent              = "persistent"
	redeliveryDelay         = "redeliveryDelay"

	defaultTenant     = "public"
	defaultNamespace  = "default"
	cachedNumProducer = 10
	pulsarPrefix      = "pulsar://"
	pulsarToken       = "token"
	// topicFormat is the format for pulsar, which have a well-defined structure: {persistent|non-persistent}://tenant/namespace/topic,
	// see https://pulsar.apache.org/docs/en/concepts-messaging/#topics for details.
	topicFormat      = "%s://%s/%s/%s"
	persistentStr    = "persistent"
	nonPersistentStr = "non-persistent"

	// defaultBatchingMaxPublishDelay init default for maximum delay to batch messages.
	defaultBatchingMaxPublishDelay = 10 * time.Millisecond
	// defaultMaxMessages init default num of entries in per batch.
	defaultMaxMessages = 1000
	// defaultMaxBatchSize init default for maximum number of bytes per batch.
	defaultMaxBatchSize = 128 * 1024
	// defaultRedeliveryDelay init default for redelivery delay.
	defaultRedeliveryDelay = 30 * time.Second
)

type Pulsar struct {
	logger   logger.Logger
	client   pulsar.Client
	metadata pulsarMetadata

	publishCtx    context.Context
	publishCancel context.CancelFunc
	cache         *lru.Cache
}

func NewPulsar(l logger.Logger) pubsub.PubSub {
	return &Pulsar{logger: l}
}

func parsePulsarMetadata(meta pubsub.Metadata) (*pulsarMetadata, error) {
	m := pulsarMetadata{Persistent: true, Tenant: defaultTenant, Namespace: defaultNamespace}
	m.ConsumerID = meta.Properties[consumerID]

	if val, ok := meta.Properties[host]; ok && val != "" {
		m.Host = val
	} else {
		return nil, errors.New("pulsar error: missing pulsar host")
	}
	if val, ok := meta.Properties[enableTLS]; ok && val != "" {
		tls, err := strconv.ParseBool(val)
		if err != nil {
			return nil, errors.New("pulsar error: invalid value for enableTLS")
		}
		m.EnableTLS = tls
	}
	// DisableBatching is defaultly batching.
	m.DisableBatching = false
	if val, ok := meta.Properties[disableBatching]; ok {
		disableBatching, err := strconv.ParseBool(val)
		if err != nil {
			return nil, errors.New("pulsar error: invalid value for disableBatching")
		}
		m.DisableBatching = disableBatching
	}
	m.BatchingMaxPublishDelay = defaultBatchingMaxPublishDelay
	if val, ok := meta.Properties[batchingMaxPublishDelay]; ok {
		batchingMaxPublishDelay, err := formatDuration(val)
		if err != nil {
			return nil, errors.New("pulsar error: invalid value for batchingMaxPublishDelay")
		}
		m.BatchingMaxPublishDelay = batchingMaxPublishDelay
	}
	m.BatchingMaxMessages = defaultMaxMessages
	if val, ok := meta.Properties[batchingMaxMessages]; ok {
		batchingMaxMessages, err := strconv.ParseUint(val, 10, 64)
		if err != nil {
			return nil, errors.New("pulsar error: invalid value for batchingMaxMessages")
		}
		m.BatchingMaxMessages = uint(batchingMaxMessages)
	}
	m.BatchingMaxSize = defaultMaxBatchSize
	if val, ok := meta.Properties[batchingMaxSize]; ok {
		batchingMaxSize, err := strconv.ParseUint(val, 10, 64)
		if err != nil {
			return nil, errors.New("pulsar error: invalid value for batchingMaxSize")
		}
		m.BatchingMaxSize = uint(batchingMaxSize)
	}
	m.RedeliveryDelay = defaultRedeliveryDelay
	if val, ok := meta.Properties[redeliveryDelay]; ok {
		redeliveryDelay, err := formatDuration(val)
		if err != nil {
			return nil, errors.New("pulsar error: invalid value for redeliveryDelay")
		}
		m.RedeliveryDelay = redeliveryDelay
	}
	if val, ok := meta.Properties[persistent]; ok && val != "" {
		per, err := strconv.ParseBool(val)
		if err != nil {
			return nil, errors.New("pulsar error: invalid value for persistent")
		}
		m.Persistent = per
	}
	if val, ok := meta.Properties[tenant]; ok && val != "" {
		m.Tenant = val
	}
	if val, ok := meta.Properties[namespace]; ok && val != "" {
		m.Namespace = val
	}
	if val, ok := meta.Properties[pulsarToken]; ok && val != "" {
		m.Token = val
	}

	return &m, nil
}

func (p *Pulsar) Init(metadata pubsub.Metadata) error {
	m, err := parsePulsarMetadata(metadata)
	if err != nil {
		return err
	}
	pulsarURL := m.Host
	if !strings.HasPrefix(m.Host, "http://") &&
		!strings.HasPrefix(m.Host, "https://") {
		pulsarURL = fmt.Sprintf("%s%s", pulsarPrefix, m.Host)
	}
	options := pulsar.ClientOptions{
		URL:                        pulsarURL,
		OperationTimeout:           30 * time.Second,
		ConnectionTimeout:          30 * time.Second,
		TLSAllowInsecureConnection: !m.EnableTLS,
	}
	if m.Token != "" {
		options.Authentication = pulsar.NewAuthenticationToken(m.Token)
	}
	client, err := pulsar.NewClient(options)
	if err != nil {
		return fmt.Errorf("could not instantiate pulsar client: %v", err)
	}

	// initialize lru cache with size 10
	// TODO: make this number configurable in pulsar metadata
	c, err := lru.NewWithEvict(cachedNumProducer, func(k interface{}, v interface{}) {
		producer := v.(pulsar.Producer)
		if producer != nil {
			producer.Close()
		}
	})
	if err != nil {
		return fmt.Errorf("could not initialize pulsar lru cache for publisher")
	}
	p.cache = c
	defer p.cache.Purge()

	p.publishCtx, p.publishCancel = context.WithCancel(context.Background())

	p.client = client
	p.metadata = *m

	return nil
}

func (p *Pulsar) Publish(req *pubsub.PublishRequest) error {
	var (
		producer pulsar.Producer
		msg      *pulsar.ProducerMessage
		err      error
	)
	topic := p.formatTopic(req.Topic)
	cache, _ := p.cache.Get(topic)
	if cache == nil {
		p.logger.Debugf("creating producer for topic %s, full topic name in pulsar is %s", req.Topic, topic)
		producer, err = p.client.CreateProducer(pulsar.ProducerOptions{
			Topic:                   topic,
			DisableBatching:         p.metadata.DisableBatching,
			BatchingMaxPublishDelay: p.metadata.BatchingMaxPublishDelay,
			BatchingMaxMessages:     p.metadata.BatchingMaxMessages,
			BatchingMaxSize:         p.metadata.BatchingMaxSize,
		})
		if err != nil {
			return err
		}

		p.cache.Add(topic, producer)
	} else {
		producer = cache.(pulsar.Producer)
	}

	msg, err = parsePublishMetadata(req)
	if err != nil {
		return err
	}
	if _, err = producer.Send(p.publishCtx, msg); err != nil {
		return err
	}

	return nil
}

// parsePublishMetadata parse publish metadata.
func parsePublishMetadata(req *pubsub.PublishRequest) (
	msg *pulsar.ProducerMessage, err error,
) {
	msg = &pulsar.ProducerMessage{
		Payload: req.Data,
	}
	if val, ok := req.Metadata[deliverAt]; ok {
		msg.DeliverAt, err = time.Parse(time.RFC3339, val)
		if err != nil {
			return nil, err
		}
	}
	if val, ok := req.Metadata[deliverAfter]; ok {
		msg.DeliverAfter, err = time.ParseDuration(val)
		if err != nil {
			return nil, err
		}
	}

	return
}

func (p *Pulsar) Subscribe(ctx context.Context, req pubsub.SubscribeRequest, handler pubsub.Handler) error {
	channel := make(chan pulsar.ConsumerMessage, 100)

	topic := p.formatTopic(req.Topic)
	options := pulsar.ConsumerOptions{
		Topic:               topic,
		SubscriptionName:    p.metadata.ConsumerID,
		Type:                pulsar.Shared,
		MessageChannel:      channel,
		NackRedeliveryDelay: p.metadata.RedeliveryDelay,
	}

	consumer, err := p.client.Subscribe(options)
	if err != nil {
		p.logger.Debugf("Could not subscribe to %s, full topic name in pulsar is %s", req.Topic, topic)
		return err
	}

	go p.listenMessage(ctx, req.Topic, consumer, handler)

	return nil
}

func (p *Pulsar) listenMessage(ctx context.Context, originTopic string, consumer pulsar.Consumer, handler pubsub.Handler) {
	defer consumer.Close()

	var err error
	for {
		select {
		case msg := <-consumer.Chan():
			err = p.handleMessage(ctx, originTopic, msg, handler)
			if err != nil && !errors.Is(err, context.Canceled) {
				p.logger.Errorf("Error processing message: %s/%#v [key=%s]: %v", msg.Topic(), msg.ID(), msg.Key(), err)
			}

		case <-ctx.Done():
			p.logger.Errorf("Subscription context done. Closing consumer. Err: %s", ctx.Err())
			return
		}
	}
}

func (p *Pulsar) handleMessage(ctx context.Context, originTopic string, msg pulsar.ConsumerMessage, handler pubsub.Handler) error {
	pubsubMsg := pubsub.NewMessage{
		Data:     msg.Payload(),
		Topic:    originTopic,
		Metadata: msg.Properties(),
	}

	p.logger.Debugf("Processing Pulsar message %s/%#v", msg.Topic(), msg.ID())
	err := handler(ctx, &pubsubMsg)
	if err != nil {
		msg.Nack(msg.Message)
		return err
	}

	msg.Ack(msg.Message)
	return nil
}

func (p *Pulsar) Close() error {
	p.publishCancel()
	for _, k := range p.cache.Keys() {
		producer, _ := p.cache.Peek(k)
		if producer != nil {
			p.logger.Debugf("closing producer for topic %s", k)
			producer.(pulsar.Producer).Close()
		}
	}
	p.client.Close()

	return nil
}

func (p *Pulsar) Features() []pubsub.Feature {
	return nil
}

// formatTopic formats the topic into pulsar's structure with tenant and namespace.
func (p *Pulsar) formatTopic(topic string) string {
	persist := persistentStr
	if !p.metadata.Persistent {
		persist = nonPersistentStr
	}
	return fmt.Sprintf(topicFormat, persist, p.metadata.Tenant, p.metadata.Namespace, topic)
}

func formatDuration(durationString string) (time.Duration, error) {
	if val, err := strconv.Atoi(durationString); err == nil {
		return time.Duration(val) * time.Millisecond, nil
	}

	// Convert it by parsing
	d, err := time.ParseDuration(durationString)

	return d, err
}
