// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

package api

import (
	"strings"

	"github.com/google/uuid"
	"github.com/meshcore-go/meshcore-go"
)

// NodeNeighbor represents a neighboring node relationship observed in a given IATA.
type NodeNeighbor struct {
	ID               uuid.UUID `json:"id"`
	Name             *string   `json:"name,omitempty"`
	PublicKey        string    `json:"publicKey"`
	NodeType         int16     `json:"nodeType"`
	NodeTypeName     string    `json:"nodeTypeName"`
	Latitude         *float64  `json:"lat,omitempty"`
	Longitude        *float64  `json:"lng,omitempty"`
	IATA             string    `json:"iata"`
	ObservationCount int64     `json:"observationCount"`
	FirstSeen        int64     `json:"firstSeen"` // epoch ms
	LastSeen         int64     `json:"lastSeen"`  // epoch ms
	SNR              *float32  `json:"snr,omitempty"`
}

// NodeIATA represents a single IATA code and the last time the node was heard there.
type NodeIATA struct {
	IATA      string `json:"iata"`
	LastHeard int64  `json:"lastHeard"` // epoch ms
}

// NodeSummary is the minimal node representation used in list responses.
type NodeSummary struct {
	ID                 uuid.UUID   `json:"id"`
	PublicKey          string      `json:"publicKey"` // hex-encoded Ed25519 public key
	NodeType           int16       `json:"nodeType"`  // 1=companion, 2=repeater, 3=room_server, 4=sensor
	NodeTypeName       string      `json:"nodeTypeName"`
	Name               *string     `json:"name,omitempty"`
	IsObserver         bool        `json:"isObserver"`             // true if this node is also a known observer
	ObserverID         *uuid.UUID  `json:"observerId,omitempty"`   // UUID of the associated observer row, if any
	Latitude           *float64    `json:"lat,omitempty"`          // decimal degrees, from advert AppData
	Longitude          *float64    `json:"lng,omitempty"`          // decimal degrees, from advert AppData
	Radio              *string     `json:"radio,omitempty"`        // shorthand: "freqMhz,bwKhz,sf" e.g. "910.525,62.5,7"
	IATAs              []NodeIATA  `json:"iatas"`                  // IATAs where this node has been heard, with last heard timestamps
	DefaultScope       *string     `json:"defaultScope,omitempty"` // most recently matched transport scope name e.g. "#bc"
	KnownNeighborCount int64       `json:"knownNeighborCount"`
	NeighborIDs        []uuid.UUID `json:"neighborIds,omitempty"` // only populated when the list request opts in; see ?neighbors=true
	// Stale is true when the node hasn't been seen (last_seen) within the configured
	// staleness window (default 24h; internal/config.ResolvedConfig.NodeStaleThreshold).
	// Applies to every node type, unlike ClockDriftSeconds/ClockOutOfSync on Node, which
	// are repeater/room-server only.
	Stale bool `json:"stale"`
}

// Node is the full node representation including firmware capability flags,
// location source, and timing metadata.
type Node struct {
	NodeSummary
	LocationSource          *string        `json:"locationSource,omitempty"`     // "advert" or "manual"
	LastAdvertAt            *int64         `json:"lastAdvertAt,omitempty"`       // epoch ms, nil if no advert received
	SupportsMultibytePaths  bool           `json:"supportsMultibytePaths"`       // firmware >= 1.14.0; detected via path hash size
	SupportsMultibyteTraces bool           `json:"supportsMultibyteTraces"`      // firmware >= 1.11.0; detected via trace hash size
	MinFirmwareVersion      *string        `json:"minFirmwareVersion,omitempty"` // derived from capability flags
	FirstSeen               int64          `json:"firstSeen"`                    // epoch ms
	LastSeen                int64          `json:"lastSeen"`                     // epoch ms
	Metadata                any            `json:"metadata,omitempty"`           // raw JSONB metadata
	Neighbors               []NodeNeighbor `json:"neighbors"`
	// Clock drift, repeaters/room servers only (nodeType 2/3); omitted entirely for other
	// node types or when no qualifying advert has been measured yet. Device minus server
	// time, in seconds, from the advert's self-reported timestamp: +ve = device ahead.
	// clockCheckedAt is the server receive time of the advert this was measured from (same
	// moment as lastAdvertAt). clockOutOfSync is |clockDriftSeconds| exceeding a configured
	// threshold (default 5m) -- see internal/config.ResolvedConfig.ClockDriftThreshold.
	ClockDriftSeconds *int   `json:"clockDriftSeconds,omitempty"`
	ClockOutOfSync    *bool  `json:"clockOutOfSync,omitempty"`
	ClockCheckedAt    *int64 `json:"clockCheckedAt,omitempty"`
}

// NodeTypeName returns a human-readable name for a node type integer.
// NOTE: truncation is fine here until there are at least over 200 types of node
func NodeTypeName(t int16) string {
	switch byte(t) {
	case meshcore.AdvertTypeChat:
		return "companion"
	case meshcore.AdvertTypeRepeater:
		return "repeater"
	case meshcore.AdvertTypeRoom:
		return "room_server"
	case meshcore.AdvertTypeSensor:
		return "sensor"
	default:
		return "unknown"
	}
}

// NodeTypeFromString returns the integer node type for a given name.
// Returns 0 (no filter) if the string is empty or unrecognized.
func NodeTypeFromString(s string) int16 {
	switch strings.ToLower(s) {
	case "companion", "chat":
		return int16(meshcore.AdvertTypeChat)
	case "repeater":
		return int16(meshcore.AdvertTypeRepeater)
	case "room_server", "roomserver", "room-server", "room":
		return int16(meshcore.AdvertTypeRoom)
	case "sensor":
		return int16(meshcore.AdvertTypeSensor)
	default:
		return 0
	}
}
