package schema

import (
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"time"

	"xorm.io/builder"

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

func init() {
	RegisterModel(new(MeshCorePacket))
	RegisterModel(new(MeshCoreNode))
	RegisterModel(new(MeshCoreNodePosition))
	RegisterModel(new(MeshCoreGroup))
}

type MeshCorePacket struct {
	ID          int64     `xorm:"pk autoincr" json:"id"`
	RadioID     int64     `xorm:"index" json:"radio_id"`
	Radio       *Radio    `xorm:"-" json:"radio"`
	SNR         float64   `xorm:"not null default 0" json:"snr"`
	RSSI        int       `xorm:"not null default 0" json:"rssi"`
	Version     int       `xorm:"not null default 1" json:"version"`
	RouteType   uint8     `xorm:"index not null" json:"route_type"`
	PayloadType uint8     `xorm:"index not null" json:"payload_type"`
	Hash        string    `xorm:"varchar(16) index not null" json:"hash"`
	Path        []byte    `xorm:"bytea" json:"path"`
	Payload     []byte    `xorm:"bytea not null" json:"payload"`
	Raw         []byte    `xorm:"bytea not null" json:"raw"`
	Parsed      *string   `xorm:"jsonb" json:"parsed"`
	ChannelHash string    `xorm:"varchar(2) index" json:"channel_hash,omitempty"`
	ReceivedAt  time.Time `json:"received_at"`
}

func (MeshCorePacket) TableName() string {
	return "meshcore_packet"
}

func GetMeshCorePackets(ctx context.Context, limit int) ([]*MeshCorePacket, error) {
	packets := make([]*MeshCorePacket, 0, limit)
	return packets, Query(ctx).
		OrderBy("`received_at` DESC").
		Limit(limit).
		Find(&packets)
}

func GetMeshCorePacketsByHash(ctx context.Context, hash string) ([]*MeshCorePacket, error) {
	if len(hash) != 16 {
		return nil, errors.New("invalid hash")
	} else if _, err := hex.DecodeString(hash); err != nil {
		return nil, err
	}

	packets := make([]*MeshCorePacket, 0, 10)
	return packets, Query(ctx).
		Where(builder.Eq{"hash": hash}).
		OrderBy("`received_at` ASC").
		Find(&packets)
}

func GetMeshCorePacketsByPayloadType(ctx context.Context, payloadType meshcore.PayloadType) ([]*MeshCorePacket, error) {
	packets := make([]*MeshCorePacket, 0, 10)
	return packets, Query(ctx).
		Where(builder.Eq{"payload_type": int(payloadType)}).
		OrderBy("`received_at` DESC").
		Find(&packets)
}

func GetMeshCorePacketsByChannelHash(ctx context.Context, hash string) ([]*MeshCorePacket, error) {
	packets := make([]*MeshCorePacket, 0, 10)
	return packets, Query(ctx).
		Where(builder.Eq{
			"`payload_type`": int(meshcore.TypeGroupText),
			"`channel_hash`": hash,
		}).
		OrderBy("`received_at` DESC").
		Find(&packets)
}

type MeshCoreGroup struct {
	ID       int64  `xorm:"pk autoincr" json:"id"`
	Name     string `xorm:"varchar(32) not null unique" json:"name"`
	Secret   string `xorm:"varchar(32) not null" json:"secret"`
	IsPublic bool   `xorm:"boolean not null default false" json:"-"`
}

func (MeshCoreGroup) TableName() string {
	return "meshcore_group"
}

func GetMeshCoreGroups(ctx context.Context) ([]*MeshCoreGroup, error) {
	groups := make([]*MeshCoreGroup, 0, 10)
	return groups, Query(ctx).
		Where(builder.Eq{"is_public": true}).
		OrderBy("name asc").
		Find(&groups)
}

type MeshCoreNode struct {
	ID            int64             `xorm:"pk autoincr" json:"id"`
	PacketHash    string            `xorm:"varchar(16) index 'meshcore_packet_hash'" json:"-"`
	Packets       []*MeshCorePacket `xorm:"-" json:"packet"`
	Name          string            `xorm:"varchar(100) not null" json:"name"`
	Type          uint8             `xorm:"index not null" json:"type"`
	Prefix        string            `xorm:"varchar(2) not null" json:"prefix"`
	PublicKey     string            `xorm:"varchar(64) not null unique" json:"public_key"`
	FirstHeardAt  time.Time         `xorm:"timestamp not null" json:"first_heard_at"`
	LastHeardAt   time.Time         `xorm:"timestamp not null" json:"last_heard_at"`
	LastLatitude  *float64          `json:"last_latitude"`
	LastLongitude *float64          `json:"last_longitude"`
	Distance      float64           `xorm:"-" json:"distance,omitempty"`
}

func (MeshCoreNode) TableName() string {
	return "meshcore_node"
}

func GetMeshCoreNodeByPublicKey(ctx context.Context, publicKey string) (*MeshCoreNode, error) {
	node := new(MeshCoreNode)
	if ok, err := Query(ctx).
		Where(builder.Eq{"public_key": publicKey}).
		Get(node); err != nil {
		return nil, err
	} else if !ok {
		return nil, os.ErrNotExist
	}
	return node, nil
}

func GetMeshCoreNodes(ctx context.Context) ([]*MeshCoreNode, error) {
	nodes := make([]*MeshCoreNode, 0, 100)
	return nodes, Query(ctx).
		OrderBy("`last_heard_at` DESC").
		Find(&nodes)
}

func GetMeshCoreNodesByType(ctx context.Context, nodeType meshcore.NodeType) ([]*MeshCoreNode, error) {
	nodes := make([]*MeshCoreNode, 0, 100)
	return nodes, Query(ctx).
		Where(builder.Eq{"type": nodeType}).
		OrderBy("`last_heard_at` DESC").
		Find(&nodes)
}

type MeshCoreNodeWithDistance struct {
	MeshCoreNode `xorm:"extends"`
	Distance     float64 `xorm:"distance"`
}

func GetMeshCoreNodesCloseTo(ctx context.Context, publicKey string, radius float64) (*MeshCoreNode, []*MeshCoreNode, error) {
	node, err := GetMeshCoreNodeByPublicKey(ctx, publicKey)
	if err != nil {
		return nil, nil, err
	} else if node.LastLatitude == nil || node.LastLongitude == nil {
		return nil, nil, errors.New("node has no location")
	}

	nodesWithDistance := make([]*MeshCoreNodeWithDistance, 0, 100)
	selectClause := fmt.Sprintf("*, "+
		"ST_Distance("+
		"ST_SetSRID(ST_MakePoint(%f, %f), 4326)::geography, "+
		"ST_SetSRID(ST_MakePoint(last_longitude, last_latitude), 4326)::geography"+
		") as distance",
		*node.LastLongitude, *node.LastLatitude)
	if err = Query(ctx).
		Select(selectClause).
		Where("id != ?", node.ID).
		Where("ST_DWithin("+
			"ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, "+
			"ST_SetSRID(ST_MakePoint(last_longitude, last_latitude), 4326)::geography, ?)",
			*node.LastLongitude, *node.LastLatitude, radius).
		OrderBy("`distance` ASC").
		Find(&nodesWithDistance); err != nil {
		return nil, nil, err
	}

	nodes := make([]*MeshCoreNode, len(nodesWithDistance))
	for i, node := range nodesWithDistance {
		node.MeshCoreNode.Distance = node.Distance
		nodes[i] = &node.MeshCoreNode
	}
	return node, nodes, nil
}

type MeshCoreNodePosition struct {
	ID             int64         `xorm:"pk autoincr" json:"id"`
	MeshCoreNodeId int64         `xorm:"not null 'meshcore_node_id'" json:"-"`
	MeshCoreNode   *MeshCoreNode `xorm:"-" json:"node"`
	Latitude       float64       `json:"latitude"`
	Longitude      float64       `json:"longitude"`
	ReceivedAt     time.Time     `xorm:"timestamp not null" json:"received_at"`
}

func (MeshCoreNodePosition) TableName() string {
	return "meshcore_node_position"
}

type MeshCoreStats struct {
	Messages  int64 `json:"messages"`
	Nodes     int64 `json:"nodes"`
	Receivers int64 `json:"receivers"`
	Packets   struct {
		Timestamps []int64 `json:"timestamps"`
		Packets    []int64 `json:"packets"`
	} `json:"packets"`
}

func GetMeshCoreStats(ctx context.Context) (*MeshCoreStats, error) {
	var (
		engine = Query(ctx)
		stats  = new(MeshCoreStats)
		err    error
	)
	if stats.Messages, err = engine.
		In("`payload_type`", meshcore.TypeText, meshcore.TypeGroupText).
		Count(&MeshCorePacket{}); err != nil {
		return nil, err
	}
	if stats.Nodes, err = engine.
		Count(&MeshCoreNode{}); err != nil {
		return nil, err
	}
	if stats.Receivers, err = engine.
		Where(builder.Eq{"`protocol`": protocol.MeshCore}).
		Count(&Radio{}); err != nil {
		return nil, err
	}
	return stats, nil
}

/*
    Column    |           Type           | Collation | Nullable |                   Default
--------------+--------------------------+-----------+----------+---------------------------------------------
 id           | bigint                   |           | not null | nextval('meshcore_packet_id_seq'::regclass)
 snr          | real                     |           | not null | 0
 rssi         | smallint                 |           | not null | 0
 hash         | bytea                    |           | not null |
 route_type   | smallint                 |           | not null |
 payload_type | smallint                 |           | not null |
 path         | bytea                    |           |          |
 payload      | bytea                    |           |          |
 raw          | bytea                    |           |          |
 parsed       | jsonb                    |           |          |
 received_at  | timestamp with time zone |           | not null | now()
 created_at   | timestamp with time zone |           | not null | now()
*/

/*
 id             | bigint                   |           | not null | nextval('meshcore_node_id_seq'::regclass)
 last_advert_id | bigint                   |           |          |
 node_type      | smallint                 |           | not null | 0
 public_key     | bytea                    |           | not null |
 name           | text                     |           |          |
 local_time     | timestamp with time zone |           | not null |
 first_heard    | timestamp with time zone |           | not null | now()
 last_heard     | timestamp with time zone |           | not null | now()
 last_latitude  | numeric(10,8)            |           |          |
 last_longitude | numeric(11,8)            |           |          |
 prefix         | bytea                    |           |          | generated always as ("substring"(public_key, 0, 2)) stored
 last_position  | geometry(Point,4326)     |           |          | generated always as (                                                                                                                                                   +
                |                          |           |          | CASE                                                                                                                                                                    +
                |                          |           |          |     WHEN last_latitude IS NOT NULL AND last_longitude IS NOT NULL THEN st_setsrid(st_makepoint(last_latitude::double precision, last_longitude::double precision), 4326)+
                |                          |           |          |     ELSE NULL::geometry                                                                                                                                                 +
                |                          |           |          | END) stored
 position       | geometry(Point,4326)     |           |          |
*/
