package async

import (
	"encoding/json"
	"fmt"
	blog "log"
	"log/slog"
	"os"
	"path"
	"strconv"
	"time"

	mqtt "github.com/eclipse/paho.mqtt.golang"
	"github.com/golang-jwt/jwt/v5"
	"quantex.com/qfixdpl/src/app"
	"quantex.com/qfixdpl/src/app/version"
	"quantex.com/qfixdpl/src/common/tracerr"
	"quantex.com/qfixdpl/src/domain"
)

const defaultTokenExpireSeconds = 60

type MQTTManager struct {
	client mqtt.Client
	config app.Async
	notify domain.Notifier
}

func New(cfg app.Async, n domain.Notifier) *MQTTManager {
	manager := &MQTTManager{
		config: cfg,
		notify: n,
	}

	manager.client = manager.newClient(version.AppName+strconv.FormatInt(time.Now().Unix(), 10), nil)

	return manager
}

func (m *MQTTManager) Start() {
	token := m.client.Connect()
	if token.Wait() && token.Error() != nil {
		slog.Error("Error trying to connect to broker = " + token.Error().Error())

		return
	}

	slog.Info("MQTT Client successfully created!")
}

//nolint:ireturn,nolintlint // We don't control this
func (m *MQTTManager) newClient(clientID string, onConnectHandler mqtt.OnConnectHandler) mqtt.Client {
	mqtt.ERROR = blog.New(os.Stdout, "", 0)
	clientID = clientID + "_" + strconv.Itoa(time.Now().Nanosecond())
	opts := mqtt.NewClientOptions()

	err := checkMQTTConfig(m.config)
	if err != nil {
		panic(err.Error())
	}

	mqttBkr := fmt.Sprintf("%s://%s", m.config.Protocol, m.config.URL)

	opts.AddBroker(mqttBkr).SetClientID(clientID)
	opts.SetAutoReconnect(true)
	opts.SetKeepAlive(5 * time.Second)
	opts.SetPingTimeout(2 * time.Second)
	opts.SetMaxReconnectInterval(2 * time.Second)
	opts.SetDefaultPublishHandler(msgHandler)
	opts.SetCredentialsProvider(m.credentialHandler)
	opts.OnConnectionLost = m.connectionLostHandler

	if onConnectHandler != nil {
		opts.SetOnConnectHandler(onConnectHandler)
	}

	return mqtt.NewClient(opts)
}

func (m *MQTTManager) credentialHandler() (username, password string) {
	token, err := generateMqttToken(version.AppName, m.config.Secret, defaultTokenExpireSeconds)
	if err != nil {
		msg := tracerr.Errorf("Error getting token = %w", err)
		slog.Error(msg.Error())

		m.notify.SendMsg(domain.MessageChannelError, msg.Error(), domain.MessageStatusStopper, nil)

		return "", ""
	}

	return version.AppName, token
}

func (m *MQTTManager) Subscribe(topic string, handler func(topic string, msg []byte)) {
	t := path.Join(m.config.Subdomain, "quantex/", topic)
	token := m.client.Subscribe(t, qos, func(_ mqtt.Client, msg mqtt.Message) {
		handler(msg.Topic(), msg.Payload())
	})
	m.CheckSubscribe(token, t)
}

func (m *MQTTManager) Publish(topic string, msg any) {
	payload, err := json.Marshal(msg)
	if err != nil {
		slog.Error("error. could not send alert msg: " + err.Error())

		return
	}

	// Publish a message
	t := path.Join(m.config.Subdomain, "quantex/", topic)
	token := m.client.Publish(t, qos, false, payload)
	m.CheckPublish(token, t, payload)
}

func (m *MQTTManager) connectionLostHandler(client mqtt.Client, reason error) {
	opts := client.OptionsReader()
	msg := fmt.Sprintf("MQTT Connection lost for client: %s. Reason: %s", opts.ClientID(), reason.Error())
	slog.Warn(msg)
	m.notify.SendMsg(domain.MessageChannelError, msg, domain.MessageStatusWarning, nil)
}

func msgHandler(_ mqtt.Client, msg mqtt.Message) {
	slog.Info(fmt.Sprintf("Message received: [mqtt] -> [A] | received: '%s' topic: '%s'",
		msg.Payload(), msg.Topic()))
}

func generateMqttToken(user, secret string, exp int64) (string, error) {
	// Create a new token object, specifying signing method and the claims
	// you would like it to contain.
	now := time.Now().Unix()
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"username": user,
		"exp":      now + exp,
		"iat":      now,
	})

	// Sign and get the complete encoded token as a string using the secret
	tokenString, err := token.SignedString([]byte(secret))
	if err != nil {
		e := tracerr.Errorf("error generating the MQTT token %w", err)

		return "", e
	}

	return tokenString, nil
}

// CheckPublish check the result of publish action.
// For asynchronous check use:
// go CheckPublish(token, topic, payload)
func (m *MQTTManager) CheckPublish(token mqtt.Token, topic string, payload []byte) {
	slog.Info("[mqtt: %s] <- %s\n", topic, string(payload))

	errMsg := tracerr.Errorf("MQTT Publish Error. Topic: %s. Error: %w", topic, token.Error())
	m.checkToken(token, errMsg)
}

// CheckSubscribe check the result of subscribe action.
// For asynchronous check use:
// go CheckSubscribe(token, topic)
func (m *MQTTManager) CheckSubscribe(token mqtt.Token, topic string) {
	slog.Info("subscribing to [mqtt: " + topic + "]")

	errMsg := tracerr.Errorf("MQTT Subscriber Error. Topic: %s. Error: %w", topic, token.Error())
	m.checkToken(token, errMsg)
}

func (m *MQTTManager) checkToken(token mqtt.Token, errMsg error) {
	if token.Wait() && token.Error() != nil {
		err := tracerr.Errorf("checkToken error: %w", errMsg)
		slog.Error(err.Error())
		m.notify.SendMsg(domain.MessageChannelError, err.Error(), domain.MessageStatusWarning, nil)
	}
}

func checkMQTTConfig(config app.Async) error {
	if config.Protocol == "" ||
		config.URL == "" ||
		config.Subdomain == "" ||
		config.Secret == "" {
		return tracerr.Errorf("mqtt configuration is needed: Protocol, URL, Subdomain and/or Secret are empty")
	}

	return nil
}
