// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package routecheck

import (
	"cmp"
	"iter"
	"maps"
	"net/netip"
	"slices"
	"time"

	jsonv2 "github.com/go-json-experiment/json"
	"github.com/go-json-experiment/json/jsontext"
	jsonv1 "github.com/go-json-experiment/json/v1"

	"tailscale.com/net/routecheck/peernode"
	"tailscale.com/tailcfg"
	"tailscale.com/util/clientmetric"
	"tailscale.com/util/mak"
)

var (
	metricReport = clientmetric.NewCounter("routecheck_report")
)

// Report returns the latest reachability report.
// It returns nil if a report isn’t available, which happens during initialization.
func (c *Client) Report() *Report {
	metricReport.Add(1)
	return c.report.Load()
}

// Report contains the result of a single routecheck.
type Report struct {
	// Done is the time when the report was finished.
	Done time.Time `json:"done"`

	// Reachable is the set of nodes that were reachable from the current host
	// when this report was compiled. Missing nodes may or may not be reachable.
	Reachable NodeSet `json:"reachable"`

	// LastProbed tracks the last time a given node was probed.
	// This is used to rate-limit reachability probing, so an entry’s
	// presence doesn’t imply that it is reachable.
	LastProbed map[tailcfg.NodeID]time.Time `json:"-"` // not marshaled
}

// IsReachable reports whether a peer is reachable by the current node.
func (rp Report) IsReachable(id tailcfg.NodeID) peernode.Reachability {
	// TODO(sfllaw): We should actually track all routers and consider the
	// absence of a router in the report as it being recently added for
	// consideration, so it is unknown. Then we should positively track
	// whether a node was reachable or not.
	_, k := rp.Reachable[id]
	if k {
		return peernode.Reachable
	}
	return peernode.Unknown
}

// RoutablePrefixes returns a map of routable network prefixes associated with
// each prefix’s routers that were reachable by the current host,
// at the time the report was finished.
// Each slice of routers are ordered by their node ID.
//
// Note: Fallback routes are not supported by design. If a subnet prefix
// contained within another more general prefix has no reachable routers,
// traffic is still sent to one of those unreachable routers.
// Routers for the general prefix aren’t candidates. See tailscale/tailscale#18550.
func (rp Report) RoutablePrefixes() RoutablePrefixes {
	var out map[netip.Prefix][]Node
	for _, n := range rp.Reachable {
		for _, p := range n.Routes {
			mak.Set(&out, p, append(out[p], n))
		}
	}
	for p := range out {
		slices.SortFunc(out[p], Node.Compare)
	}
	return out
}

// Node represents a node in the reachability report.
type Node struct {
	ID tailcfg.NodeID `json:"id"`

	// Name is the FQDN of the node.
	// It is also the MagicDNS name for the node.
	// It has a trailing dot.
	// e.g. "host.tail-scale.ts.net."
	Name string `json:"name"`

	// Addr is the IP address that was probed.
	Addr netip.Addr `json:"addr"`

	// Routes are the subnets that the node will route.
	Routes []netip.Prefix `json:"routes"`
}

// Compare returns an integer comparing two nodes, ordered by their node ID.
// The result will be 0 if n.ID == n2.ID, -1 if n.ID < n2.ID, and +1 if n.ID > n2.ID.
func (n Node) Compare(n2 Node) int {
	return cmp.Compare(n.ID, n2.ID)
}

// NodeSet is a set of nodes keyed by node ID, so duplicates are easily detected.
// To prevent stuttering, it marshals itself as a JSON array, sorted by node ID.
type NodeSet map[tailcfg.NodeID]Node

var (
	_ jsonv1.Marshaler       = &NodeSet{}
	_ jsonv1.Unmarshaler     = &NodeSet{}
	_ jsonv2.MarshalerTo     = &NodeSet{}
	_ jsonv2.UnmarshalerFrom = &NodeSet{}
)

// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (ns NodeSet) MarshalJSONTo(enc *jsontext.Encoder) error {
	nodes := slices.SortedFunc(maps.Values(ns), Node.Compare)
	return jsonv2.MarshalEncode(enc, nodes)
}

// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (ns *NodeSet) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
	var nodes []Node
	if err := jsonv2.UnmarshalDecode(dec, &nodes); err != nil {
		return err
	}
	if *ns == nil {
		*ns = make(NodeSet, len(nodes))
	}
	for _, n := range nodes {
		(*ns)[n.ID] = n
	}
	return nil
}

// MarshalJSON implements [jsonv1.Marshaler].
func (ns *NodeSet) MarshalJSON() ([]byte, error) {
	return jsonv2.Marshal(ns, jsonv1.DefaultOptionsV1())
}

// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (ns *NodeSet) UnmarshalJSON(b []byte) error {
	return jsonv2.Unmarshal(b, ns, jsonv1.DefaultOptionsV1())

}

// RoutablePrefixes is a map of routers,
// keyed by the network prefix for which they route.
type RoutablePrefixes map[netip.Prefix][]Node

// Sorted returns an iterator over the map of routers,
// ordered by the network prefix as described in [netip.Prefix.Compare].
func (rt RoutablePrefixes) Sorted() iter.Seq2[netip.Prefix, []Node] {
	return func(yield func(netip.Prefix, []Node) bool) {
		prefixes := slices.SortedFunc(maps.Keys(rt), netip.Prefix.Compare)
		for _, p := range prefixes {
			if !yield(p, rt[p]) {
				return
			}
		}
	}
}
