package main

import (
	"context"
	"fmt"
	"math"
	"strconv"
	"time"

	"github.com/AFASystems/presence/internal/pkg/config"
	"github.com/AFASystems/presence/internal/pkg/kafkaclient"
	"github.com/AFASystems/presence/internal/pkg/model"
	"github.com/AFASystems/presence/internal/pkg/mqttclient"
	presenseredis "github.com/AFASystems/presence/internal/pkg/redis"
	"github.com/redis/go-redis/v9"
)

// Move Kafka topics, Redis keys, intervals to env config
// Replace hardcoded IPs with env vars
// avoid defers -> lock and unlock right before and after usage
// Distance formula uses twos_comp incorrectly should parse signed int not hex string
// Use buffered log instead of fmt.Println ???
// Limit metrics slice size with ring buffer ??
// handle go routine exit signals with context.WithCancel() ??

// Make internal package for Kafka and Redis
// Make internal package for processor:
// Helper functions: twos_comp, getBeaconId

func main() {
	// Load global context to init beacons and latest list
	appCtx := model.AppContext{
		Beacons: model.BeaconsList{
			Beacons: make(map[string]model.Beacon),
		},
		LatestList: model.LatestBeaconsList{
			LatestList: make(map[string]model.Beacon),
		},
		Settings: model.Settings{
			Settings: model.SettingsVal{
				Location_confidence:  4,
				Last_seen_threshold:  15,
				Beacon_metrics_size:  30,
				HA_send_interval:     5,
				HA_send_changes_only: false,
			},
		},
	}

	cfg := config.Load()

	// Kafka writer idk why yet
	writer := kafkaclient.KafkaWriter(cfg.KafkaURL, "beacons")
	defer writer.Close()

	// Kafka reader for Raw MQTT beacons
	rawReader := kafkaclient.KafkaReader(cfg.KafkaURL, "rawbeacons", "someID")
	defer rawReader.Close()

	// Kafka reader for API server updates
	apiReader := kafkaclient.KafkaReader(cfg.KafkaURL, "apibeacons", "someID")
	defer apiReader.Close()

	// Kafka reader for latest list updates
	latestReader := kafkaclient.KafkaReader(cfg.KafkaURL, "latestbeacons", "someID")
	defer latestReader.Close()

	// Kafka reader for settings updates
	settingsReader := kafkaclient.KafkaReader(cfg.KafkaURL, "settings", "someID")
	defer settingsReader.Close()

	ctx := context.Background()

	// Init Redis Client
	client := redis.NewClient(&redis.Options{
		Addr:     cfg.RedisURL,
		Password: "",
	})

	beaconsList := presenseredis.LoadBeaconsList(client, ctx)
	appCtx.Beacons.Beacons = beaconsList

	latestList := presenseredis.LoadLatestList(client, ctx)
	appCtx.LatestList.LatestList = latestList

	settings := presenseredis.LoadSettings(client, ctx)
	appCtx.Settings.Settings = settings

	// declare channel for collecting Kafka messages
	chRaw := make(chan model.Incoming_json, 2000)
	chApi := make(chan model.ApiUpdate, 2000)
	chLatest := make(chan model.Incoming_json, 2000)
	chSettings := make(chan model.SettingsVal, 10)

	go kafkaclient.Consume(rawReader, chRaw)
	go kafkaclient.Consume(apiReader, chApi)
	go kafkaclient.Consume(latestReader, chLatest)
	go kafkaclient.Consume(settingsReader, chSettings)

	go func() {
		// Syncing Redis cache every 1s with 2 lists: beacons, latest list
		ticker := time.NewTicker(1 * time.Second)
		defer ticker.Stop()

		for range ticker.C {
			presenseredis.SaveBeaconsList(&appCtx, client, ctx)
			presenseredis.SaveLatestList(&appCtx, client, ctx)
			presenseredis.SaveSettings(&appCtx, client, ctx)
		}
	}()

	for {
		select {
		case msg := <-chRaw:
			processIncoming(msg, &appCtx)
		case msg := <-chApi:
			switch msg.Method {
			case "POST":
				appCtx.Beacons.Lock.Lock()
				appCtx.Beacons.Beacons[msg.Beacon.Beacon_id] = msg.Beacon
			case "DELETE":
				_, exists := appCtx.Beacons.Beacons[msg.ID]
				if exists {
					appCtx.Beacons.Lock.Lock()
					delete(appCtx.Beacons.Beacons, msg.ID)
				}
			default:
				fmt.Println("unknown method: ", msg.Method)
			}
			appCtx.Beacons.Lock.Unlock()
		case msg := <-chLatest:
			fmt.Println("latest msg: ", msg)
		case msg := <-chSettings:
			appCtx.Settings.Lock.Lock()
			appCtx.Settings.Settings = msg
			fmt.Println("settings channel: ", msg)
			appCtx.Settings.Lock.Unlock()
		}
	}
}

func processIncoming(incoming model.Incoming_json, ctx *model.AppContext) {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("work failed:", err)
		}
	}()

	fmt.Println("message came")

	incoming = mqttclient.IncomingBeaconFilter(incoming)
	id := mqttclient.GetBeaconID(incoming)
	now := time.Now().Unix()

	beacons := &ctx.Beacons

	beacons.Lock.Lock()
	defer beacons.Lock.Unlock()

	latestList := &ctx.LatestList

	latestList.Lock.Lock()
	defer latestList.Lock.Unlock()

	beacon, exists := beacons.Beacons[id]
	if !exists {
		x, exists := latestList.LatestList[id]
		if exists {
			x.Last_seen = now
			x.Incoming_JSON = incoming
			x.Distance = getBeaconDistance(incoming)
			latestList.LatestList[id] = x
		} else {
			latestList.LatestList[id] = model.Beacon{Beacon_id: id, Beacon_type: incoming.Beacon_type, Last_seen: now, Incoming_JSON: incoming, Beacon_location: incoming.Hostname, Distance: getBeaconDistance(incoming)}
		}
		// Move this to seperate routine?
		for k, v := range latestList.LatestList {
			if (now - v.Last_seen) > 10 {
				delete(latestList.LatestList, k)
			}
		}
		return
	}

	updateBeacon(&beacon, incoming)
	beacons.Beacons[id] = beacon
}

func getBeaconDistance(incoming model.Incoming_json) float64 {
	rssi := incoming.RSSI
	power := incoming.TX_power
	distance := 100.0

	ratio := float64(rssi) * (1.0 / float64(twos_comp(power)))
	if ratio < 1.0 {
		distance = math.Pow(ratio, 10)
	} else {
		distance = (0.89976)*math.Pow(ratio, 7.7095) + 0.111
	}
	return distance
}

func updateBeacon(beacon *model.Beacon, incoming model.Incoming_json) {
	now := time.Now().Unix()

	beacon.Incoming_JSON = incoming
	beacon.Last_seen = now
	beacon.Beacon_type = incoming.Beacon_type
	beacon.HB_ButtonCounter = incoming.HB_ButtonCounter
	beacon.HB_Battery = incoming.HB_Battery
	beacon.HB_RandomNonce = incoming.HB_RandomNonce
	beacon.HB_ButtonMode = incoming.HB_ButtonMode

	if beacon.Beacon_metrics == nil {
		beacon.Beacon_metrics = make([]model.BeaconMetric, 10) // 10 is a placeholder for now
	}

	metric := model.BeaconMetric{}
	metric.Distance = getBeaconDistance(incoming)
	metric.Timestamp = now
	metric.Rssi = int64(incoming.RSSI)
	metric.Location = incoming.Hostname
	beacon.Beacon_metrics = append(beacon.Beacon_metrics, metric)

	// Leave the HB button implementation for now
}

func twos_comp(inp string) int64 {
	i, _ := strconv.ParseInt("0x"+inp, 0, 64)
	return i - 256
}
