package mq

import (
	"context"
	"fmt"
	"sync"
	"time"

	paho "github.com/eclipse/paho.mqtt.golang"
)

type MQTT struct {
	client paho.Client
	mu     sync.RWMutex
	closed bool
}

func NewMQTT(cfg Config) (*MQTT, error) {
	if !cfg.Enabled {
		return &MQTT{}, nil
	}
	if cfg.Broker == "" {
		return nil, fmt.Errorf("mqtt broker is empty")
	}
	if cfg.KeepAlive <= 0 {
		cfg.KeepAlive = 30 * time.Second
	}
	if cfg.ConnectTimeout <= 0 {
		cfg.ConnectTimeout = 10 * time.Second
	}
	opts := paho.NewClientOptions().AddBroker(cfg.Broker).SetClientID(cfg.ClientID).SetUsername(cfg.Username).SetPassword(cfg.Password)
	opts.SetKeepAlive(cfg.KeepAlive).SetCleanSession(cfg.CleanSession).SetConnectTimeout(cfg.ConnectTimeout).SetAutoReconnect(true)
	opts.SetResumeSubs(true).SetOrderMatters(false)
	c := &MQTT{}
	token := paho.NewClient(opts)
	connect := token.Connect()
	if !connect.WaitTimeout(cfg.ConnectTimeout) {
		return nil, fmt.Errorf("connect mqtt: %w", ErrUnavailable)
	}
	if err := connect.Error(); err != nil {
		return nil, err
	}
	c.client = token
	return c, nil
}

func (c *MQTT) Publish(ctx context.Context, topic string, payload []byte, qos byte, retain bool) error {
	if topic == "" {
		return fmt.Errorf("mqtt topic is empty")
	}
	if qos > ExactlyOnce {
		return fmt.Errorf("invalid mqtt qos %d", qos)
	}
	if c == nil || c.client == nil || !c.client.IsConnected() {
		return ErrUnavailable
	}
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}
	return waitToken(ctx, c.client.Publish(topic, qos, retain, payload))
}

func (c *MQTT) Subscribe(ctx context.Context, topic string, qos byte, handler Handler) error {
	if topic == "" {
		return fmt.Errorf("mqtt topic is empty")
	}
	if qos > ExactlyOnce {
		return fmt.Errorf("invalid mqtt qos %d", qos)
	}
	if c == nil || c.client == nil || !c.client.IsConnected() {
		return ErrUnavailable
	}
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}
	if handler == nil {
		return fmt.Errorf("mqtt handler is nil")
	}
	return waitToken(ctx, c.client.Subscribe(topic, qos, func(_ paho.Client, msg paho.Message) {
		handler(context.Background(), Message{Topic: msg.Topic(), Payload: append([]byte(nil), msg.Payload()...), QoS: msg.Qos(), Retain: msg.Retained()})
	}))
}

func (c *MQTT) Unsubscribe(ctx context.Context, topics ...string) error {
	if len(topics) == 0 {
		return fmt.Errorf("mqtt topics are empty")
	}
	if c == nil || c.client == nil || !c.client.IsConnected() {
		return ErrUnavailable
	}
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}
	return waitToken(ctx, c.client.Unsubscribe(topics...))
}

func waitToken(ctx context.Context, token paho.Token) error {
	if token == nil {
		return ErrUnavailable
	}
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-token.Done():
		return token.Error()
	}
}
func (c *MQTT) Connected() bool { return c != nil && c.client != nil && c.client.IsConnected() }
func (c *MQTT) Close() error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed {
		return nil
	}
	c.closed = true
	if c.client != nil && c.client.IsConnected() {
		c.client.Disconnect(250)
	}
	return nil
}
