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

package conn25

import (
	"cmp"
	"encoding/json"
	"maps"
	"net/http"
	"net/netip"
	"slices"
	"strings"

	"tailscale.com/envknob"
	"tailscale.com/ipn/ipnlocal"
	"tailscale.com/ipn/localapi"
	"tailscale.com/types/appctype"
	"tailscale.com/util/dnsname"
	"tailscale.com/util/httpm"
	"tailscale.com/util/mak"
	"tailscale.com/util/testenv"
)

// serveLocalAPIStateGet serves the localapi endpoint /conn25/state.
// See also [*Conn25.GetActiveState].
func serveLocalAPIStateGet(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
	// TODO(tailscale/corp#39033): Remove for alpha release.
	if !envknob.UseWIPCode() && !testenv.InTest() {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}
	if !h.PermitRead {
		http.Error(w, "conn25/state access denied", http.StatusForbidden)
		return
	}
	if r.Method != httpm.GET {
		http.Error(w, "GET required", http.StatusMethodNotAllowed)
		return
	}
	ext, ok := ipnlocal.GetExt[*extension](h.LocalBackend())
	if !ok {
		http.Error(w, "miswired", http.StatusInternalServerError)
		return
	}
	state := ext.conn25.GetActiveState()
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(state); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

// serveC2NStateGet serves the C2N endpoint /conn25/state.
// See also [*Conn25.GetActiveState].
func serveC2NStateGet(b *ipnlocal.LocalBackend, w http.ResponseWriter, r *http.Request) {
	// TODO(tailscale/corp#39033): Remove for alpha release.
	if !envknob.UseWIPCode() && !testenv.InTest() {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	logf := b.Logger()
	logf("c2n: GET /conn25/state received")

	ext, ok := ipnlocal.GetExt[*extension](b)
	if !ok {
		http.Error(w, "miswired", http.StatusInternalServerError)
		return
	}
	state := ext.conn25.GetActiveState()
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(state); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

// GetActiveState returns active state for the client and the connector,
// including IP pool usage, address mappings, domains, and active flow counts.
func (c *Conn25) GetActiveState() appctype.Conn25ActiveState {
	if !c.isConfigured() {
		return appctype.Conn25ActiveState{}
	}

	return appctype.Conn25ActiveState{
		Configured: true,
		Client:     c.client.getActiveState(),
		Connector:  c.connector.getActiveState(),
	}
}

// getActiveState gets active state from the client.
// See [appctype.Conn25ClientState] for structure.
func (c *client) getActiveState() appctype.Conn25ClientState {
	c.mu.Lock()
	defer c.mu.Unlock()

	var appToDomains map[string]map[dnsname.FQDN][]appctype.Conn25ClientAddressState

	// Pre-sort addresses by destination IP. They'll later be primary sorted
	// by active flow count.
	domainDstKeys := slices.Collect(maps.Keys(c.assignments.byDomainDst))
	slices.SortFunc(domainDstKeys, func(a, b domainDst) int {
		return a.dst.Compare(b.dst)
	})

	for _, domainDstKey := range domainDstKeys {
		assignment := c.assignments.byDomainDst[domainDstKey]
		domainMap := appToDomains[assignment.app]
		addresses := domainMap[assignment.domain]
		addresses = append(addresses, appctype.Conn25ClientAddressState{
			ActiveFlowCount: assignment.activeFlowCount,
			DestinationIP:   assignment.dst.String(),
			MagicIP:         assignment.magic.String(),
			TransitIP:       assignment.transit.String(),
			ExpiresAt:       assignment.expiresAt,
		})

		mak.Set(&domainMap, assignment.domain, addresses)
		mak.Set(&appToDomains, assignment.app, domainMap)
	}

	var apps []appctype.Conn25ClientAppState
	for appName, domainMap := range appToDomains {
		var app appctype.Conn25ClientAppState
		app.App = appName
		for domain, addresses := range domainMap {
			var domainState appctype.Conn25ClientDomainState
			domainState.Domain = domain

			// Sort address mappings by descending active flow count.
			slices.SortStableFunc(addresses, func(a, b appctype.Conn25ClientAddressState) int {
				if a.ActiveFlowCount > b.ActiveFlowCount {
					return -1
				}
				if a.ActiveFlowCount < b.ActiveFlowCount {
					return 1
				}
				return 0
			})
			domainState.Addresses = addresses
			app.Domains = append(app.Domains, domainState)
		}

		// Sort domains by hierarchy, e.g. "example.com", "sub.example.com".
		slices.SortFunc(app.Domains, compareFQDNHierarchical)
		apps = append(apps, app)
	}

	// Sort apps lexicographically.
	slices.SortFunc(apps, func(a, b appctype.Conn25ClientAppState) int {
		return strings.Compare(a.App, b.App)
	})

	return appctype.Conn25ClientState{
		Apps: apps,
		IPPoolStats: appctype.Conn25ClientIPPoolStats{
			IPv4MagicIPsInUse:      c.v4MagicIPPool.inUseCount(),
			IPv4MagicIPsCapacity:   c.v4MagicIPPool.capacity(),
			IPv6MagicIPsInUse:      c.v6MagicIPPool.inUseCount(),
			IPv6MagicIPsCapacity:   c.v6MagicIPPool.capacity(),
			IPv4TransitIPsInUse:    c.v4TransitIPPool.inUseCount(),
			IPv4TransitIPsCapacity: c.v4TransitIPPool.capacity(),
			IPv6TransitIPsInUse:    c.v6TransitIPPool.inUseCount(),
			IPv6TransitIPsCapacity: c.v6TransitIPPool.capacity(),
		},
	}
}

// getActiveState gets active state from the connector.
// See [appctype.Conn25ConnectorState] for structure.
func (c *connector) getActiveState() appctype.Conn25ConnectorState {
	c.mu.Lock()
	defer c.mu.Unlock()

	var peers []appctype.Conn25ConnectorPeerState
	// Sort peers by client IP.
	for _, clientIP := range slices.SortedFunc(maps.Keys(c.transitIPs), netip.Addr.Compare) {
		transitToAddr := c.transitIPs[clientIP]
		var appToAddrs map[string][]appctype.Conn25ConnectorAddressState

		// Sort address mappings by transit IP.
		for _, transitIP := range slices.SortedFunc(maps.Keys(transitToAddr), netip.Addr.Compare) {
			addr := transitToAddr[transitIP]
			apiAddr := appctype.Conn25ConnectorAddressState{
				DestinationIP: addr.addr.String(),
				TransitIP:     transitIP.String(),
			}
			mak.Set(&appToAddrs, addr.app, append(appToAddrs[addr.app], apiAddr))
		}

		var apps []appctype.Conn25ConnectorAppState
		for appName, addrs := range appToAddrs {
			apps = append(apps, appctype.Conn25ConnectorAppState{
				App:       appName,
				Addresses: addrs,
			})
		}

		// Sort apps lexicographically.
		slices.SortFunc(apps, func(a, b appctype.Conn25ConnectorAppState) int {
			return strings.Compare(a.App, b.App)
		})

		peers = append(peers, appctype.Conn25ConnectorPeerState{
			ClientIP: clientIP.String(),
			Apps:     apps,
		})
	}

	return appctype.Conn25ConnectorState{Peers: peers}
}

// compareFQDNHierarchical sorts [appctype.Conn25ClientDomainState] hierarchically
// such that parents precede their children, e.g. "example.com" precedes
// "sub.example.com". If two domains aren't related, they are sorted lexicographically
// from the root and moving forward, e.g. "example.com" precedes "abc.org".
func compareFQDNHierarchical(ad, bd appctype.Conn25ClientDomainState) int {
	a, b := ad.Domain, bd.Domain
	al := strings.Split(a.WithoutTrailingDot(), ".")
	bl := strings.Split(b.WithoutTrailingDot(), ".")
	for i := 1; i <= len(al) && i <= len(bl); i++ {
		if c := strings.Compare(al[len(al)-i], bl[len(bl)-i]); c != 0 {
			return c
		}
	}
	return cmp.Compare(len(al), len(bl))
}
