package main

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"slices"
	"strings"

	MQTT "github.com/eclipse/paho.mqtt.golang"
	"github.com/jedib0t/go-pretty/v6/table"
	"gitlab.imt-atlantique.fr/xaal/code/go/core/uuid"
)

const (
	AccessLevelR   AccessLevel = 1
	AccessLevelW   AccessLevel = 2
	AccessLevelRW  AccessLevel = 3
	AccessLevelRN  AccessLevel = 5
	AccessLevelRWN AccessLevel = 7
)

type (
	AccessLevel int
)

var ignoredTopics = []string{
	mqttTopic + "/bridge/groups",
	mqttTopic + "/bridge/definitions",
	mqttTopic + "/bridge/extensions",
	mqttTopic + "/bridge/info",
	mqttTopic + "/bridge/state",
	mqttTopic + "/bridge/logging",
	mqttTopic + "/bridge/config",
}

// JSON structures for the z2m device from /bridge/devices
type Z2MDevice struct {
	Definition struct {
		Vendor  string   `json:"vendor"`
		Model   string   `json:"model"`
		Exposes []Expose `json:"exposes"`
	} `json:"definition"`
	Type         string `json:"type"`
	IeeeAddress  string `json:"ieee_address"`
	SwBuildID    string `json:"software_build_id"`
	FriendlyName string `json:"friendly_name"`
	XAALDevices  []XAALDeviceInterface
}

type Expose struct {
	Name     string    `json:"name"`
	Type     string    `json:"type"`
	Unit     string    `json:"unit,omitempty"`
	Features []Feature `json:"features,omitempty"`
	Values   []string  `json:"values,omitempty"`
	Access   int       `json:"access,omitempty"`
}

type Feature struct {
	Name     string `json:"name"`
	Type     string `json:"type"`
	Property string `json:"property,omitempty"`
	Unit     string `json:"unit,omitempty"`
	Access   int    `json:"access,omitempty"`
}

// =============================================================================
// Z2MDevice API
// =============================================================================

// returns the topic for the device
func (zDev *Z2MDevice) GetTopic() string {
	return mqttTopic + "/" + zDev.FriendlyName
}

// return the expose with the given name
func (zDev *Z2MDevice) GetExpose(name string) *Expose {
	for _, expose := range zDev.Definition.Exposes {
		if expose.Name == name {
			return &expose
		}
	}
	return nil
}

// updates the xAAL device with the MQTT message
func (zDev *Z2MDevice) HandleMessage(msg MQTT.Message) {
	var data map[string]interface{}
	err := json.Unmarshal(msg.Payload(), &data)
	if err != nil {
		slog.Error("Error decoding JSON", "err", err)
	} else {
		slog.Info("Updating device:", "name", zDev.FriendlyName)
		for _, dev := range zDev.XAALDevices {
			dev.update(data)
		}

	}
}

// creates new xAAL devices from a bridge device
func (zDev *Z2MDevice) setupXAALDevices(gw *Gateway) {
	// TODO: Handle errors
	baseAddr := gw.baseAddr
	ieeeAddr, _ := hexStringToInteger(zDev.IeeeAddress)
	baseAddr, _ = baseAddr.Add(int64(ieeeAddr))

	for i, expose := range zDev.Definition.Exposes {
		addr, _ := baseAddr.Add(int64(i))
		grpAdd, _ := baseAddr.Add(int64(0xff))

		var dev XAALDeviceInterface
		deviceMap := map[string]func(uuid.UUID, *Z2MDevice) XAALDeviceInterface{
			"contact":     NewContact,
			"temperature": NewThermometer,
			"humidity":    NewHygrometer,
			"linkquality": NewLinkQuality,
			"battery":     NewBattery,
			"power":       NewPowerMeter,
			"action":      NewButtonRemote,
		}

		if createFunc, ok := deviceMap[expose.Name]; ok {
			dev = createFunc(addr, zDev)
		} else if expose.Type == "switch" {
			property := "state"
			for _, exp := range expose.Features {
				if strings.Contains(exp.Property, "state") {
					property = exp.Property
					break
				}
			}
			dev = NewPowerRelay(addr, zDev, property)
		} else if expose.Type == "light" {
			// type := NewLamp
			// for _, exp := range expose.Features {
			// }
			dev = NewLamp(addr, zDev, &expose)
		}

		if dev != nil {
			zDev.XAALDevices = append(zDev.XAALDevices, dev)
			xaalDev := dev.getXAALDevice()
			xaalDev.GroupID = grpAdd
			gw.engine.AddDevice(xaalDev)
		}
	}
}

//==============================================================================
// MQTT API
//==============================================================================

// Publish the payload to the right topic
func (zDev *Z2MDevice) Publish(topic string, payload interface{}) {
	topic = zDev.GetTopic() + "/" + topic
	slog.Debug("Sending", "topic", topic, "payload", payload)
	client := GetGW().client
	if token := client.Publish(topic, 0, false, payload); token.Wait() && token.Error() != nil {
		slog.Error("PUBLISH Error", ":", token.Error())
	}
}

// Publish the device wanted state
func (zDev *Z2MDevice) Set(payload interface{}) {
	zDev.Publish("set", payload)
}

// Check if the device is available, Z2M will send an availability message
// if this enable in Z2M config
func (zDev *Z2MDevice) Available() {
	zDev.Publish("availability", `{"state": "online"}`)
}

// Ask for a device to dump its state. Z2M doesn't support this but the
// dump-state-ext.js provide this feature
func (zDev *Z2MDevice) Sync() {
	zDev.Publish("dump", "{}")
}

func (zDev *Z2MDevice) dump() {
	tab := table.NewWriter()
	tab.SetTitle("Def:" + zDev.FriendlyName)
	tab.SetStyle(table.StyleRounded)
	tab.AppendRow(table.Row{"IeeeAddr", zDev.IeeeAddress})
	tab.AppendRow(table.Row{"Vendor", zDev.Definition.Vendor})
	tab.AppendRow(table.Row{"Model", zDev.Definition.Model})
	tab.AppendRow(table.Row{"Type", zDev.Type})
	fmt.Println(tab.Render())

	if len(zDev.Definition.Exposes) > 0 {
		expTab := table.NewWriter()
		expTab.SetTitle("Exp:" + zDev.FriendlyName)
		expTab.SetStyle(table.StyleRounded)
		expTab.AppendHeader(table.Row{"Name", "Type", "Acc", "Unit", "Values", "Features: Name[Type]-Acc-(Unit){Property}"})
		for _, expose := range zDev.Definition.Exposes {
			values := ""
			if len(expose.Values) > 0 {
				values = strings.Join(expose.Values, "\n")
			}
			features := ""
			if len(expose.Features) > 0 {
				for _, feature := range expose.Features {
					features += fmt.Sprintf("- %s[%s]-%s-(%s){%s}\n", feature.Name, feature.Type, AccessLevel(feature.Access), feature.Unit, feature.Property)
				}
				features = strings.TrimSuffix(features, "\n")
			}
			expTab.AppendRow(table.Row{expose.Name, expose.Type, AccessLevel(expose.Access), expose.Unit, values, features})
		}
		fmt.Println(expTab.Render())
	}
}

// =============================================================================
// Expose
// =============================================================================
func (e *Expose) GetFeature(name string) *Feature {
	for _, feature := range e.Features {
		if feature.Name == name {
			return &feature
		}
	}
	return nil
}

// =============================================================================
// Helpers
// =============================================================================
func (l AccessLevel) String() string {
	return [...]string{"--", "R", "W", "RW", "Err", "RN", "Err", "RWN"}[l]
}

func (l AccessLevel) EnumIndex() int {
	return int(l)
}

// jsonParseDevices parses the bridge/devices json and creates new xAAL devices
// if they don't exist
func jsonParseDevices(jsonData []byte) {
	var devices []Z2MDevice
	err := json.Unmarshal([]byte(jsonData), &devices)
	if err != nil {
		slog.Error("Error decoding JSON", "err", err)
	}

	gw := GetGW()
	for _, zDev := range devices {
		known := gw.GetZDevice(zDev.FriendlyName)
		if known != nil {
			continue
		}
		zDev.dump()
		zDev.setupXAALDevices(gw)
		gw.AddZDevice(&zDev)
		zDev.Sync()
		// zDev.Available()
	}
}

// mqttPublishHander handles all incoming MQTT messages
// If the topic is /bridge/devices it will parse the json and create new devices
// Else it will find the device with the topic and call the mqttDeviceHandler
func mqttPublishHander(client MQTT.Client, msg MQTT.Message) {
	// we ignore some topics
	if slices.Contains(ignoredTopics, msg.Topic()) {
		return
	}
	slog.Debug("Received message on", "topic", msg.Topic())

	// Is it devices definitions ?
	if msg.Topic() == mqttTopic+"/bridge/devices" {
		jsonParseDevices(msg.Payload())
	} else {
		dev := GetGW().GetZDeviceByTopic(msg.Topic())
		mqttDumpMsg(msg)
		if dev != nil {
			dev.HandleMessage(msg)
		}
	}
}