// Package nats provides a NATS registry using broadcast queries
package nats

import (
	"context"
	"encoding/json"
	"strings"
	"sync"
	"time"

	"github.com/nats-io/nats.go"
	"go-micro.dev/v6/registry"
)

type natsRegistry struct {
	addrs          []string
	opts           registry.Options
	nopts          nats.Options
	queryTopic     string
	watchTopic     string
	registerAction string

	sync.RWMutex
	conn      *nats.Conn
	services  map[string][]*registry.Service
	listeners map[string]chan bool
}

var (
	defaultQueryTopic     = "micro.nats.query"
	defaultWatchTopic     = "micro.nats.watch"
	defaultRegisterAction = "create"
)

func configure(n *natsRegistry, opts ...registry.Option) error {
	for _, o := range opts {
		o(&n.opts)
	}

	natsOptions := nats.GetDefaultOptions()
	if n, ok := n.opts.Context.Value(optionsKey{}).(nats.Options); ok {
		natsOptions = n
	}

	queryTopic := defaultQueryTopic
	if qt, ok := n.opts.Context.Value(queryTopicKey{}).(string); ok {
		queryTopic = qt
	}

	watchTopic := defaultWatchTopic
	if wt, ok := n.opts.Context.Value(watchTopicKey{}).(string); ok {
		watchTopic = wt
	}

	registerAction := defaultRegisterAction
	if ra, ok := n.opts.Context.Value(registerActionKey{}).(string); ok {
		registerAction = ra
	}

	// Options have higher priority than nats.Options
	// only if Addrs, Secure or TLSConfig were not set through a Option
	// we read them from nats.Option
	if len(n.opts.Addrs) == 0 {
		n.opts.Addrs = natsOptions.Servers
	}

	if !n.opts.Secure {
		n.opts.Secure = natsOptions.Secure
	}

	if n.opts.TLSConfig == nil {
		n.opts.TLSConfig = natsOptions.TLSConfig
	}

	// check & add nats:// prefix (this makes also sure that the addresses
	// stored in natsaddrs and n.opts.Addrs are identical)
	n.opts.Addrs = setAddrs(n.opts.Addrs)

	n.addrs = n.opts.Addrs
	n.nopts = natsOptions
	n.queryTopic = queryTopic
	n.watchTopic = watchTopic
	n.registerAction = registerAction

	return nil
}

func setAddrs(addrs []string) []string {
	var cAddrs []string
	for _, addr := range addrs {
		if len(addr) == 0 {
			continue
		}
		if !strings.HasPrefix(addr, "nats://") {
			addr = "nats://" + addr
		}
		cAddrs = append(cAddrs, addr)
	}
	if len(cAddrs) == 0 {
		cAddrs = []string{nats.DefaultURL}
	}
	return cAddrs
}

func (n *natsRegistry) newConn() (*nats.Conn, error) {
	opts := n.nopts
	opts.Servers = n.addrs
	opts.Secure = n.opts.Secure
	opts.TLSConfig = n.opts.TLSConfig

	// secure might not be set
	if opts.TLSConfig != nil {
		opts.Secure = true
	}

	return opts.Connect()
}

func (n *natsRegistry) getConn() (*nats.Conn, error) {
	n.Lock()
	defer n.Unlock()

	if n.conn != nil {
		return n.conn, nil
	}

	c, err := n.newConn()
	if err != nil {
		return nil, err
	}
	n.conn = c

	return n.conn, nil
}

func (n *natsRegistry) register(s *registry.Service) error {
	conn, err := n.getConn()
	if err != nil {
		return err
	}

	n.Lock()
	defer n.Unlock()

	// cache service
	n.services[s.Name] = addServices(n.services[s.Name], cp([]*registry.Service{s}))

	// create query listener
	if n.listeners[s.Name] == nil {
		listener := make(chan bool)

		// create a subscriber that responds to queries
		sub, err := conn.Subscribe(n.queryTopic, func(m *nats.Msg) {
			var result *registry.Result

			if err := json.Unmarshal(m.Data, &result); err != nil {
				return
			}

			var services []*registry.Service

			switch result.Action {
			// is this a get query and we own the service?
			case "get":
				if result.Service.Name != s.Name {
					return
				}
				n.RLock()
				services = cp(n.services[s.Name])
				n.RUnlock()
			// it's a list request, but we're still only a
			// subscriber for this service... so just get this service
			// totally suboptimal
			case "list":
				n.RLock()
				services = cp(n.services[s.Name])
				n.RUnlock()
			default:
				// does not match
				return
			}

			// respond to query
			for _, service := range services {
				b, err := json.Marshal(service)
				if err != nil {
					continue
				}
				_ = conn.Publish(m.Reply, b)
			}
		})
		if err != nil {
			return err
		}

		// Unsubscribe if we're told to do so
		go func() {
			<-listener
			_ = sub.Unsubscribe()
		}()

		n.listeners[s.Name] = listener
	}

	return nil
}

func (n *natsRegistry) deregister(s *registry.Service) error {
	n.Lock()
	defer n.Unlock()

	services := delServices(n.services[s.Name], cp([]*registry.Service{s}))
	if len(services) > 0 {
		n.services[s.Name] = services
		return nil
	}

	// delete cached service
	delete(n.services, s.Name)

	// delete query listener
	if listener, lexists := n.listeners[s.Name]; lexists {
		close(listener)
		delete(n.listeners, s.Name)
	}

	return nil
}

func (n *natsRegistry) query(s string, quorum int) ([]*registry.Service, error) {
	conn, err := n.getConn()
	if err != nil {
		return nil, err
	}

	var action string
	var service *registry.Service

	if len(s) > 0 {
		action = "get"
		service = &registry.Service{Name: s}
	} else {
		action = "list"
	}

	inbox := nats.NewInbox()

	response := make(chan *registry.Service, 10)

	sub, err := conn.Subscribe(inbox, func(m *nats.Msg) {
		var service *registry.Service
		if err := json.Unmarshal(m.Data, &service); err != nil {
			return
		}
		select {
		case response <- service:
		case <-time.After(n.opts.Timeout):
		}
	})
	if err != nil {
		return nil, err
	}
	defer func() { _ = sub.Unsubscribe() }()

	b, err := json.Marshal(&registry.Result{Action: action, Service: service})
	if err != nil {
		return nil, err
	}

	if err := conn.PublishMsg(&nats.Msg{
		Subject: n.queryTopic,
		Reply:   inbox,
		Data:    b,
	}); err != nil {
		return nil, err
	}

	timeoutChan := time.After(n.opts.Timeout)

	serviceMap := make(map[string]*registry.Service)

loop:
	for {
		select {
		case service := <-response:
			key := service.Name + "-" + service.Version
			srv, ok := serviceMap[key]
			if ok {
				srv.Nodes = append(srv.Nodes, service.Nodes...)
				serviceMap[key] = srv
			} else {
				serviceMap[key] = service
			}

			if quorum > 0 && len(serviceMap[key].Nodes) >= quorum {
				break loop
			}
		case <-timeoutChan:
			break loop
		}
	}

	var services []*registry.Service
	for _, service := range serviceMap {
		services = append(services, service)
	}
	return services, nil
}

func (n *natsRegistry) Init(opts ...registry.Option) error {
	return configure(n, opts...)
}

func (n *natsRegistry) Options() registry.Options {
	return n.opts
}

func (n *natsRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
	if err := n.register(s); err != nil {
		return err
	}

	conn, err := n.getConn()
	if err != nil {
		return err
	}

	b, err := json.Marshal(&registry.Result{Action: n.registerAction, Service: s})
	if err != nil {
		return err
	}

	return conn.Publish(n.watchTopic, b)
}

func (n *natsRegistry) Deregister(s *registry.Service, opts ...registry.DeregisterOption) error {
	if err := n.deregister(s); err != nil {
		return err
	}

	conn, err := n.getConn()
	if err != nil {
		return err
	}

	b, err := json.Marshal(&registry.Result{Action: "delete", Service: s})
	if err != nil {
		return err
	}
	return conn.Publish(n.watchTopic, b)
}

func (n *natsRegistry) GetService(s string, opts ...registry.GetOption) ([]*registry.Service, error) {
	services, err := n.query(s, getQuorum(n.opts))
	if err != nil {
		return nil, err
	}
	return services, nil
}

func (n *natsRegistry) ListServices(opts ...registry.ListOption) ([]*registry.Service, error) {
	s, err := n.query("", 0)
	if err != nil {
		return nil, err
	}

	var services []*registry.Service
	serviceMap := make(map[string]*registry.Service)

	for _, v := range s {
		serviceMap[v.Name] = &registry.Service{Name: v.Name, Version: v.Version}
	}

	for _, v := range serviceMap {
		services = append(services, v)
	}

	return services, nil
}

func (n *natsRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
	conn, err := n.getConn()
	if err != nil {
		return nil, err
	}

	sub, err := conn.SubscribeSync(n.watchTopic)
	if err != nil {
		return nil, err
	}

	var wo registry.WatchOptions
	for _, o := range opts {
		o(&wo)
	}

	return &natsWatcher{sub, wo}, nil
}

func (n *natsRegistry) String() string {
	return "nats"
}

func NewNatsRegistry(opts ...registry.Option) registry.Registry {
	options := registry.Options{
		Timeout: time.Millisecond * 100,
		Context: context.Background(),
	}

	n := &natsRegistry{
		opts:      options,
		services:  make(map[string][]*registry.Service),
		listeners: make(map[string]chan bool),
	}
	_ = configure(n, opts...)
	return n
}
