package main

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"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
)

// JSON structures for the z2m device from /bridge/devices
type Z2MDevice struct {
	Gateway      *Gateway
	Type         string `json:"type"`
	IeeeAddress  string `json:"ieee_address"`
	SwBuildID    string `json:"software_build_id"`
	FriendlyName string `json:"friendly_name"`
	Definition   struct {
		Vendor  string   `json:"vendor"`
		Model   string   `json:"model"`
		Exposes []Expose `json:"exposes"`
	} `json:"definition"`
	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 zDev.Gateway.Config.topic + "/" + 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, "topic", msg.Topic(), "data", msg.Payload())
	} else {
		slog.Debug("Updating device:", "name", zDev.FriendlyName)
		for _, dev := range zDev.XAALDevices {
			dev.update(data)
		}
	}
}

// creates new xAAL devices from a bridge device
func (zDev *Z2MDevice) FindXAALDevices(gw *Gateway) {
	// There is a trick here. We call FindXAALDevices w/ gw argument, but the Gateway is only set
	// in the AddZDevice method. This is not mandatory but avoid to have Gateway at random place
	baseAddr := gw.Config.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, *Expose) XAALDeviceInterface{
			"contact":     NewContact,
			"temperature": NewThermometer,
			"humidity":    NewHygrometer,
			"linkquality": NewLinkQuality,
			"battery":     NewBattery,
			"power":       NewPowerMeter,
			"action":      NewButtonRemote,
			"switch":      NewPowerRelay,
			"light":       NewLamp,
			"occupancy":   NewMotion,
			"illuminance": NewLuxMeter,
			"voltage":     NewVoltMeter,
			"current":     NewAmpMeter,
		}

		// Search a matching expose name
		if createFunc, ok := deviceMap[expose.Name]; ok {
			dev = createFunc(addr, zDev, &expose)
		}
		// Search a matching expose type
		if createFunc, ok := deviceMap[expose.Type]; ok {
			dev = createFunc(addr, zDev, &expose)
		}

		if dev != nil {
			dev.setup()
			zDev.XAALDevices = append(zDev.XAALDevices, dev)
			xaalDev := dev.GetXAALDevice()
			xaalDev.GroupID = grpAdd
		}
	}
}

//==============================================================================
// 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 := zDev.Gateway.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)
}