
// peerClient provides functions to create and manage outgoing connections to hosts.
// Each connections has an associated encryption key that is established through a key handshake
// The encryption key encrypts all requests and responses between the host and requestor
// These connections are useful because they negate the need to reconnect and perform a new key handshake with each request to the same host

package peerClient

import "seekia/internal/cryptography/csidh"
import "seekia/internal/cryptography/frodo"
import "seekia/internal/cryptography/mlkem"
import "seekia/internal/cryptography/nacl"
import "seekia/internal/helpers"
import "seekia/internal/mySettings"
import "seekia/internal/network/maliciousHosts"
import "seekia/internal/network/peerConnection"
import "seekia/internal/network/serverRequest"
import "seekia/internal/network/serverResponse"
import "seekia/internal/network/unreachableHosts"
import "seekia/internal/profiles/profileStorage"
import "seekia/internal/profiles/readProfiles"
import "seekia/internal/profiles/viewableProfiles"

import messagepack "github.com/vmihailenco/msgpack/v5"

import "net"
import "net/url"
import "net/netip"
import "sync"
import "time"
import "errors"

type connectionMetadata struct{

	// The host that the connection is made with
	HostIdentityHash [16]byte

	// The key to use to encrypt all communications
	ConnectionKey [32]byte

	// The NetworkType that the connection exists on
	NetworkType byte
}

var hostConnectionObjectsMapMutex sync.RWMutex

// Map structure: Connection Identifier -> Connection object
var hostConnectionObjectsMap map[[21]byte]net.Conn = make(map[[21]byte]net.Conn)

var hostConnectionMetadatasMapMutex sync.RWMutex

// Map structure: Connection Identifier -> Connection Metadata
var hostConnectionMetadatasMap map[[21]byte]connectionMetadata = make(map[[21]byte]connectionMetadata)

func addConnectionToConnectionObjectsMap(connectionIdentifier [21]byte, connectionObject net.Conn){

	hostConnectionObjectsMapMutex.Lock()
	hostConnectionObjectsMap[connectionIdentifier] = connectionObject
	hostConnectionObjectsMapMutex.Unlock()
}

func getConnectionFromConnectionObjectsMap(connectionIdentifier [21]byte)(bool, net.Conn){

	hostConnectionObjectsMapMutex.RLock()
	connectionObject, exists := hostConnectionObjectsMap[connectionIdentifier]
	hostConnectionObjectsMapMutex.RUnlock()

	if (exists == false){
		return false, nil
	}

	return true, connectionObject
}

func deleteConnectionFromConnectionObjectsMap(connectionIdentifier [21]byte){

	hostConnectionObjectsMapMutex.Lock()
	delete(hostConnectionObjectsMap, connectionIdentifier)
	hostConnectionObjectsMapMutex.Unlock()
}

func addMetadataToConnectionMetadatasMap(connectionIdentifier [21]byte, hostIdentityHash [16]byte, connectionKey [32]byte, networkType byte)error{

	connectionMetadataObject := connectionMetadata{
		HostIdentityHash: hostIdentityHash,
		ConnectionKey: connectionKey,
		NetworkType: networkType,
	}

	hostConnectionMetadatasMapMutex.Lock()
	hostConnectionMetadatasMap[connectionIdentifier] = connectionMetadataObject
	hostConnectionMetadatasMapMutex.Unlock()

	return nil
}

func deleteMetadataFromConnectionMetadatasMap(connectionIdentifier [21]byte){

	hostConnectionMetadatasMapMutex.Lock()
	delete(hostConnectionMetadatasMap, connectionIdentifier)
	hostConnectionMetadatasMapMutex.Unlock()
}

//Outputs:
//	-bool: Connection found
//	-[16]byte: Host identity hash
//	-[32]byte: Connection key
//	-byte: Network type
//	-error
func GetConnectionMetadata(connectionIdentifier [21]byte)(bool, [16]byte, [32]byte, byte, error){

	hostConnectionMetadatasMapMutex.RLock()
	connectionMetadataObject, exists := hostConnectionMetadatasMap[connectionIdentifier]
	hostConnectionMetadatasMapMutex.RUnlock()
	if (exists == false){
		return false, [16]byte{}, [32]byte{}, 0, nil
	}

	hostIdentityHash := connectionMetadataObject.HostIdentityHash
	connectionKey := connectionMetadataObject.ConnectionKey
	networkType := connectionMetadataObject.NetworkType

	return true, hostIdentityHash, connectionKey, networkType, nil
}

//Outputs:
//	-bool: Connection successful
//	-[]byte: Response received
//	-error
func SendRequestThroughConnection(connectionIdentifier [21]byte, contentToSend []byte)(bool, []byte, error){

	connectionExists, connectionObject := getConnectionFromConnectionObjectsMap(connectionIdentifier)
	if (connectionExists == false){
		return false, nil, nil
	}

	//TODO: Fix max response size
	requestSuccessful, responseBytes, err := peerConnection.SendRequestThroughConnection(connectionObject, contentToSend, 100000)
	if (err != nil) { return false, nil, err }
	if (requestSuccessful == false){
		return false, nil, nil
	}

	return true, responseBytes, nil
}

func CloseConnection(connectionIdentifier [21]byte)error{

	connectionExists, connectionObject := getConnectionFromConnectionObjectsMap(connectionIdentifier)
	if (connectionExists == false){
		deleteMetadataFromConnectionMetadatasMap(connectionIdentifier)
		return nil
	}

	_ = connectionObject.Close()
	//TODO: Deal with error

	deleteConnectionFromConnectionObjectsMap(connectionIdentifier)
	deleteMetadataFromConnectionMetadatasMap(connectionIdentifier)

	return nil
}

//Outputs:
//	-bool: Host profile found
//	-bool: Connection established
//	-[21]byte: The connection identifier
//	-error
func EstablishNewConnectionToHost(allowClearnet bool, hostIdentityHash [16]byte, networkType byte)(bool, bool, [21]byte, error){

	isValid := helpers.VerifyNetworkType(networkType)
	if (isValid == false){
		networkTypeString := helpers.ConvertByteToString(networkType)
		return false, false, [21]byte{}, errors.New("EstablishNewConnectionToHost called with invalid networkType: " + networkTypeString)
	}

	getHostRawProfileMap := func()(bool, map[int]messagepack.RawMessage, error){

		profileExists, _, _, _, _, hostRawProfileMap, err := viewableProfiles.GetNewestViewableUserProfile(hostIdentityHash, networkType, true, true, true)
		if (err != nil) { return false, nil, err }
		if (profileExists == true){
			return true, hostRawProfileMap, nil
		}

		// Now we check to see if any host profile exists.
		// This would be true if host's profile is banned.
		// We will query banned hosts if there are not enough viewable hosts available

		profileExists, _, _, _, _, hostRawProfileMap, err = profileStorage.GetNewestUserProfile(hostIdentityHash, networkType)
		if (err != nil) { return false, nil, err }
		if (profileExists == true){
			return true, hostRawProfileMap, nil	
		}

		return false, nil, nil
	}

	hostProfileExists, hostRawProfileMap, err := getHostRawProfileMap()
	if (err != nil) { return false, false, [21]byte{}, err }
	if (hostProfileExists == false){
		// This should only happen if host profile was deleted after eligible hosts list was created
		return false, false, [21]byte{}, nil
	}


	profileIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "Disabled")
	if (err != nil) { return false, false, [21]byte{}, err }
	if (profileIsDisabled == true){
		// Profile is disabled.
		return false, false, [21]byte{}, nil
	}

	// This function will return the address and method we will use to contact the host
	//
	// If allowClearnet is false, function will only send request over Tor
	// If allowClearnet is true, function will determine if we are in HostOverClearnet mode
	// If we are, function will see if Host has a clearnet address and send request to that
	// Otherwise, function will send request to host's .onion address over tor
	//
	// Sending over Tor prefers .onion address, but will connect to host's clearnet address over Tor exit node if onion address is offline or not provided
	// Onion address could be offline due to DDOS attack, too many requests, or host network is blocking Tor access
	// A host's client should automatically disable the .onion address if it is not reachable
	// We will probably want to prefer connecting to clearnet URLs over Tor because .onion domains have become much slower due to recent spam attacks
	//
	//Outputs:
	//	-string: Method to use (Tor/Clearnet)
	//	-string: Address type (Tor/Clearnet)
	//	-string: Address to contact
	//	-int: Address port (if addressType is clearnet)
	//	-error
	getHostAddressAndMethod := func()(string, string, string, int, error){

		//Outputs:
		//	-bool: Host has tor address
		//	-string: Host tor address
		//	-error: Host 
		getHostTorAddress := func()(bool, string, error){

			exists, hostTorAddress, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "TorAddress")
			if (err != nil) { return false, "", err }
			if (exists == false) {
				return false, "", nil
			}
			return true, hostTorAddress, nil
		}

		//Outputs:
		//	-bool: Host clearnet address exists
		//	-string: Host clearnet address
		//	-int: Host clearnet port
		//	-error
		getHostClearnetAddress := func()(bool, string, int, error){

			exists, clearnetAddress, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "ClearnetAddress")
			if (err != nil) { return false, "", 0, err }
			if (exists == false) {
				return false, "", 0, nil
			}

			exists, clearnetPort, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "ClearnetPort")
			if (err != nil) { return false, "", 0, err }
			if (exists == false) {
				return false, "", 0, errors.New("Host profile with ClearnetAddress missing ClearnetPort")
			}

			clearnetPortInt, err := helpers.ConvertStringToInt(clearnetPort)
			if (err != nil) {
				return false, "", 0, errors.New("Database corrupt: Contains host profile with invalid ClearnetPort: " + clearnetPort)
			}

			return true, clearnetAddress, clearnetPortInt, nil
		}

		hostHasTorAddress, hostTorAddress, err := getHostTorAddress()
		if (err != nil) { return "", "", "", 0, err }

		hostHasClearnetAddress, hostClearnetAddress, hostClearnetPort, err := getHostClearnetAddress()
		if (err != nil) { return "", "", "", 0, err }

		if (hostHasTorAddress == false && hostHasClearnetAddress == false){
			return "", "", "", 0, errors.New("Database corrupt: Contains host profile missing Tor and Clearnet addresses.")
		}

		hostIsUnreachableClearnet, err := unreachableHosts.CheckIfHostIsUnreachable(hostIdentityHash, networkType, "Clearnet")
		if (err != nil) { return "", "", "", 0, err }

		hostIsUnreachableTor, err := unreachableHosts.CheckIfHostIsUnreachable(hostIdentityHash, networkType, "Tor")
		if (err != nil) { return "", "", "", 0, err }

		// If host is unreachable from both addresses, we will assume that eligibleHosts provided the host because there were not enough reachable hosts available

		getHostIsFullyUnreachableStatus := func()bool{

			if (hostIsUnreachableClearnet == true && hostIsUnreachableTor == true){
				return true
			}
			return false
		}

		hostIsFullyUnreachable := getHostIsFullyUnreachableStatus()

		if (allowClearnet == true && hostHasClearnetAddress == true){

			checkIfHostOverClearnetModeIsEnabled := func()(bool, error){

				exists, hostModeOnOffStatus, err := mySettings.GetSetting("HostModeOnOffStatus")
				if (err != nil) { return false, err }
				if (exists == false){
					return false, nil
				}

				if (hostModeOnOffStatus != "On"){
					return false, nil
				}

				exists, hostOverClearnetStatus, err := mySettings.GetSetting("HostOverClearnetOnOffStatus")
				if (err != nil) { return false, err }
				if (exists == false){
					return false, nil
				}
				if (hostOverClearnetStatus != "On"){
					return false, nil
				}

				//TODO: Check for ModerateOverClearnet mode

				return true, nil
			}

			hostOverClearnetModeEnabledStatus, err := checkIfHostOverClearnetModeIsEnabled()
			if (err != nil) { return "", "", "", 0, err }

			if (hostOverClearnetModeEnabledStatus == true){

				if (hostIsUnreachableClearnet == false || hostIsFullyUnreachable == true){

					return "Clearnet", "Clearnet", hostClearnetAddress, hostClearnetPort, nil
				}
			}
		}

		// Either hostOverClearnet is disabled or recipient does not have reachable clearnet address
		// We will send request over tor
		// We prefer .onion address but will also try to access their clearnet address over tor if .onion is unreachable/nonexistent

		if (hostHasTorAddress == true){

			if (hostIsUnreachableTor == false || hostIsFullyUnreachable == true){
		
				return "Tor", "Tor", hostTorAddress, 0, nil
			}
		}

		return "Tor", "Clearnet", hostClearnetAddress, hostClearnetPort, nil
	}

	methodToUse, hostAddressType, hostAddress, addressPort, err := getHostAddressAndMethod()
	if (err != nil) { return false, false, [21]byte{}, err }

	getFormattedAddressToContact := func()(string, error){

		if (hostAddressType == "Tor"){
			return hostAddress, nil
		}
		hostIPAddressObject, err := netip.ParseAddr(hostAddress)
		if (err == nil){

			portString := helpers.ConvertIntToString(addressPort)

			addressIsIpv6 := hostIPAddressObject.Is6()
			if (addressIsIpv6 == false){
				formattedAddress := hostAddress + ":" + portString
				return formattedAddress, nil
			}
			formattedAddress := "[" + hostAddress + "]" + ":" + portString
			return formattedAddress, nil
		}

		// Address must be a clearnet URL, rather than an IP address
		addressObject, err := url.Parse(hostAddress)
		if (err != nil){
			// Address is not IP or clearnet. The profile is invalid.
			return "", errors.New("Database corrupt: Contains host profile with invalid clearnetAddress: " + hostAddress)
		}

		addressHost := addressObject.Host
		formattedAddress := addressHost + ":http"

		return formattedAddress, nil
	}

	formattedAddressToContact, err := getFormattedAddressToContact()
	if (err != nil){ return false, false, [21]byte{}, err }

	//Outputs:
	//	-bool: Connection successful
	//	-net.Conn: Connection object
	//	-error
	getConnectionObject := func()(bool, net.Conn, error){

		if (methodToUse == "Tor"){
			//TODO
			return false, nil, nil
		}
		// methodToUse == "Clearnet"	

		newDialer := &net.Dialer{
			Timeout: time.Minute,
		}
		connectionObject, err := newDialer.Dial("tcp", formattedAddressToContact)
		if (err != nil) { 
			//TODO: Distinguish between network error and other types of errors
			return false, nil, nil
		}
		return true, connectionObject, nil
	}

	connectionSuccessful, connectionObject, err := getConnectionObject()
	if (err != nil) { return false, false, [21]byte{}, err }
	if (connectionSuccessful == false){
		return true, false, [21]byte{}, nil
	}

	//Outputs:
	//	-bool: Handshake successful
	//	-[32]byte: Connection key
	//	-error
	performKeyHandshake := func()(bool, [32]byte, error){

		myNaclPublicKey, myNaclPrivateKey, err := nacl.GetNewRandomPublicPrivateNaclKeys()
		if (err != nil) { return false, [32]byte{}, err }

		myMlkemPublicKey, myMlkemPrivateKey, err := mlkem.GetNewRandomPublicPrivateMlkemKeys()
		if (err != nil) { return false, [32]byte{}, err }

		myCsidhPublicKey, myCsidhPrivateKey, err := csidh.GetNewRandomPublicPrivateCsidhKeys()
		if (err != nil) { return false, [32]byte{}, err }

		myFrodoPublicKey, myFrodoPrivateKey, err := frodo.GetNewRandomPublicPrivateFrodoKeys()
		if (err != nil) { return false, [32]byte{}, err }

		requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_EstablishConnectionKey(hostIdentityHash, networkType, myNaclPublicKey, myMlkemPublicKey, myCsidhPublicKey, myFrodoPublicKey)
		if (err != nil) { return false, [32]byte{}, err }

		//TODO: Fix max response size
		requestSuccessful, responseBytes, err := peerConnection.SendRequestThroughConnection(connectionObject, requestBytes, 100000)
		if (err != nil) { return false, [32]byte{}, err }
		if (requestSuccessful == false){
			return false, [32]byte{}, nil
		}

		//TODO: Add ability to handle busy response/different network type response
		// If host is busy, we will keep track of that in a new package and not try to recontact them for a while

		ableToRead, requestIdentifier_Received, hostIdentityHash_Received, connectionKey, err := serverResponse.ReadServerResponse_EstablishConnectionKey(responseBytes, myNaclPublicKey, myNaclPrivateKey, myMlkemPrivateKey, myCsidhPrivateKey, myFrodoPrivateKey)
		if (err != nil) { return false, [32]byte{}, err }
		if (ableToRead == false){
			// Peer sent invalid response, must be malicious

			err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash)
			if (err != nil){ return false, [32]byte{}, err }

			return false, [32]byte{}, nil
		}

		checkIfResponseIsValid := func()bool{

			if (requestIdentifier_Received != requestIdentifier){
				return false
			}
			if (hostIdentityHash_Received != hostIdentityHash){
				return false
			}
			return true
		}

		responseIsValid := checkIfResponseIsValid()
		if (responseIsValid == false){

			err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash)
			if (err != nil){ return false, [32]byte{}, err }
		}

		return true, connectionKey, nil
	}

	handshakeSuccessful, connectionKey, err := performKeyHandshake()
	if (err != nil){
		_ = connectionObject.Close()
		//TODO: Deal with possible errors
		return false, false, [21]byte{}, err
	}
	if (handshakeSuccessful == false){
		_ = connectionObject.Close()
		//TODO: Deal with possible errors
		return true, false, [21]byte{}, nil
	}

	newConnectionIdentifier, err := helpers.GetNewRandomBytes(21)
	if (err != nil) { return false, false, [21]byte{}, err }

	newConnectionIdentifierArray := [21]byte(newConnectionIdentifier)

	addConnectionToConnectionObjectsMap(newConnectionIdentifierArray, connectionObject)
	addMetadataToConnectionMetadatasMap(newConnectionIdentifierArray, hostIdentityHash, connectionKey, networkType)

	return true, true, newConnectionIdentifierArray, nil
}



