package hamview

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"time"

	"github.com/Vaniog/go-postgis"
	"github.com/tarm/serial"
	"go.yaml.in/yaml/v3"

	"git.maze.io/go/ham/protocol"
	"git.maze.io/go/ham/protocol/meshcore"
)

type MeshCoreConfig struct {
	Type string    `yaml:"type"`
	Conf yaml.Node `yaml:"conf"`
}

type MeshCoreCompanionConfig struct {
	Port   string `yaml:"port"`
	Baud   int    `yaml:"baud"`
	Addr   string `yaml:"addr"`
	HasSNR bool   `yaml:"has_snr"` // patch: adds SNR/RSSI data
}

type MeshCorePrefix byte

func (prefix *MeshCorePrefix) MarshalJSON() ([]byte, error) {
	s := fmt.Sprintf("%02x", *prefix)
	return json.Marshal(s)
}

func (prefix *MeshCorePrefix) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}
	if n, err := fmt.Sscanf(s, "%02x", &prefix); err != nil {
		return err
	} else if n != 1 {
		return errors.New("no prefix could be decoded")
	}
	return nil
}

func NewMeshCoreReceiver(config *MeshCoreConfig) (protocol.PacketReceiver, error) {
	switch config.Type {
	case "companion", "":
		return newMeshCoreCompanionReceiver(config.Conf)

	case "repeater":
		return newMeshCoreRepeaterReceiver(config.Conf)

	default:
		return nil, fmt.Errorf("hamview: unsupported MeshCore node type %q", config.Type)
	}
}

func newMeshCoreCompanionReceiver(node yaml.Node) (protocol.PacketReceiver, error) {
	var config MeshCoreCompanionConfig
	if err := node.Decode(&config); err != nil {
		return nil, err
	}

	var (
		conn io.ReadWriteCloser
		err  error
	)
	switch {
	case config.Addr != "":
		Logger.Infof("receiver: connecting to MeshCore companion at tcp://%s", config.Addr)
		conn, err = net.Dial("tcp", config.Addr)

	default:
		if config.Port == "" {
			// TODO: detect serial ports
			config.Port = "/dev/ttyUSB0"
		}
		if config.Baud == 0 {
			config.Baud = 115200
		}
		Logger.Infof("receiver: connecting to MeshCore companion on %s at %d baud", config.Port, config.Baud)
		conn, err = serial.OpenPort(&serial.Config{
			Name: config.Port,
			Baud: config.Baud,
		})
	}
	if err != nil {
		return nil, err
	}

	receiver, err := meshcore.NewCompanion(conn)
	if err != nil {
		_ = conn.Close()
		Logger.Warnf("receiver: error connecting to companion: %v", err)
		return nil, err
	}

	info := receiver.Info()
	Logger.Infof("receiver: connected to MeshCore Companion %q model %q version %q", info.Name, info.Manufacturer, info.FirmwareVersion)
	return receiver, nil
}

func newMeshCoreRepeaterReceiver(node yaml.Node) (protocol.PacketReceiver, error) {
	var config MeshCoreCompanionConfig
	if err := node.Decode(&config); err != nil {
		return nil, err
	}

	var (
		conn io.ReadWriteCloser
		err  error
	)
	switch {
	case config.Addr != "":
		Logger.Infof("receiver: connecting to MeshCore repeater at tcp://%s", config.Addr)
		conn, err = net.Dial("tcp", config.Addr)

	default:
		if config.Port == "" {
			// TODO: detect serial ports
			config.Port = "/dev/ttyUSB0"
		}
		if config.Baud == 0 {
			config.Baud = 115200
		}
		Logger.Infof("receiver: connecting to MeshCore repeater on %s at %d baud", config.Port, config.Baud)
		conn, err = serial.OpenPort(&serial.Config{
			Name: config.Port,
			Baud: config.Baud,
		})
	}
	if err != nil {
		return nil, err
	}

	receiver, err := meshcore.NewRepeater(conn, config.HasSNR)
	if err != nil {
		_ = conn.Close()
		Logger.Warnf("receiver: error connecting to repeater: %v", err)
		return nil, err
	}

	info := receiver.Info()
	Logger.Infof("receiver: connected to MeshCore repeater %q model %q version %q", info.Name, info.Manufacturer, info.FirmwareVersion)
	return receiver, nil
}

type meshCoreNode struct {
	Name       string             `json:"name"`
	PublicKey  []byte             `json:"public_key"`
	Prefix     MeshCorePrefix     `json:"prefix"`
	NodeType   meshcore.NodeType  `json:"node_type"`
	FirstHeard time.Time          `json:"first_heard"`
	LastHeard  time.Time          `json:"last_heard"`
	Position   *meshcore.Position `json:"position"`
}

type meshCoreNodeDistance struct {
	meshCoreNode
	Distance float64 `json:"distance"`
}

func meshCoreRepeaterWithPrefixCloseTo(db *sql.DB, prefix MeshCorePrefix, position *meshcore.Position) (node *meshCoreNodeDistance, err error) {
	if position == nil {
		return nil, os.ErrNotExist
	}

	node = &meshCoreNodeDistance{
		meshCoreNode: meshCoreNode{
			Position: new(meshcore.Position),
		},
	}
	var nodePrefix []byte
	if err = db.QueryRow(`
	SELECT
		n.name,
		n.public_key,
		n.prefix,
		n.first_heard,
		n.last_heard,
		n.last_latitude,
		n.last_longitude,
		ST_DistanceSphere(
			last_position,
			GeomFromEWKB($2)
		) AS distance
	FROM
		meshcore_node n
	WHERE
		n.prefix = $1
	AND
		n.last_latitude IS NOT NULL
	AND
		n.last_longitude IS NOT NULL
	AND
		n.last_position IS NOT NULL
	ORDER BY
		distance ASC
	LIMIT 1
	`,
		[]byte{byte(prefix)},
		postgis.PointS{
			SRID: 4326,
			X:    position.Latitude,
			Y:    position.Longitude,
		},
	).Scan(
		&node.Name,
		&node.PublicKey,
		&nodePrefix,
		&node.FirstHeard,
		&node.LastHeard,
		&node.Position.Latitude,
		&node.Position.Longitude,
		&node.Distance,
	); err != nil {
		if err == sql.ErrNoRows {
			return nil, os.ErrNotExist
		}
		return
	}
	if len(nodePrefix) > 0 {
		node.Prefix = MeshCorePrefix(nodePrefix[0])
	}
	return
}
