package socket

import (
	
	

	
)

// Path-state pruning limits. These bound the number of candidate paths an actor
// keeps per remote so the set cannot grow without limit. Relay paths are never
// counted or pruned. Values match the Rust reference
// (iroh/src/socket/remote_map/remote_state/path_state.rs:18,23).
const (
	// MaxNonRelayPaths is the maximum number of non-relay paths kept per remote.
	MaxNonRelayPaths = 30

	// MaxInactiveNonRelayPaths is the maximum number of inactive (previously
	// open, now closed) non-relay paths kept per remote.
	MaxInactiveNonRelayPaths = 10
)

// PathStatus is the lifecycle status of a candidate path. It mirrors the Rust
// PathStatus enum (path_state.rs:44).
type PathStatus int

const (
	// PathStatusUnknown is a path that has never been dialed: it was added by an
	// address-lookup mechanism and is only potentially usable.
	PathStatusUnknown PathStatus = iota
	// PathStatusOpen is a path that is currently open in QUIC.
	PathStatusOpen
	// PathStatusInactive is a path that was open at some point but has since
	// closed. The time records when it closed, used to prune oldest-first.
	PathStatusInactive
	// PathStatusUnusable is a path where hole-punching was attempted and failed.
	PathStatusUnusable
)

func ( PathStatus) () string {
	switch  {
	case PathStatusUnknown:
		return "unknown"
	case PathStatusOpen:
		return "open"
	case PathStatusInactive:
		return "inactive"
	case PathStatusUnusable:
		return "unusable"
	default:
		return "invalid"
	}
}

// PathState is the per-path bookkeeping kept by [RemotePathState].
type PathState struct {
	// Status is the current lifecycle status of the path.
	Status PathStatus
	// lastActive records when an open path was last observed as active.
	lastActive time.Time
	// closedAt records when an inactive path was last closed; it is only
	// meaningful when Status is PathStatusInactive and orders inactive-path
	// pruning (most recently closed kept first).
	closedAt time.Time
}

// TransportAddrUsage reports whether a remote transport address is currently
// active.
type TransportAddrUsage int

const (
	// TransportAddrInactive means the address is known but not currently used.
	TransportAddrInactive TransportAddrUsage = iota
	// TransportAddrActive means the address is currently used.
	TransportAddrActive
)

// TransportAddrInfo is a remote transport address plus usage metadata.
type TransportAddrInfo struct {
	Addr       netaddr.TransportAddr
	Usage      TransportAddrUsage
	Provenance string
}

// RemotePathState tracks all candidate paths to a single remote endpoint:
// direct IP, relay, and custom transport addresses, each with a [PathStatus].
// It is the Go analog of the Rust RemotePathState (path_state.rs).
//
// Paths added by address lookup start [PathStatusUnknown]; QUIC path events move
// them through Open and Inactive; failed hole-punches mark them Unusable. The
// set is bounded by [RemotePathState.Prune], which keeps at most
// [MaxNonRelayPaths] non-relay paths plus [MaxInactiveNonRelayPaths] inactive
// non-relay paths. Relay paths are never pruned.
//
// RemotePathState is not safe for concurrent use; it is owned by a single
// [RemoteStateActor] goroutine.
type RemotePathState struct {
	paths map[string]pathEntry
}

// pathEntry stores a path's [Addr] alongside its [PathState]. The map is keyed
// by Addr.String() because [Addr] is not directly comparable (it embeds a
// non-comparable netaddr.CustomAddr).
type pathEntry struct {
	addr       Addr
	state      PathState
	provenance string
}

// NewRemotePathState returns an empty path-state tracker.
func () *RemotePathState {
	return &RemotePathState{paths: make(map[string]pathEntry)}
}

// key returns the stable map key for an [Addr].
func pathKey( Addr) string { return .String() }

// IsEmpty reports whether no paths are known.
func ( *RemotePathState) () bool { return len(.paths) == 0 }

// Len returns the number of known paths, including relay paths.
func ( *RemotePathState) () int { return len(.paths) }

// Addrs returns the addresses of all known paths in unspecified order.
func ( *RemotePathState) () []Addr {
	 := make([]Addr, 0, len(.paths))
	for ,  := range .paths {
		 = append(, .addr)
	}
	return 
}

// OpenAddrs returns the addresses of all currently open paths.
func ( *RemotePathState) () []Addr {
	 := make([]Addr, 0, len(.paths))
	for ,  := range .paths {
		if .state.Status == PathStatusOpen {
			 = append(, .addr)
		}
	}
	sort.Slice(, func(,  int) bool {
		return [].String() < [].String()
	})
	return 
}

// RemoteAddrs returns all known remote addresses with active/inactive usage.
func ( *RemotePathState) () []TransportAddrInfo {
	 := make([]TransportAddrInfo, 0, len(.paths))
	for ,  := range .paths {
		,  := transportAddrFromAddr(.addr)
		if ! {
			continue
		}
		 := TransportAddrInactive
		if .state.Status == PathStatusOpen {
			 = TransportAddrActive
		}
		 = append(, TransportAddrInfo{Addr: , Usage: , Provenance: .provenance})
	}
	sort.Slice(, func(,  int) bool {
		return [].Addr.String() < [].Addr.String()
	})
	return 
}

// Status returns the status of addr and whether it is known.
func ( *RemotePathState) ( Addr) (PathStatus, bool) {
	,  := .paths[pathKey()]
	if ! {
		return PathStatusUnknown, false
	}
	return .state.Status, true
}

// Add records a candidate path with [PathStatusUnknown] if it is not already
// known. A path already present keeps its current status.
func ( *RemotePathState) ( Addr) {
	.AddWithProvenance(, "")
}

// AddWithProvenance records a candidate path with lookup provenance.
func ( *RemotePathState) ( Addr,  string) {
	 := pathKey()
	if ,  := .paths[];  {
		if .provenance == "" &&  != "" {
			.provenance = 
			.paths[] = 
		}
		return
	}
	.paths[] = pathEntry{addr: , state: PathState{Status: PathStatusUnknown}, provenance: }
}

// SetOpen marks addr as open, adding it if unknown. It mirrors the Rust
// add_path / on path-open transition (path_state.rs:90).
func ( *RemotePathState) ( Addr) {
	.SetOpenAt(, time.Now())
}

// SetOpenAt marks addr as open with an explicit activity time. It is used by
// tests and by the actor heartbeat, which already has a shared timestamp for
// all observed paths.
func ( *RemotePathState) ( Addr,  time.Time) {
	 := pathKey()
	 := .paths[]
	.addr = 
	.state = PathState{Status: PathStatusOpen, lastActive: }
	.paths[] = 
}

// SetClosed transitions addr toward an inactive/unusable status, recording the
// close time for inactive pruning. It mirrors the Rust remove_path transition
// (path_state.rs:106): an open or already-inactive path becomes inactive (still
// considered usable later); an unusable or unknown path becomes unusable.
func ( *RemotePathState) ( Addr,  time.Time) {
	 := pathKey()
	,  := .paths[]
	if ! {
		return
	}
	switch .state.Status {
	case PathStatusOpen, PathStatusInactive:
		.state.Status = PathStatusInactive
		.state.closedAt = 
	case PathStatusUnusable, PathStatusUnknown:
		.state.Status = PathStatusUnusable
	}
	.paths[] = 
}

// SetUnusable marks addr unusable: a hole-punch was attempted and failed.
func ( *RemotePathState) ( Addr) {
	 := pathKey()
	,  := .paths[]
	if ! {
		 = pathEntry{addr: }
	}
	.addr = 
	.state.Status = PathStatusUnusable
	.paths[] = 
}

// ExpireIdle closes open paths that have not been observed within their path
// idle timeout. Direct and custom paths use [PathMaxIdleTimeout]; relay paths
// use [RelayPathMaxIdleTimeout].
func ( *RemotePathState) ( time.Time) []Addr {
	var  []Addr
	for ,  := range .paths {
		if .state.Status != PathStatusOpen || .state.lastActive.IsZero() {
			continue
		}
		 := PathMaxIdleTimeout
		if isRelayAddr(.addr) {
			 = RelayPathMaxIdleTimeout
		}
		if .Sub(.state.lastActive) <  {
			continue
		}
		.state.Status = PathStatusInactive
		.state.closedAt = 
		.paths[] = 
		 = append(, .addr)
	}
	return 
}

// Prune bounds the non-relay path set. It is a no-op when there are fewer than
// [MaxNonRelayPaths] non-relay paths. Otherwise it removes failed (unusable)
// paths and all but the [MaxInactiveNonRelayPaths] most-recently-closed inactive
// paths. Open and unknown paths are always kept; relay paths are never pruned or
// counted. It mirrors prune_non_relay_paths (path_state.rs:254).
func ( *RemotePathState) () {
	// Bail early if the total path count is below the limit.
	if len(.paths) < MaxNonRelayPaths {
		return
	}

	 := 0
	for ,  := range .paths {
		if !isRelayAddr(.addr) {
			++
		}
	}
	if  < MaxNonRelayPaths {
		return
	}

	type  struct {
		      string
		 time.Time
	}
	var  []
	var  []string
	for ,  := range .paths {
		if isRelayAddr(.addr) {
			continue
		}
		switch .state.Status {
		case PathStatusInactive:
			 = append(, {: , : .state.closedAt})
		case PathStatusUnusable:
			 = append(, )
		}
	}

	// If every path failed, do not prune all of them: keep MaxNonRelayPaths.
	// This implies inactive is empty.
	if len() == len(.paths) {
		 := len(.paths) - MaxNonRelayPaths
		if  < 0 {
			 = 0
		}
		 = [:]
	}

	// Sort inactive most-recently-closed first, then drop everything beyond the
	// MaxInactiveNonRelayPaths we keep.
	sort.Slice(, func(,  int) bool {
		return []..After([].)
	})
	 := MaxInactiveNonRelayPaths
	if  > len() {
		 = len()
	}
	 := [:]

	 := make(map[string]struct{}, len()+len())
	for ,  := range  {
		[] = struct{}{}
	}
	for ,  := range  {
		[.] = struct{}{}
	}
	for  := range  {
		delete(.paths, )
	}
}

// isRelayAddr reports whether a is a relay path.
func isRelayAddr( Addr) bool { return .Kind() == AddrRelay }

func transportAddrFromAddr( Addr) (netaddr.TransportAddr, bool) {
	if ,  := .IP();  {
		return netaddr.IPAddr{Addr: }, true
	}
	if , ,  := .Relay();  {
		return netaddr.RelayAddr{URL: }, true
	}
	if ,  := .Custom();  {
		return , true
	}
	return nil, false
}