package mq

import (
	"context"
	"log/slog"
	"sync"
	"time"

	"kra/app/system/internal/conf"
	"kra/pkg/mq"
)

// Reloadable follows the runtime snapshot and keeps a single shared EMQX
// connection for all modules in this process.
type Reloadable struct {
	mu            sync.RWMutex
	opMu          sync.Mutex
	current       mq.Client
	subscriptions map[string]subscription
	stop          func()
	logger        *slog.Logger
	closed        bool
}

type subscription struct {
	qos     byte
	handler mq.Handler
}

func New(runtime *conf.Runtime, logger *slog.Logger) (*Reloadable, func(), error) {
	if logger == nil {
		logger = slog.Default()
	}
	r := &Reloadable{logger: logger, subscriptions: make(map[string]subscription)}
	if runtime != nil {
		var config *conf.AdminBackend_MQ
		if admin := runtime.Admin(); admin != nil {
			config = admin.GetMq()
		}
		r.replace(config)
		r.stop = runtime.Subscribe(func(_ *conf.Data, admin *conf.AdminBackend) {
			if admin != nil {
				r.replace(admin.GetMq())
			}
		})
	}
	cleanup := func() {
		if r.stop != nil {
			r.stop()
		}
		_ = r.Close()
	}
	return r, cleanup, nil
}

func (r *Reloadable) replace(config *conf.AdminBackend_MQ) {
	r.opMu.Lock()
	defer r.opMu.Unlock()
	if r.closed {
		return
	}
	if config == nil {
		config = &conf.AdminBackend_MQ{}
	}
	cfg := mq.Config{Enabled: config.Enabled, Broker: config.Broker, ClientID: config.ClientId, Username: config.Username, Password: config.Password, CleanSession: config.CleanSession}
	if config.KeepAlive > 0 {
		cfg.KeepAlive = time.Duration(config.KeepAlive) * time.Second
	}
	if config.ConnectTimeout > 0 {
		cfg.ConnectTimeout = time.Duration(config.ConnectTimeout) * time.Second
	}
	client, err := mq.NewMQTT(cfg)
	if err != nil {
		r.logger.Warn("emqx unavailable", "mod", "mq", "error", err)
		return
	}
	if config.Enabled {
		if err = r.restoreSubscriptions(context.Background(), client); err != nil {
			_ = client.Close()
			r.logger.Warn("restore emqx subscriptions failed", "mod", "mq", "error", err)
			return
		}
	}
	r.mu.Lock()
	old := r.current
	r.current = client
	r.mu.Unlock()
	if old != nil {
		_ = old.Close()
	}
}

func (r *Reloadable) restoreSubscriptions(ctx context.Context, client mq.Client) error {
	for topic, item := range r.subscriptions {
		if err := client.Subscribe(ctx, topic, item.qos, item.handler); err != nil {
			return err
		}
	}
	return nil
}

func (r *Reloadable) client() mq.Client { r.mu.RLock(); defer r.mu.RUnlock(); return r.current }
func (r *Reloadable) Publish(ctx context.Context, topic string, payload []byte, qos byte, retain bool) error {
	c := r.client()
	if c == nil {
		return mq.ErrUnavailable
	}
	return c.Publish(ctx, topic, payload, qos, retain)
}
func (r *Reloadable) Subscribe(ctx context.Context, topic string, qos byte, handler mq.Handler) error {
	r.opMu.Lock()
	defer r.opMu.Unlock()
	c := r.client()
	if c == nil {
		return mq.ErrUnavailable
	}
	if err := c.Subscribe(ctx, topic, qos, handler); err != nil {
		return err
	}
	r.subscriptions[topic] = subscription{qos: qos, handler: handler}
	return nil
}
func (r *Reloadable) Unsubscribe(ctx context.Context, topics ...string) error {
	r.opMu.Lock()
	defer r.opMu.Unlock()
	c := r.client()
	if c == nil {
		return mq.ErrUnavailable
	}
	if err := c.Unsubscribe(ctx, topics...); err != nil {
		return err
	}
	for _, topic := range topics {
		delete(r.subscriptions, topic)
	}
	return nil
}
func (r *Reloadable) Connected() bool { c := r.client(); return c != nil && c.Connected() }
func (r *Reloadable) Close() error {
	r.opMu.Lock()
	defer r.opMu.Unlock()
	if r.closed {
		return nil
	}
	r.closed = true
	r.mu.Lock()
	old := r.current
	r.current = nil
	r.mu.Unlock()
	if old != nil {
		return old.Close()
	}
	return nil
}
