package mesh

import (
	"fmt"
	"net"
)

// Status is our current state as a peer, as taken from a router.
// This is designed to be used as diagnostic information.
type Status struct {
	Protocol           string
	ProtocolMinVersion int
	ProtocolMaxVersion int
	Encryption         bool
	PeerDiscovery      bool
	Name               string
	NickName           string
	Port               int
	Peers              []PeerStatus
	UnicastRoutes      []unicastRouteStatus
	BroadcastRoutes    []broadcastRouteStatus
	Connections        []LocalConnectionStatus
	TerminationCount   int
	Targets            []string
	OverlayDiagnostics interface{}
	TrustedSubnets     []string
}

// NewStatus returns a Status object, taken as a snapshot from the router.
func NewStatus(router *Router) *Status {
	return &Status{
		Protocol:           Protocol,
		ProtocolMinVersion: ProtocolMinVersion,
		ProtocolMaxVersion: ProtocolMaxVersion,
		Encryption:         router.usingPassword(),
		PeerDiscovery:      router.PeerDiscovery,
		Name:               router.Ourself.Name.String(),
		NickName:           router.Ourself.NickName,
		Port:               router.Port,
		Peers:              makePeerStatusSlice(router.Peers),
		UnicastRoutes:      makeUnicastRouteStatusSlice(router.Routes),
		BroadcastRoutes:    makeBroadcastRouteStatusSlice(router.Routes),
		Connections:        makeLocalConnectionStatusSlice(router.ConnectionMaker),
		TerminationCount:   router.ConnectionMaker.terminationCount,
		Targets:            router.ConnectionMaker.Targets(false),
		OverlayDiagnostics: router.Overlay.Diagnostics(),
		TrustedSubnets:     makeTrustedSubnetsSlice(router.TrustedSubnets),
	}
}

// PeerStatus is the current state of a peer in the mesh.
type PeerStatus struct {
	Name        string
	NickName    string
	UID         PeerUID
	ShortID     PeerShortID
	Version     uint64
	Connections []connectionStatus
}

// makePeerStatusSlice takes a snapshot of the state of peers.
func makePeerStatusSlice(peers *Peers) []PeerStatus {
	var slice []PeerStatus

	peers.forEach(func(peer *Peer) {
		var connections []connectionStatus
		if peer == peers.ourself.Peer {
			for conn := range peers.ourself.getConnections() {
				connections = append(connections, makeConnectionStatus(conn))
			}
		} else {
			// Modifying peer.connections requires a write lock on
			// Peers, and since we are holding a read lock (due to the
			// ForEach), access without locking the peer is safe.
			for _, conn := range peer.connections {
				connections = append(connections, makeConnectionStatus(conn))
			}
		}
		slice = append(slice, PeerStatus{
			peer.Name.String(),
			peer.NickName,
			peer.UID,
			peer.ShortID,
			peer.Version,
			connections,
		})
	})

	return slice
}

type connectionStatus struct {
	Name        string
	NickName    string
	Address     string
	Outbound    bool
	Established bool
}

func makeConnectionStatus(c Connection) connectionStatus {
	return connectionStatus{
		Name:        c.Remote().Name.String(),
		NickName:    c.Remote().NickName,
		Address:     c.remoteTCPAddress(),
		Outbound:    c.isOutbound(),
		Established: c.isEstablished(),
	}
}

// unicastRouteStatus is the current state of an established unicast route.
type unicastRouteStatus struct {
	Dest, Via string
}

// makeUnicastRouteStatusSlice takes a snapshot of the unicast routes in routes.
func makeUnicastRouteStatusSlice(r *routes) []unicastRouteStatus {
	r.RLock()
	defer r.RUnlock()

	var slice []unicastRouteStatus
	for dest, via := range r.unicast {
		slice = append(slice, unicastRouteStatus{dest.String(), via.String()})
	}
	return slice
}

// BroadcastRouteStatus is the current state of an established broadcast route.
type broadcastRouteStatus struct {
	Source string
	Via    []string
}

// makeBroadcastRouteStatusSlice takes a snapshot of the broadcast routes in routes.
func makeBroadcastRouteStatusSlice(r *routes) []broadcastRouteStatus {
	r.RLock()
	defer r.RUnlock()

	var slice []broadcastRouteStatus
	for source, via := range r.broadcast {
		var hops []string
		for _, hop := range via {
			hops = append(hops, hop.String())
		}
		slice = append(slice, broadcastRouteStatus{source.String(), hops})
	}
	return slice
}

// LocalConnectionStatus is the current state of a physical connection to a peer.
type LocalConnectionStatus struct {
	Address  string
	Outbound bool
	State    string
	Info     string
	Attrs    map[string]interface{}
}

// makeLocalConnectionStatusSlice takes a snapshot of the active local
// connections in the ConnectionMaker.
func makeLocalConnectionStatusSlice(cm *connectionMaker) []LocalConnectionStatus {
	resultChan := make(chan []LocalConnectionStatus, 0)
	cm.actionChan <- func() bool {
		var slice []LocalConnectionStatus
		for conn := range cm.connections {
			state := "pending"
			if conn.isEstablished() {
				state = "established"
			}
			lc, _ := conn.(*LocalConnection)
			attrs := lc.OverlayConn.Attrs()
			name, ok := attrs["name"]
			if !ok {
				name = "none"
			}
			info := fmt.Sprintf("%-6v %v", name, conn.Remote())
			if lc.router.usingPassword() {
				if lc.untrusted() {
					info = fmt.Sprintf("%-11v %v", "encrypted", info)
				} else {
					info = fmt.Sprintf("%-11v %v", "unencrypted", info)
				}
			}
			slice = append(slice, LocalConnectionStatus{conn.remoteTCPAddress(), conn.isOutbound(), state, info, attrs})
		}
		for address, target := range cm.targets {
			add := func(state, info string) {
				slice = append(slice, LocalConnectionStatus{address, true, state, info, nil})
			}
			switch target.state {
			case targetWaiting:
				until := "never"
				if !target.tryAfter.IsZero() {
					until = target.tryAfter.String()
				}
				if target.lastError == nil { // shouldn't happen
					add("waiting", "until: "+until)
				} else {
					add("failed", target.lastError.Error()+", retry: "+until)
				}
			case targetAttempting:
				if target.lastError == nil {
					add("connecting", "")
				} else {
					add("retrying", target.lastError.Error())
				}
			case targetConnected:
			case targetSuspended:
			}
		}
		resultChan <- slice
		return false
	}
	return <-resultChan
}

// makeTrustedSubnetsSlice makes a human-readable copy of the trustedSubnets.
func makeTrustedSubnetsSlice(trustedSubnets []*net.IPNet) []string {
	trustedSubnetStrs := []string{}
	for _, trustedSubnet := range trustedSubnets {
		trustedSubnetStrs = append(trustedSubnetStrs, trustedSubnet.String())
	}
	return trustedSubnetStrs
}
