package rabbit

import (
	"github.com/streadway/amqp"
	"time"
)

type Config struct {
	DSN            *string       `mapstructure:"dsn" json:"dsn" yaml:"dsn"`
	Host           *string       `mapstructure:"host" json:"host" yaml:"host"`
	Port           *int          `mapstructure:"port" json:"port" yaml:"port"`
	Username       *string       `mapstructure:"username" json:"username" yaml:"username"`
	Password       *string       `mapstructure:"password" json:"password" yaml:"password"`
	Vhost          *string       `mapstructure:"vhost" json:"vhost" yaml:"vhost"`
	SSL            *bool         `mapstructure:"ssl" json:"ssl" yaml:"ssl"`
	ReconnectDelay time.Duration `mapstructure:"reconnect_delay" json:"reconnect_delay" yaml:"reconnect_delay"`
	Exchanges      Exchanges     `mapstructure:"exchanges" json:"exchanges" yaml:"exchanges"`
	Queues         Queues        `mapstructure:"queues" json:"queues" yaml:"queues"`
	Producers      Producers     `mapstructure:"producers" json:"producers" yaml:"producers"`
	Consumers      Consumers     `mapstructure:"consumers" json:"consumers" yaml:"consumers"`
	Bindings       Bindings      `mapstructure:"bindings" json:"bindings" yaml:"bindings"`
}

func (config Config) normalize() {
	config.Exchanges.normalize()
	config.Queues.normalize()
	config.Producers.normalize()
	config.Consumers.normalize()
}

func (config Config) GetDSN() string {
	if config.DSN != nil {
		return *config.DSN
	}

	host := config.hostOrDefault()
	port := config.portOrDefault()
	username := config.usernameOrDefault()
	password := config.passwordOrDefault()
	vhost := config.vhostOrDefault()
	scheme := "amqp"
	if config.SSL != nil && *config.SSL == true {
		scheme = "amqps"
	}

	url := amqp.URI{
		Username: username,
		Password: password,
		Vhost:    vhost,
		Port:     port,
		Host:     host,
		Scheme:   scheme,
	}

	return url.String()
}

func (config Config) hostOrDefault() string {
	if config.Host != nil {
		return *config.Host
	}

	return "localhost"
}

func (config Config) usernameOrDefault() string {
	if config.Username != nil {
		return *config.Username
	}

	return "guest"
}

func (config Config) passwordOrDefault() string {
	if config.Password != nil {
		return *config.Password
	}

	return "guest"
}

func (config Config) vhostOrDefault() string {
	if config.Vhost != nil {
		return *config.Vhost
	}

	return ""
}

func (config Config) portOrDefault() int {
	if config.Port != nil {
		return *config.Port
	}

	return 5672
}

type Exchanges []ExchangeConfig

func (exchanges Exchanges) normalize() {
	for _, exchange := range exchanges {
		exchange.normalize()
	}
}

type ExchangeConfig struct {
	Name    string  `mapstructure:"name" json:"name" yaml:"name"`
	Type    string  `mapstructure:"type" json:"type" yaml:"type"`
	Options Options `mapstructure:"options" json:"options" yaml:"options"`
}

func (config ExchangeConfig) normalize() {
	config.Options.normalizeKeys()
}

type Bindings []BindingConfig

type BindingConfig struct {
	Destination string  `mapstructure:"destination" json:"destination" yaml:"destination"`
	Source      string  `mapstructure:"source" json:"source" yaml:"source"`
	RoutingKey  string  `mapstructure:"routing_key" json:"routing_key" yaml:"routing_key"`
	Options     Options `mapstructure:"options" json:"options" yaml:"options"`
}

type Queues []QueueConfig

func (queues Queues) normalize() {
	for _, queue := range queues {
		queue.normalize()
	}
}

type QueueConfig struct {
	Exchange       string  `mapstructure:"exchange" json:"exchange" yaml:"exchange"`
	Name           string  `mapstructure:"name" json:"name" yaml:"name"`
	RoutingKey     string  `mapstructure:"routing_key" json:"routing_key" yaml:"routing_key"`
	BindingOptions Options `mapstructure:"binding_options" json:"binding_options" yaml:"binding_options"`
	Options        Options `mapstructure:"options" json:"options" yaml:"options"`
}

func (config QueueConfig) normalize() {
	config.BindingOptions.normalizeKeys()
	config.Options.normalizeKeys()
	config.Options.buildArgs()
}

type Producers []ProducerConfig

func (producers Producers) normalize() {
	for _, producer := range producers {
		producer.normalize()
	}
}

type ProducerConfig struct {
	BufferSize int     `mapstructure:"buffer_size" json:"buffer_size" yaml:"buffer_size"`
	Exchange   string  `mapstructure:"exchange" json:"exchange" yaml:"exchange"`
	Name       string  `mapstructure:"name" json:"name" yaml:"name"`
	RoutingKey string  `mapstructure:"routing_key" json:"routing_key" yaml:"routing_key"`
	Options    Options `mapstructure:"options" json:"options" yaml:"options"`
}

func (config ProducerConfig) normalize() {
	config.Options.normalizeKeys()
}

type Consumers []ConsumerConfig

func (consumers Consumers) normalize() {
	for _, consumer := range consumers {
		consumer.normalize()
	}
}

type ConsumerConfig struct {
	Name          string  `mapstructure:"name" json:"name" yaml:"name"`
	Queue         string  `mapstructure:"queue" json:"queue" yaml:"queue"`
	Workers       int     `mapstructure:"workers" json:"workers" yaml:"workers"`
	Options       Options `mapstructure:"options" json:"options" yaml:"options"`
	PrefetchCount int     `mapstructure:"prefetch_count" json:"prefetch_count" yaml:"prefetch_count"`
	PrefetchSize  int     `mapstructure:"prefetch_size" json:"prefetch_size" yaml:"prefetch_size"`
}

func (config ConsumerConfig) normalize() {
	config.Options.normalizeKeys()
}

type Options map[string]interface{}

var capitalizationMap = map[string]string{
	"autodelete":       "autoDelete",
	"auto_delete":      "autoDelete",
	"contentencoding":  "contentEncoding",
	"content_encoding": "contentEncoding",
	"contenttype":      "contentType",
	"content_type":     "contentType",
	"deliverymode":     "deliveryMode",
	"delivery_mode":    "deliveryMode",
	"noack":            "noAck",
	"no_ack":           "noAck",
	"nolocal":          "noLocal",
	"no_local":         "noLocal",
	"nowait":           "noWait",
	"no_wait":          "noWait",
}

func (options Options) normalizeKeys() {
	for name, value := range options {
		if correctName, needFix := capitalizationMap[name]; needFix {
			delete(options, name)
			options[correctName] = value
		}
	}
}

func (options Options) buildArgs() {
	if _, ok := options["args"]; ok {
		args := amqp.Table{}

		for k, v := range options["args"].(map[interface{}]interface{}) {
			args[k.(string)] = v
		}

		options["args"] = args
	}
}
package rabbit

import (
	"github.com/streadway/amqp"
	"time"
)

// Config describes all available options for amqp connection creation.
type Config struct {
	DSN            *string       `mapstructure:"dsn" json:"dsn" yaml:"dsn"`
	Host           *string       `mapstructure:"host" json:"host" yaml:"host"`
	Port           *int          `mapstructure:"port" json:"port" yaml:"port"`
	Username       *string       `mapstructure:"username" json:"username" yaml:"username"`
	Password       *string       `mapstructure:"password" json:"password" yaml:"password"`
	Vhost          *string       `mapstructure:"vhost" json:"vhost" yaml:"vhost"`
	SSL            *bool         `mapstructure:"ssl" json:"ssl" yaml:"ssl"`
	ReconnectDelay time.Duration `mapstructure:"reconnect_delay" json:"reconnect_delay" yaml:"reconnect_delay"`
	Exchanges      Exchanges     `mapstructure:"exchanges" json:"exchanges" yaml:"exchanges"`
	Queues         Queues        `mapstructure:"queues" json:"queues" yaml:"queues"`
	Producers      Producers     `mapstructure:"producers" json:"producers" yaml:"producers"`
	Consumers      Consumers     `mapstructure:"consumers" json:"consumers" yaml:"consumers"`
	Bindings       Bindings      `mapstructure:"bindings" json:"bindings" yaml:"bindings"`
}

// Traverses the amqpConfig tree and fixes option keys name.
func (config Config) normalize() {
	config.Exchanges.normalize()
	config.Queues.normalize()
	config.Producers.normalize()
	config.Consumers.normalize()
}

func (config Config) GetDSN() string {
	if config.DSN != nil {
		return *config.DSN
	}

	host := config.hostOrDefault()
	port := config.portOrDefault()
	username := config.usernameOrDefault()
	password := config.passwordOrDefault()
	vhost := config.vhostOrDefault()
	scheme := "amqp"
	if config.SSL != nil && *config.SSL == true {
		scheme = "amqps"
	}

	url := amqp.URI{
		Username: username,
		Password: password,
		Vhost:    vhost,
		Port:     port,
		Host:     host,
		Scheme:   scheme,
	}

	return url.String()
}

func (config Config) hostOrDefault() string {
	if config.Host != nil {
		return *config.Host
	}

	return "localhost"
}

func (config Config) usernameOrDefault() string {
	if config.Username != nil {
		return *config.Username
	}

	return "guest"
}

func (config Config) passwordOrDefault() string {
	if config.Password != nil {
		return *config.Password
	}

	return "guest"
}

func (config Config) vhostOrDefault() string {
	if config.Vhost != nil {
		return *config.Vhost
	}

	return ""
}

func (config Config) portOrDefault() int {
	if config.Port != nil {
		return *config.Port
	}

	return 5672
}

// Exchanges describes configuration list for exchanges.
type Exchanges []ExchangeConfig

func (exchanges Exchanges) normalize() {
	for _, exchange := range exchanges {
		exchange.normalize()
	}
}

// ExchangeConfig describes exchange's configuration.
type ExchangeConfig struct {
	Name    string  `mapstructure:"name" json:"name" yaml:"name"`
	Type    string  `mapstructure:"type" json:"type" yaml:"type"`
	Options Options `mapstructure:"options" json:"options" yaml:"options"`
}

func (config ExchangeConfig) normalize() {
	config.Options.normalizeKeys()
}

// Bindings describes configuration list for bindings.
type Bindings []BindingConfig

type BindingConfig struct {
	Destination string  `mapstructure:"destination" json:"destination" yaml:"destination"`
	Source      string  `mapstructure:"source" json:"source" yaml:"source"`
	RoutingKey  string  `mapstructure:"routing_key" json:"routing_key" yaml:"routing_key"`
	Options     Options `mapstructure:"options" json:"options" yaml:"options"`
}

// Queues describes configuration list for queues.
type Queues []QueueConfig

func (queues Queues) normalize() {
	for _, queue := range queues {
		queue.normalize()
	}
}

// QueueConfig describes queue's configuration.
type QueueConfig struct {
	Exchange       string  `mapstructure:"exchange" json:"exchange" yaml:"exchange"`
	Name           string  `mapstructure:"name" json:"name" yaml:"name"`
	RoutingKey     string  `mapstructure:"routing_key" json:"routing_key" yaml:"routing_key"`
	BindingOptions Options `mapstructure:"binding_options" json:"binding_options" yaml:"binding_options"`
	Options        Options `mapstructure:"options" json:"options" yaml:"options"`
}

func (config QueueConfig) normalize() {
	config.BindingOptions.normalizeKeys()
	config.Options.normalizeKeys()
	config.Options.buildArgs()
}

// Producers describes configuration list for publishers.
type Producers []ProducerConfig

func (producers Producers) normalize() {
	for _, producer := range producers {
		producer.normalize()
	}
}

// ProducerConfig describes producer's configuration.
type ProducerConfig struct {
	BufferSize int     `mapstructure:"buffer_size" json:"buffer_size" yaml:"buffer_size"`
	Exchange   string  `mapstructure:"exchange" json:"exchange" yaml:"exchange"`
	Name       string  `mapstructure:"name" json:"name" yaml:"name"`
	RoutingKey string  `mapstructure:"routing_key" json:"routing_key" yaml:"routing_key"`
	Options    Options `mapstructure:"options" json:"options" yaml:"options"`
}

func (config ProducerConfig) normalize() {
	config.Options.normalizeKeys()
}

// Consumers describes configuration list for consumers.
type Consumers []ConsumerConfig

func (consumers Consumers) normalize() {
	for _, consumer := range consumers {
		consumer.normalize()
	}
}

// ConsumerConfig describes consumer's configuration.
type ConsumerConfig struct {
	Name          string  `mapstructure:"name" json:"name" yaml:"name"`
	Queue         string  `mapstructure:"queue" json:"queue" yaml:"queue"`
	Workers       int     `mapstructure:"workers" json:"workers" yaml:"workers"`
	Options       Options `mapstructure:"options" json:"options" yaml:"options"`
	PrefetchCount int     `mapstructure:"prefetch_count" json:"prefetch_count" yaml:"prefetch_count"`
	PrefetchSize  int     `mapstructure:"prefetch_size" json:"prefetch_size" yaml:"prefetch_size"`
}

func (config ConsumerConfig) normalize() {
	config.Options.normalizeKeys()
}

// Options describes optional configuration.
type Options map[string]interface{}

// Map from lowercase option name to the expected name.
var capitalizationMap = map[string]string{
	"autodelete":       "autoDelete",
	"auto_delete":      "autoDelete",
	"contentencoding":  "contentEncoding",
	"content_encoding": "contentEncoding",
	"contenttype":      "contentType",
	"content_type":     "contentType",
	"deliverymode":     "deliveryMode",
	"delivery_mode":    "deliveryMode",
	"noack":            "noAck",
	"no_ack":           "noAck",
	"nolocal":          "noLocal",
	"no_local":         "noLocal",
	"nowait":           "noWait",
	"no_wait":          "noWait",
}

// By default yaml reader unmarshals keys in lowercase,
// but AMQP client looks for keys in camelcase,
// so we're going to fix this issue.
func (options Options) normalizeKeys() {
	for name, value := range options {
		if correctName, needFix := capitalizationMap[name]; needFix {
			delete(options, name)
			options[correctName] = value
		}
	}
}

// Build extra arguments table
func (options Options) buildArgs() {
	if _, ok := options["args"]; ok {
		args := amqp.Table{}

		for k, v := range options["args"].(map[interface{}]interface{}) {
			args[k.(string)] = v
		}

		options["args"] = args
	}
}
