package ipfscluster

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"net"

	blake2b "golang.org/x/crypto/blake2b"

	"github.com/ipfs-cluster/ipfs-cluster/api"
	peer "github.com/libp2p/go-libp2p/core/peer"
	ma "github.com/multiformats/go-multiaddr"
	madns "github.com/multiformats/go-multiaddr-dns"
)

// PeersFromMultiaddrs returns all the different peers in the given addresses.
// each peer only will appear once in the result, even if several
// multiaddresses for it are provided.
func PeersFromMultiaddrs(addrs []ma.Multiaddr) []peer.ID {
	var pids []peer.ID
	pm := make(map[peer.ID]struct{})
	for _, addr := range addrs {
		pinfo, err := peer.AddrInfoFromP2pAddr(addr)
		if err != nil {
			continue
		}
		_, ok := pm[pinfo.ID]
		if !ok {
			pm[pinfo.ID] = struct{}{}
			pids = append(pids, pinfo.ID)
		}
	}
	return pids
}

// // connect to a peer ID.
// func connectToPeer(ctx context.Context, h host.Host, id peer.ID, addr ma.Multiaddr) error {
// 	err := h.Connect(ctx, peerstore.PeerInfo{
// 		ID:    id,
// 		Addrs: []ma.Multiaddr{addr},
// 	})
// 	return err
// }

// // return the local multiaddresses used to communicate to a peer.
// func localMultiaddrsTo(h host.Host, pid peer.ID) []ma.Multiaddr {
// 	var addrs []ma.Multiaddr
// 	conns := h.Network().ConnsToPeer(pid)
// 	logger.Debugf("conns to %s are: %s", pid, conns)
// 	for _, conn := range conns {
// 		addrs = append(addrs, multiaddrJoin(conn.LocalMultiaddr(), h.ID()))
// 	}
// 	return addrs
// }

func logError(fmtstr string, args ...interface{}) error {
	msg := fmt.Sprintf(fmtstr, args...)
	logger.Error(msg)
	return errors.New(msg)
}

func containsPeer(list []peer.ID, peer peer.ID) bool {
	for _, p := range list {
		if p == peer {
			return true
		}
	}
	return false
}

func minInt(x, y int) int {
	if x < y {
		return x
	}
	return y
}

// // updatePinParents modifies the api.Pin input to give it the correct parents
// // so that previous additions to the pins parents are maintained after this
// // pin is committed to consensus.  If this pin carries new parents they are
// // merged with those already existing for this CID.
// func updatePinParents(pin *api.Pin, existing *api.Pin) {
// 	// no existing parents this pin is up to date
// 	if existing.Parents == nil || len(existing.Parents.Keys()) == 0 {
// 		return
// 	}
// 	for _, c := range existing.Parents.Keys() {
// 		pin.Parents.Add(c)
// 	}
// }

type distance [blake2b.Size256]byte

type distanceChecker struct {
	local      peer.ID
	otherPeers []peer.ID
	cache      map[peer.ID]distance
}

func (dc distanceChecker) isClosest(ci api.Cid) bool {
	ciHash := convertKey(ci.KeyString())
	localPeerHash := dc.convertPeerID(dc.local)
	myDistance := xor(ciHash, localPeerHash)

	for _, p := range dc.otherPeers {
		peerHash := dc.convertPeerID(p)
		distance := xor(peerHash, ciHash)

		// if myDistance is larger than for other peers...
		if bytes.Compare(myDistance[:], distance[:]) > 0 {
			return false
		}
	}
	return true
}

// convertPeerID hashes a Peer ID (Multihash).
func (dc distanceChecker) convertPeerID(id peer.ID) distance {
	hash, ok := dc.cache[id]
	if ok {
		return hash
	}

	hashBytes := convertKey(string(id))
	dc.cache[id] = hashBytes
	return hashBytes
}

// convertKey hashes a key.
func convertKey(id string) distance {
	return blake2b.Sum256([]byte(id))
}

func xor(a, b distance) distance {
	var c distance
	for i := 0; i < len(c); i++ {
		c[i] = a[i] ^ b[i]
	}
	return c
}

// peersSubtract subtracts peers ID slice b from peers ID slice a.
func peersSubtract(a []peer.ID, b []peer.ID) []peer.ID {
	var result []peer.ID
	bMap := make(map[peer.ID]struct{}, len(b))

	for _, p := range b {
		bMap[p] = struct{}{}
	}

	for _, p := range a {
		_, ok := bMap[p]
		if ok {
			continue
		}
		result = append(result, p)
	}

	return result
}

// pingValue describes the value carried by ping metrics
type pingValue struct {
	Peername      string          `json:"peer_name,omitempty"`
	IPFSID        peer.ID         `json:"ipfs_id,omitempty"`
	IPFSAddresses []api.Multiaddr `json:"ipfs_addresses,omitempty"`
}

// Valid returns true if the PingValue has IPFSID set.
func (pv pingValue) Valid() bool {
	return pv.IPFSID != ""
}

// PingValue from metric parses a ping value from the value of a given metric,
// if possible.
func pingValueFromMetric(m api.Metric) (pv pingValue) {
	json.Unmarshal([]byte(m.Value), &pv)
	return
}

func publicIPFSAddresses(in []api.Multiaddr) []api.Multiaddr {
	var out []api.Multiaddr
	for _, maddr := range in {
		if madns.Matches(maddr.Value()) { // a dns multiaddress: take it
			out = append(out, maddr)
			continue
		}

		ip, err := maddr.ValueForProtocol(ma.P_IP4)
		if err != nil {
			ip, err = maddr.ValueForProtocol(ma.P_IP6)
			if err != nil {
				continue
			}
		}
		// We have an IP in the multiaddress. Only include
		// global unicast.
		netip := net.ParseIP(ip)
		if netip == nil {
			continue
		}

		if !netip.IsGlobalUnicast() {
			continue
		}
		out = append(out, maddr)
	}
	return out
}