package hamview

import (
	"database/sql"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os"
	"slices"
	"strconv"
	"strings"
	"time"

	echologrus "github.com/cemkiy/echo-logrus"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"

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

const DefaultServerListen = ":8073"

type ServerConfig struct {
	Listen string `yaml:"listen"`
}

type Server struct {
	listen     string
	listenAddr *net.TCPAddr
	db         *sql.DB
}

func NewServer(serverConfig *ServerConfig, databaseConfig *DatabaseConfig) (*Server, error) {
	if serverConfig.Listen == "" {
		serverConfig.Listen = DefaultServerListen
	}

	listenAddr, err := net.ResolveTCPAddr("tcp", serverConfig.Listen)
	if err != nil {
		return nil, fmt.Errorf("hamview: invalid listen address %q: %v", serverConfig.Listen, err)
	}

	db, err := sql.Open(databaseConfig.Type, databaseConfig.Conf)
	if err != nil {
		return nil, err
	}

	return &Server{
		listen:     serverConfig.Listen,
		listenAddr: listenAddr,
		db:         db,
	}, nil
}

func (server *Server) Run() error {
	echologrus.Logger = Logger

	e := echo.New()
	e.Logger = echologrus.GetEchoLogger()
	e.Use(echologrus.Hook())
	e.Use(middleware.RequestLogger())
	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		AllowOrigins: []string{"*"},
		AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
	}))

	e.GET("/api/v1/meshcore/nodes", server.apiGetMeshCoreNodes)
	e.GET("/api/v1/meshcore/packets", server.apiGetMeshCorePackets)
	e.GET("/api/v1/meshcore/path/:origin/:path", server.apiGetMeshCorePath)
	e.GET("/api/v1/meshcore/sources", server.apiGetMeshCoreSources)

	if server.listenAddr.IP == nil || server.listenAddr.IP.Equal(net.ParseIP("0.0.0.0")) || server.listenAddr.IP.Equal(net.ParseIP("::")) {
		Logger.Infof("server: listening on http://127.0.0.1:%d", server.listenAddr.Port)
	} else {
		Logger.Infof("server: listening on http://%s:%d", server.listenAddr.IP, server.listenAddr.Port)
	}

	return e.Start(server.listen)
}

func (server *Server) apiError(ctx echo.Context, err error, status ...int) error {
	Logger.Warnf("server: error serving %s %s: %v", ctx.Request().Method, ctx.Request().URL.Path, err)

	if len(status) > 0 {
		return ctx.JSON(status[0], map[string]any{
			"error": err.Error(),
		})
	}

	switch {
	case os.IsNotExist(err):
		return ctx.JSON(http.StatusNotFound, nil)
	case os.IsPermission(err):
		return ctx.JSON(http.StatusUnauthorized, nil)
	default:
		return ctx.JSON(http.StatusInternalServerError, map[string]any{
			"error": err.Error(),
		})
	}
}

type meshCoreNodeResponse struct {
	SNR        float64            `json:"snr"`
	RSSI       int8               `json:"rssi"`
	Name       string             `json:"name"`
	PublicKey  []byte             `json:"public_key"`
	Prefix     byte               `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"`
}

func (server *Server) apiGetMeshCoreNodes(ctx echo.Context) error {
	/*
		nodeTypes := getQueryInts(ctx, "type")
		if len(nodeTypes) == 0 {
			nodeTypes = []int{
				int(meshcore.Repeater),
			}
		}
	*/
	nodeType := meshcore.Repeater
	if ctx.QueryParam("type") != "" {
		nodeType = meshcore.NodeType(getQueryInt(ctx, "type"))
	}

	rows, err := server.db.Query(sqlSelectMeshCoreNodesLastPosition, nodeType, 25)
	if err != nil {
		return server.apiError(ctx, err)
	}

	var (
		response []meshCoreNodeResponse
		prefix   []byte
	)
	for rows.Next() {
		var (
			row      meshCoreNodeResponse
			lat, lng *float64
		)
		if err := rows.Scan(
			&row.SNR,
			&row.RSSI,
			&row.Name,
			&row.PublicKey,
			&prefix,
			&row.NodeType,
			&row.FirstHeard,
			&row.LastHeard,
			&lat,
			&lng,
		); err != nil {
			return server.apiError(ctx, err)
		}
		if lat != nil && lng != nil {
			row.Position = &meshcore.Position{
				Latitude:  *lat,
				Longitude: *lng,
			}
		}
		if len(prefix) > 0 {
			row.Prefix = prefix[0]
		}
		response = append(response, row)
	}

	return ctx.JSON(http.StatusOK, response)
}

type meshCorePacketResponse struct {
	SNR         float64   `json:"snr"`
	RSSI        int8      `json:"rssi"`
	Hash        []byte    `json:"hash"`
	RouteType   byte      `json:"route_type"`
	PayloadType byte      `json:"payload_type"`
	Path        []byte    `json:"path"`
	ReceivedAt  time.Time `json:"received_at"`
	Raw         []byte    `json:"raw"`
	Parsed      []byte    `json:"parsed"`
}

func (server *Server) apiGetMeshCorePackets(ctx echo.Context) error {
	var (
		query string
		limit = 25
		args  []any
	)
	if hashParam := ctx.QueryParam("hash"); hashParam != "" {
		var (
			hash []byte
			err  error
		)
		switch len(hashParam) {
		case base64.URLEncoding.EncodedLen(8):
			hash, err = base64.URLEncoding.DecodeString(hashParam)
		case hex.EncodedLen(8):
			hash, err = hex.DecodeString(hashParam)
		default:
			err = errors.New("invalid encoding")
		}
		if err != nil {
			return server.apiError(ctx, err, http.StatusBadRequest)
		}
		query = sqlSelectMeshCorePacketsByHash
		args = []any{hash}
	} else {
		query = sqlSelectMeshCorePackets
		args = []any{limit}
	}

	rows, err := server.db.Query(query, args...)
	if err != nil {
		return server.apiError(ctx, err)
	}

	var response []meshCorePacketResponse
	for rows.Next() {
		var row meshCorePacketResponse
		if err := rows.Scan(
			&row.SNR,
			&row.RSSI,
			&row.Hash,
			&row.RouteType,
			&row.PayloadType,
			&row.Path,
			&row.ReceivedAt,
			&row.Raw,
			&row.Parsed,
		); err != nil {
			return server.apiError(ctx, err)
		}
		response = append(response, row)
	}

	return ctx.JSON(http.StatusOK, response)
}

type meshCorePathResponse struct {
	Origin *meshCoreNode           `json:"origin"`
	Path   []*meshCoreNodeDistance `json:"path"`
}

func (server *Server) apiGetMeshCorePath(ctx echo.Context) error {
	origin, err := hex.DecodeString(ctx.Param("origin"))
	if err != nil || len(origin) != crypto.PublicKeySize {
		return ctx.JSON(http.StatusBadRequest, map[string]any{
			"error": "invalid origin",
		})
	}

	path, err := hex.DecodeString(ctx.Param("path"))
	if err != nil || len(path) == 0 {
		return ctx.JSON(http.StatusBadRequest, map[string]any{
			"error": "invalid path",
		})
	}

	var (
		node                meshCoreNodeDistance
		prefix              []byte
		latitude, longitude *float64
	)
	if err := server.db.QueryRow(`
		SELECT
			n.name,
			n.public_key,
			n.prefix,
			n.first_heard,
			n.last_heard,
			n.last_latitude,
			n.last_longitude
		FROM
			meshcore_node n
		WHERE
			n.node_type = 2 AND
			n.public_key = $1
		`,
		origin,
	).Scan(
		&node.Name,
		&node.PublicKey,
		&prefix,
		&node.FirstHeard,
		&node.LastHeard,
		&latitude,
		&longitude,
	); err != nil {
		return server.apiError(ctx, err)
	}
	node.Prefix = MeshCorePrefix(prefix[0])
	if latitude == nil || longitude == nil {
		return ctx.JSON(http.StatusNotFound, map[string]any{
			"error": "origin has no known position",
		})
	}
	node.Position = &meshcore.Position{
		Latitude:  *latitude,
		Longitude: *longitude,
	}

	var (
		current = &node
		trace   []*meshCoreNodeDistance
	)
	slices.Reverse(path)
	for _, prefix := range path {
		if prefix != byte(current.Prefix) {
			var hop *meshCoreNodeDistance
			if hop, err = meshCoreRepeaterWithPrefixCloseTo(server.db, MeshCorePrefix(prefix), current.Position); err != nil {
				if !os.IsNotExist(err) {
					return server.apiError(ctx, err)
				}
				current = &meshCoreNodeDistance{
					meshCoreNode: meshCoreNode{
						Prefix:   MeshCorePrefix(prefix),
						Position: current.Position,
					},
				}
			} else {
				current = hop
			}
		}
		trace = append(trace, current)
	}

	/*
		if path[len(path)-1] == node.Prefix {
			path = path[:len(path)-2]
		}
	*/

	var response = meshCorePathResponse{
		Origin: &node.meshCoreNode,
		Path:   trace,
	}
	return ctx.JSON(http.StatusOK, response)
}

type meshCoreSourcesResponse struct {
	Window  time.Time      `json:"time"`
	Packets map[string]int `json:"packets"`
}

func (server *Server) apiGetMeshCoreSources(ctx echo.Context) error {
	var (
		now     = time.Now().UTC()
		windows = map[string]struct {
			Interval int
			Since    time.Duration
		}{
			"24h": {900, time.Hour * 24},
			"1w":  {3600, time.Hour * 24 * 7},
		}
		window = ctx.QueryParam("window")
	)
	if window == "" {
		window = "24h"
	}
	params, ok := windows[window]
	if !ok {
		return server.apiError(ctx, os.ErrNotExist)
	}

	rows, err := server.db.Query(sqlSelectMeshCorePacketsByRepeaterWindowed, params.Interval, now.Add(-params.Since))
	if err != nil {
		return server.apiError(ctx, err)
	}

	var (
		response []*meshCoreSourcesResponse
		buckets  = make(map[int64]*meshCoreSourcesResponse)
	)
	for rows.Next() {
		var result struct {
			Window   time.Time
			Repeater string
			Packets  int
		}
		if err := rows.Scan(&result.Window, &result.Repeater, &result.Packets); err != nil {
			return server.apiError(ctx, err)
		}
		if result.Packets <= 10 {
			continue // ignore
		}

		if bucket, ok := buckets[result.Window.Unix()]; ok {
			bucket.Packets[result.Repeater] = result.Packets
		} else {
			bucket = &meshCoreSourcesResponse{
				Window: result.Window,
				Packets: map[string]int{
					result.Repeater: result.Packets,
				},
			}
			response = append(response, bucket)
			buckets[result.Window.Unix()] = bucket
		}
	}

	return ctx.JSON(http.StatusOK, response)
}

func getQueryInt(ctx echo.Context, param string) int {
	v, _ := strconv.Atoi(ctx.QueryParam(param))
	return v
}

func getQueryInts(ctx echo.Context, param string) []int {
	var values []int
	if keys := strings.Split(ctx.QueryParam(param), ","); len(keys) > 0 {
		for _, value := range keys {
			if v, err := strconv.Atoi(value); err == nil {
				values = append(values, v)
			}
		}
	}
	return values
}
