package board

import (
	"fmt"
	"log"
	"strings"
	"time"

	"git.openpdu.org/OpenPDU/openpdu/syslog"
	PahoMQTT "github.com/eclipse/paho.mqtt.golang"
	"github.com/spf13/viper"
)

type MQTTChannel struct {
	ID                       string
	Num                      uint
	name                     string
	mqttStateTopic           string
	mqttCommandTopic         string
	value                    bool
	onboot                   string
	parent                   *MQTTBoard
	MQTTRemoteStateTopic     string
	MQTTRemoteCommandTopic   string
	MQTTRemotePayloadOn      string
	MQTTRemotePayloadOff     string
	onChannelUpdateFunctions map[string]onChannelUpdateFunction
}

type MQTTBoard struct {
	ID                 string
	Name               string
	ChannelCount       uint
	channels           []*MQTTChannel
	MQTTRemoteSchema   string
	MQTTRemoteHost     string
	MQTTRemotePort     string
	MQTTRemoteUsername string
	MQTTRemotePassword string
	MQTTClient         PahoMQTT.Client
}

func newMQTTChannel(v *viper.Viper, channelID string) MQTTChannel {
	v.SetDefault("name", "unknown")
	v.SetDefault("lastValue", false)
	v.SetDefault("onboot", "off")
	v.SetDefault("mqtt.statetopic", v.GetString("name"))
	v.SetDefault("mqtt.commandtopic", v.GetString("name"))
	v.SetDefault("mqttremote.statetopic", v.GetString("name"))
	v.SetDefault("mqttremote.commandtopic", v.GetString("name"))
	v.SetDefault("mqttremote.payloadon", "on")
	v.SetDefault("mqttremote.payloadoff", "off")

	value := false
	switch v.GetString("onboot") {
	case "on":
		value = true
	case "last":
		value = v.GetBool("lastValue")
	}
	// newUUID := UUID.New().String()
	// v.SetDefault("id", newUUID)
	return MQTTChannel{
		ID:                     channelID,
		Num:                    v.GetUint("num"),
		name:                   v.GetString("name"),
		mqttStateTopic:         v.GetString("mqtt.statetopic"),
		mqttCommandTopic:       v.GetString("mqtt.commandtopic"),
		value:                  value,
		onboot:                 v.GetString("onboot"),
		MQTTRemoteStateTopic:   v.GetString("mqttremote.statetopic"),
		MQTTRemoteCommandTopic: v.GetString("mqttremote.commandtopic"),
		MQTTRemotePayloadOn:    v.GetString("mqttremote.payloadon"),
		MQTTRemotePayloadOff:   v.GetString("mqttremote.payloadoff"),
	}
}

func newMQTTBoard(v *viper.Viper, id string) *Board {
	var b MQTTBoard

	v.SetDefault("name", "board "+id)
	v.SetDefault("type", "mqtt")
	v.SetDefault("channelCount", 0)
	v.SetDefault("channels", "")
	v.SetDefault("MQTTRemote.Schema", "tcp")
	v.SetDefault("MQTTRemote.Host", "")
	v.SetDefault("MQTTRemote.Port", "1883")
	v.SetDefault("MQTTRemote.Username", "")
	v.SetDefault("MQTTRemote.Password", "")

	if v.GetInt("channelCount") > 0 {
		for i := 0; i < v.GetInt("channelCount"); i++ {
			v.SetDefault("channels."+fmt.Sprint(i)+".num", i)
		}
	}

	b = MQTTBoard{
		ID:                 id,
		Name:               v.GetString("name"),
		ChannelCount:       v.GetUint("channelCount"),
		MQTTRemoteSchema:   v.GetString("MQTTRemote.Schema"),
		MQTTRemoteHost:     v.GetString("MQTTRemote.Host"),
		MQTTRemotePort:     v.GetString("MQTTRemote.Port"),
		MQTTRemoteUsername: v.GetString("MQTTRemote.Username"),
		MQTTRemotePassword: v.GetString("MQTTRemote.Password"),
	}

	channels := make([]*MQTTChannel, v.GetInt("channelCount"))
	if v.GetInt("channelCount") > 0 {
		channelsConfig := v.Sub("channels")
		if channelsConfig != nil {
			for channelid1 := range channelsConfig.AllSettings() {
				channelid := strings.ToLower(channelid1)
				channelConfig := channelsConfig.Sub(channelid)
				c := newMQTTChannel(channelConfig, channelid)
				c.parent = &b
				if c.Num >= v.GetUint("channelCount") {
					continue
				}
				channels[c.Num] = &c
				AllChannels[c.ID] = &c
			}
		}
	}
	b.channels = channels

	var b1 Board = &b
	return &b1
}

func init() {
	RegisterBoardType("mqtt", newMQTTBoard)
}

func (c *MQTTChannel) Toggle() (bool, error) {
	c.value = !c.value
	c.SaveLastState()
	for f := range c.onChannelUpdateFunctions {
		c.onChannelUpdateFunctions[f](!c.value, c)
	}
	c.UpdateRemoteMQTT()
	return c.value, nil
}

func (c *MQTTChannel) On() error {
	oldval := c.value
	c.value = true
	c.SaveLastState()
	for f := range c.onChannelUpdateFunctions {
		c.onChannelUpdateFunctions[f](oldval, c)
	}
	c.UpdateRemoteMQTT()
	return nil
}

func (c *MQTTChannel) Off() error {
	oldval := c.value
	c.value = false
	c.SaveLastState()
	for f := range c.onChannelUpdateFunctions {
		c.onChannelUpdateFunctions[f](oldval, c)
	}
	c.UpdateRemoteMQTT()
	return nil
}

func (c *MQTTChannel) ToString() string {
	if !c.value {
		return "off"
	}
	return "on"
}

func (c *MQTTChannel) SaveLastState() {
	if c.onboot == "last" {
		s := fmt.Sprintf("boards.%s.channels.%s.lastvalue", c.parent.ID, c.ID)
		viper.Set(s, c.value)
		viper.WriteConfig()
	}
}

func (c *MQTTChannel) UpdateRemoteMQTT() {
	connected := c.parent.MQTTClient.IsConnected()
	if connected {
		v := c.MQTTRemotePayloadOff
		if c.value {
			v = c.MQTTRemotePayloadOn
		}
		c.parent.MQTTClient.Publish(c.MQTTRemoteCommandTopic, 0, false, v)
	}
}

func (c *MQTTChannel) Status() bool {
	return c.value
}

func (c *MQTTChannel) Parent() Board {
	return c.parent
}

func (c *MQTTChannel) Dump() {
	log.Printf(" Channel %d (on boot: %s): %s \n", c.Num, c.onboot, c.name)
}

func (b MQTTBoard) Dump() {
	log.Printf("Board '%s' (id: %s): %d channels\n", b.Name, b.ID, b.ChannelCount)
	for c := range b.channels {
		b.channels[c].Dump()
	}
}

func (b *MQTTBoard) Initialize() {
	uri := b.MQTTRemoteSchema + "://" + b.MQTTRemoteHost + ":" + b.MQTTRemotePort
	opts := PahoMQTT.NewClientOptions().AddBroker(uri)

	opts.SetClientID(b.ID)

	if b.MQTTRemoteUsername != "" {
		opts.SetUsername(b.MQTTRemoteUsername)
	}

	if b.MQTTRemotePassword != "" {
		opts.SetPassword(b.MQTTRemotePassword)
	}

	opts.SetAutoReconnect(true)
	opts.SetConnectRetryInterval(5 * time.Second)
	opts.SetConnectTimeout(5 * time.Second)

	opts.SetConnectionLostHandler(func(c PahoMQTT.Client, err error) {
		syslog.Err("mqtt connection lost error: " + err.Error())
	})
	opts.SetReconnectingHandler(func(c PahoMQTT.Client, options *PahoMQTT.ClientOptions) {
		syslog.Notice("mqtt reconnecting")
	})
	opts.SetOnConnectHandler(func(c PahoMQTT.Client) {
		syslog.Notice("mqtt connected")
	})

	b.MQTTClient = PahoMQTT.NewClient(opts)

	for {
		token := b.MQTTClient.Connect()
		token.Wait()
		if b.MQTTClient.IsConnected() {
			break
		}
		time.Sleep(5 * time.Second)
	}

	for i := range b.channels {
		topic := b.channels[i].MQTTRemoteStateTopic
		if topic == "" {
			continue
		}
		b.MQTTClient.Subscribe(topic, 1, b.channels[i].MQTTRemoteHandler)
	}
}

func (c *MQTTChannel) MQTTRemoteHandler(client PahoMQTT.Client, msg PahoMQTT.Message) {
	switch string(msg.Payload()) {
	case c.MQTTRemotePayloadOn:
		if !c.value {
			c.value = true
			c.SaveLastState()
			for f := range c.onChannelUpdateFunctions {
				c.onChannelUpdateFunctions[f](false, c)
			}
		}
	case c.MQTTRemotePayloadOff:
		if c.value {
			c.value = false
			c.SaveLastState()
			for f := range c.onChannelUpdateFunctions {
				c.onChannelUpdateFunctions[f](true, c)
			}
		}
	}
}

func (c *MQTTChannel) MQTTHandler(client PahoMQTT.Client, msg PahoMQTT.Message) {
	syslog.Debug(fmt.Sprintf("Board '%s', channel '%s', received MQTT message %s",
		c.parent.Name,
		c.name,
		string(msg.Payload()),
	))
	switch string(msg.Payload()) {
	case "on":
		if !c.value {
			c.value = true
			c.SaveLastState()
			c.UpdateRemoteMQTT()
		}
	case "off":
		if c.value {
			c.value = false
			c.SaveLastState()
			c.UpdateRemoteMQTT()
		}
	}
}

func (b *MQTTBoard) Channel(num uint64) Channel {
	return b.channels[num]
}

func (c *MQTTChannel) Name() string {
	return c.name
}

func (c *MQTTChannel) OnBoot() string {
	return c.onboot
}

func (c *MQTTChannel) SetOnBoot(str string) {
	c.onboot = str
	s := fmt.Sprintf("boards.%s.channels.%s.onboot", c.parent.ID, c.ID)
	viper.Set(s, str)
}

func (c *MQTTChannel) SetMQTTStateTopic(str string) {
	c.mqttStateTopic = str
	s := fmt.Sprintf("boards.%s.channels.%s.mqtt.statetopic", c.parent.ID, c.ID)
	viper.Set(s, str)
}

func (c *MQTTChannel) SetMQTTCommandTopic(str string) {
	c.mqttCommandTopic = str
	s := fmt.Sprintf("boards.%s.channels.%s.mqtt.commandtopic", c.parent.ID, c.ID)
	viper.Set(s, str)
}

func (c *MQTTChannel) MQTTStateTopic() string {
	return c.mqttStateTopic
}

func (c *MQTTChannel) MQTTCommandTopic() string {
	return c.mqttCommandTopic
}

func (c *MQTTChannel) AddOnChannelUpdateFunction(name string, f onChannelUpdateFunction) {
	if c.onChannelUpdateFunctions == nil {
		c.onChannelUpdateFunctions = make(map[string]onChannelUpdateFunction)
	}
	c.onChannelUpdateFunctions[name] = f
}
