//TODO: Consider restructuring  this file/folder    "github.com/gorilla/handlers"

//It may make more sense to split into different files and not call it "helpers"

package functions

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"math/rand"
	"net"
	"strings"
	"time"

	"github.com/gravitl/netmaker/database"
	"github.com/gravitl/netmaker/models"
	"github.com/gravitl/netmaker/servercfg"
)

// PrintUserLog - prints a log with a given username
func PrintUserLog(username string, message string, loglevel int) {
	log.SetFlags(log.Flags() &^ (log.Llongfile | log.Lshortfile))
	if int32(loglevel) <= servercfg.GetVerbose() && servercfg.GetVerbose() != 0 {
		log.Println("[netmaker]", username, message)
	}
}

// ParseNetwork - parses a network into a model
func ParseNetwork(value string) (models.Network, error) {
	var network models.Network
	err := json.Unmarshal([]byte(value), &network)
	return network, err
}

// ParseNode - parses a node into a model
func ParseNode(value string) (models.Node, error) {
	var node models.Node
	err := json.Unmarshal([]byte(value), &node)
	return node, err
}

// ParseExtClient - parses an extclient into a model
func ParseExtClient(value string) (models.ExtClient, error) {
	var extClient models.ExtClient
	err := json.Unmarshal([]byte(value), &extClient)
	return extClient, err
}

// ParseIntClient - parses int client
func ParseIntClient(value string) (models.IntClient, error) {
	var intClient models.IntClient
	err := json.Unmarshal([]byte(value), &intClient)
	return intClient, err
}

//Takes in an arbitrary field and value for field and checks to see if any other
//node has that value for the same field within the network

// GetUser - gets a user
func GetUser(username string) (models.User, error) {

	var user models.User
	record, err := database.FetchRecord(database.USERS_TABLE_NAME, username)
	if err != nil {
		return user, err
	}
	if err = json.Unmarshal([]byte(record), &user); err != nil {
		return models.User{}, err
	}
	return user, err
}

// SliceContains - sees if a slice contains something
func SliceContains(slice []string, item string) bool {
	set := make(map[string]struct{}, len(slice))
	for _, s := range slice {
		set[s] = struct{}{}
	}

	_, ok := set[item]
	return ok
}

// CreateServerToken - creates a server token
func CreateServerToken(netID string) (string, error) {
	var network models.Network
	var accesskey models.AccessKey

	network, err := GetParentNetwork(netID)
	if err != nil {
		return "", err
	}

	var accessToken models.AccessToken
	servervals := models.ServerConfig{}
	if servercfg.GetPlatform() == "Kubernetes" {
		log.Println("server on kubernetes")
		servervals = models.ServerConfig{
			APIConnString:  servercfg.GetPodIP() + ":" + servercfg.GetAPIPort(),
			GRPCConnString: servercfg.GetPodIP() + ":" + servercfg.GetGRPCPort(),
			GRPCSSL:        "off",
		}
	} else {
		log.Println("server on linux")
		servervals = models.ServerConfig{
			APIConnString:   "127.0.0.1:" + servercfg.GetAPIPort(),
			GRPCConnString:  "127.0.0.1:" + servercfg.GetGRPCPort(),
			GRPCSSL:         "off",
			CheckinInterval: servercfg.GetCheckinInterval(),
		}
	}
	log.Println("APIConnString:", servervals.APIConnString)
	log.Println("GRPCConnString:", servervals.GRPCConnString)
	log.Println("GRPCSSL:", servervals.GRPCSSL)
	accessToken.ServerConfig = servervals
	accessToken.ClientConfig.Network = netID
	accessToken.ClientConfig.Key = GenKey()

	accesskey.Name = GenKeyName()
	accesskey.Value = accessToken.ClientConfig.Key
	accesskey.Uses = 1
	tokenjson, err := json.Marshal(accessToken)
	if err != nil {
		return accesskey.AccessString, err
	}
	accesskey.AccessString = base64.StdEncoding.EncodeToString([]byte(tokenjson))
	log.Println("accessstring:", accesskey.AccessString)
	network.AccessKeys = append(network.AccessKeys, accesskey)
	if data, err := json.Marshal(network); err != nil {
		return "", err
	} else {
		database.Insert(netID, string(data), database.NETWORKS_TABLE_NAME)
	}

	return accesskey.AccessString, nil
}

// GetPeersList - gets peers for given network
func GetPeersList(networkName string) ([]models.PeersResponse, error) {

	var peers []models.PeersResponse
	collection, err := database.FetchRecords(database.NODES_TABLE_NAME)
	if err != nil {
		return peers, err
	}

	for _, value := range collection {

		var peer models.PeersResponse
		err := json.Unmarshal([]byte(value), &peer)
		if err != nil {
			continue // try the rest
		}
		peers = append(peers, peer)
	}

	return peers, err
}

// GetIntPeersList - get int peers list
func GetIntPeersList() ([]models.PeersResponse, error) {

	var peers []models.PeersResponse
	records, err := database.FetchRecords(database.INT_CLIENTS_TABLE_NAME)

	if err != nil {
		return peers, err
	}
	// parse the peers

	for _, value := range records {

		var peer models.PeersResponse
		err := json.Unmarshal([]byte(value), &peer)
		if err != nil {
			log.Fatal(err)
		}
		// add the node to our node array
		//maybe better to just return this? But then that's just GetNodes...
		peers = append(peers, peer)
	}

	return peers, err
}

// GetServerIntClient - get server int client
func GetServerIntClient() (*models.IntClient, error) {

	intClients, err := database.FetchRecords(database.INT_CLIENTS_TABLE_NAME)
	for _, value := range intClients {
		var intClient models.IntClient
		err = json.Unmarshal([]byte(value), &intClient)
		if err != nil {
			return nil, err
		}
		if intClient.IsServer == "yes" && intClient.Network == "comms" {
			return &intClient, nil
		}
	}
	return nil, err
}

// NetworkExists - check if network exists
func NetworkExists(name string) (bool, error) {

	var network string
	var err error
	if network, err = database.FetchRecord(database.NETWORKS_TABLE_NAME, name); err != nil {
		return false, err
	}
	return len(network) > 0, nil
}

// GetRecordKey - get record key
func GetRecordKey(id string, network string) (string, error) {
	if id == "" || network == "" {
		return "", errors.New("unable to get record key")
	}
	return id + "###" + network, nil
}

// UpdateNetworkNodeAddresses - updates network node addresses
func UpdateNetworkNodeAddresses(networkName string) error {

	collections, err := database.FetchRecords(database.NODES_TABLE_NAME)
	if err != nil {
		return err
	}

	for _, value := range collections {

		var node models.Node
		err := json.Unmarshal([]byte(value), &node)
		if err != nil {
			fmt.Println("error in node address assignment!")
			return err
		}
		if node.Network == networkName {
			ipaddr, iperr := UniqueAddress(networkName)
			if iperr != nil {
				fmt.Println("error in node  address assignment!")
				return iperr
			}

			node.Address = ipaddr
			node.PullChanges = "yes"
			data, err := json.Marshal(&node)
			if err != nil {
				return err
			}
			node.SetID()
			database.Insert(node.ID, string(data), database.NODES_TABLE_NAME)
		}
	}

	return nil
}

// NetworkNodesUpdateAction - updates action of network nodes
func NetworkNodesUpdateAction(networkName string, action string) error {

	collections, err := database.FetchRecords(database.NODES_TABLE_NAME)
	if err != nil {
		if database.IsEmptyRecord(err) {
			return nil
		}
		return err
	}

	for _, value := range collections {
		var node models.Node
		err := json.Unmarshal([]byte(value), &node)
		if err != nil {
			fmt.Println("error in node address assignment!")
			return err
		}
		if action == models.NODE_UPDATE_KEY && node.IsStatic == "yes" {
			continue
		}
		if node.Network == networkName {
			node.Action = action
			data, err := json.Marshal(&node)
			if err != nil {
				return err
			}
			node.SetID()
			database.Insert(node.ID, string(data), database.NODES_TABLE_NAME)
		}
	}
	return nil
}

// NetworkNodesUpdatePullChanges - tells nodes on network to pull
func NetworkNodesUpdatePullChanges(networkName string) error {

	collections, err := database.FetchRecords(database.NODES_TABLE_NAME)
	if err != nil {
		if database.IsEmptyRecord(err) {
			return nil
		}
		return err
	}

	for _, value := range collections {
		var node models.Node
		err := json.Unmarshal([]byte(value), &node)
		if err != nil {
			fmt.Println("error in node address assignment!")
			return err
		}
		if node.Network == networkName {
			node.PullChanges = "yes"
			data, err := json.Marshal(&node)
			if err != nil {
				return err
			}
			node.SetID()
			database.Insert(node.ID, string(data), database.NODES_TABLE_NAME)
		}
	}

	return nil
}

// UpdateNetworkLocalAddresses - updates network localaddresses
func UpdateNetworkLocalAddresses(networkName string) error {

	collection, err := database.FetchRecords(database.NODES_TABLE_NAME)

	if err != nil {
		return err
	}

	for _, value := range collection {

		var node models.Node

		err := json.Unmarshal([]byte(value), &node)
		if err != nil {
			fmt.Println("error in node address assignment!")
			return err
		}
		if node.Network == networkName {
			ipaddr, iperr := UniqueAddress(networkName)
			if iperr != nil {
				fmt.Println("error in node  address assignment!")
				return iperr
			}

			node.Address = ipaddr
			newNodeData, err := json.Marshal(&node)
			if err != nil {
				fmt.Println("error in node  address assignment!")
				return err
			}
			node.SetID()
			database.Insert(node.ID, string(newNodeData), database.NODES_TABLE_NAME)
		}
	}

	return nil
}

// IsNetworkDisplayNameUnique - checks if network display name unique
func IsNetworkDisplayNameUnique(name string) (bool, error) {

	isunique := true

	dbs, err := models.GetNetworks()
	if err != nil {
		return database.IsEmptyRecord(err), err
	}

	for i := 0; i < len(dbs); i++ {

		if name == dbs[i].DisplayName {
			isunique = false
		}
	}

	return isunique, nil
}

// IsMacAddressUnique - checks if mac is unique
func IsMacAddressUnique(macaddress string, networkName string) (bool, error) {

	_, err := database.FetchRecord(database.NODES_TABLE_NAME, macaddress+"###"+networkName)
	if err != nil {
		return database.IsEmptyRecord(err), err
	}

	return true, nil
}

// GetNetworkNonServerNodeCount - get number of network non server nodes
func GetNetworkNonServerNodeCount(networkName string) (int, error) {

	collection, err := database.FetchRecords(database.NODES_TABLE_NAME)
	count := 0
	if err != nil && !database.IsEmptyRecord(err) {
		return count, err
	}
	for _, value := range collection {
		var node models.Node
		if err = json.Unmarshal([]byte(value), &node); err != nil {
			return count, err
		} else {
			if node.Network == networkName && node.IsServer != "yes" {
				count++
			}
		}
	}

	return count, nil
}

//Checks to see if access key is valid
//Does so by checking against all keys and seeing if any have the same value
//may want to hash values before comparing...consider this
//TODO: No error handling!!!!

// IsKeyValid - check if key is valid
func IsKeyValid(networkname string, keyvalue string) bool {

	network, _ := GetParentNetwork(networkname)
	var key models.AccessKey
	foundkey := false
	isvalid := false

	for i := len(network.AccessKeys) - 1; i >= 0; i-- {
		currentkey := network.AccessKeys[i]
		if currentkey.Value == keyvalue {
			key = currentkey
			foundkey = true
		}
	}
	if foundkey {
		if key.Uses > 0 {
			isvalid = true
		}
	}
	return isvalid
}

// IsKeyValidGlobal - checks if a key is valid globally
func IsKeyValidGlobal(keyvalue string) bool {

	networks, _ := models.GetNetworks()
	var key models.AccessKey
	foundkey := false
	isvalid := false
	for _, network := range networks {
		for i := len(network.AccessKeys) - 1; i >= 0; i-- {
			currentkey := network.AccessKeys[i]
			if currentkey.Value == keyvalue {
				key = currentkey
				foundkey = true
				break
			}
		}
		if foundkey {
			break
		}
	}
	if foundkey {
		if key.Uses > 0 {
			isvalid = true
		}
	}
	return isvalid
}

//TODO: Contains a fatal error return. Need to change
//This just gets a network object from a network name
//Should probably just be GetNetwork. kind of a dumb name.
//Used in contexts where it's not the Parent network.

// GetParentNetwork - get parent network
func GetParentNetwork(networkname string) (models.Network, error) {

	var network models.Network
	networkData, err := database.FetchRecord(database.NETWORKS_TABLE_NAME, networkname)
	if err != nil {
		return network, err
	}
	if err = json.Unmarshal([]byte(networkData), &network); err != nil {
		return models.Network{}, err
	}
	return network, nil
}

// IsIpNet - checks if valid ip
func IsIpNet(host string) bool {
	return net.ParseIP(host) != nil
}

//Similar to above but checks if Cidr range is valid
//At least this guy's got some print statements
//still not good error handling

// IsIpCIDR - IsIpCIDR
func IsIpCIDR(host string) bool {

	ip, ipnet, err := net.ParseCIDR(host)

	if err != nil {
		fmt.Println(err)
		fmt.Println("Address Range is not valid!")
		return false
	}

	return ip != nil && ipnet != nil
}

//This  checks to  make sure a network name is valid.
//Switch to REGEX?

// NameInNetworkCharSet - see if name is in charset for networks
func NameInNetworkCharSet(name string) bool {

	charset := "abcdefghijklmnopqrstuvwxyz1234567890-_."

	for _, char := range name {
		if !strings.Contains(charset, strings.ToLower(string(char))) {
			return false
		}
	}
	return true
}

// NameInDNSCharSet - name in dns char set
func NameInDNSCharSet(name string) bool {

	charset := "abcdefghijklmnopqrstuvwxyz1234567890-."

	for _, char := range name {
		if !strings.Contains(charset, strings.ToLower(string(char))) {
			return false
		}
	}
	return true
}

// NameInNodeCharSet - name in node char set
func NameInNodeCharSet(name string) bool {

	charset := "abcdefghijklmnopqrstuvwxyz1234567890-"

	for _, char := range name {
		if !strings.Contains(charset, strings.ToLower(string(char))) {
			return false
		}
	}
	return true
}

//This returns a node based on its mac address.
//The mac address acts as the Unique ID for nodes.
//Is this a dumb thing to do? I thought it was cool but maybe it's dumb.
//It doesn't really provide a tangible benefit over a random ID

// GetNodeByMacAddress - gets a node by mac address
func GetNodeByMacAddress(network string, macaddress string) (models.Node, error) {

	var node models.Node

	key, err := GetRecordKey(macaddress, network)
	if err != nil {
		return node, err
	}

	record, err := database.FetchRecord(database.NODES_TABLE_NAME, key)
	if err != nil {
		return models.Node{}, err
	}

	if err = json.Unmarshal([]byte(record), &node); err != nil {
		return models.Node{}, err
	}

	node.SetDefaults()

	return node, nil
}

// GetDeletedNodeByMacAddress - get a deleted node
func GetDeletedNodeByMacAddress(network string, macaddress string) (models.Node, error) {

	var node models.Node

	key, err := GetRecordKey(macaddress, network)
	if err != nil {
		return node, err
	}

	record, err := database.FetchRecord(database.DELETED_NODES_TABLE_NAME, key)
	if err != nil {
		return models.Node{}, err
	}

	if err = json.Unmarshal([]byte(record), &node); err != nil {
		return models.Node{}, err
	}

	node.SetDefaults()

	return node, nil
}

// RemoveDeletedNode - remove deleted node
func RemoveDeletedNode(nodeid string) bool {
	return database.DeleteRecord(database.DELETED_NODES_TABLE_NAME, nodeid) == nil
}

// DeleteAllIntClients - delete all int clients
func DeleteAllIntClients() error {
	err := database.DeleteAllRecords(database.INT_CLIENTS_TABLE_NAME)
	if err != nil {
		return err
	}
	return nil
}

// GetAllIntClients - get all int clients
func GetAllIntClients() ([]models.IntClient, error) {
	var clients []models.IntClient
	collection, err := database.FetchRecords(database.INT_CLIENTS_TABLE_NAME)

	if err != nil {
		return clients, err
	}

	for _, value := range collection {
		var client models.IntClient
		err := json.Unmarshal([]byte(value), &client)
		if err != nil {
			return []models.IntClient{}, err
		}
		// add node to our array
		clients = append(clients, client)
	}

	return clients, nil
}

// GetAllExtClients - get all ext clients
func GetAllExtClients() ([]models.ExtClient, error) {
	var extclients []models.ExtClient
	collection, err := database.FetchRecords(database.EXT_CLIENT_TABLE_NAME)

	if err != nil {
		return extclients, err
	}

	for _, value := range collection {
		var extclient models.ExtClient
		err := json.Unmarshal([]byte(value), &extclient)
		if err != nil {
			return []models.ExtClient{}, err
		}
		// add node to our array
		extclients = append(extclients, extclient)
	}

	return extclients, nil
}

//This returns a unique address for a node to use
//it iterates through the list of IP's in the subnet
//and checks against all nodes to see if it's taken, until it finds one.
//TODO: We do not handle a case where we run out of addresses.
//We will need to handle that eventually

// UniqueAddress - see if address is unique
func UniqueAddress(networkName string) (string, error) {

	var network models.Network
	network, err := GetParentNetwork(networkName)
	if err != nil {
		fmt.Println("UniqueAddress encountered  an error")
		return "666", err
	}

	offset := true
	ip, ipnet, err := net.ParseCIDR(network.AddressRange)
	if err != nil {
		fmt.Println("UniqueAddress encountered  an error")
		return "666", err
	}
	for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); Inc(ip) {
		if offset {
			offset = false
			continue
		}
		if networkName == "comms" {
			if IsIPUnique(networkName, ip.String(), database.INT_CLIENTS_TABLE_NAME, false) {
				return ip.String(), err
			}
		} else {
			if IsIPUnique(networkName, ip.String(), database.NODES_TABLE_NAME, false) && IsIPUnique(networkName, ip.String(), database.EXT_CLIENT_TABLE_NAME, false) {
				return ip.String(), err
			}
		}
	}

	//TODO
	err1 := errors.New("ERROR: No unique addresses available. Check network subnet.")
	return "W1R3: NO UNIQUE ADDRESSES AVAILABLE", err1
}

// UniqueAddress6 - see if ipv6 address is unique
func UniqueAddress6(networkName string) (string, error) {

	var network models.Network
	network, err := GetParentNetwork(networkName)
	if err != nil {
		fmt.Println("Network Not Found")
		return "", err
	}
	if network.IsDualStack == "no" {
		return "", nil
	}

	offset := true
	ip, ipnet, err := net.ParseCIDR(network.AddressRange6)
	if err != nil {
		fmt.Println("UniqueAddress6 encountered  an error")
		return "666", err
	}
	for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); Inc(ip) {
		if offset {
			offset = false
			continue
		}
		if IsIPUnique(networkName, ip.String(), database.NODES_TABLE_NAME, true) {
			return ip.String(), err
		}
	}
	//TODO
	err1 := errors.New("ERROR: No unique addresses available. Check network subnet.")
	return "W1R3: NO UNIQUE ADDRESSES AVAILABLE", err1
}

// GenKey - generates access key
func GenKey() string {

	var seededRand *rand.Rand = rand.New(
		rand.NewSource(time.Now().UnixNano()))

	length := 16
	charset := "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

	b := make([]byte, length)
	for i := range b {
		b[i] = charset[seededRand.Intn(len(charset))]
	}
	return string(b)
}

//generate a key value
//we should probably just have 1 random string generator
//that  can be used across all functions
//have a "base string" a "length" and a "charset"

// GenKeyName - generates a key name
func GenKeyName() string {

	var seededRand *rand.Rand = rand.New(
		rand.NewSource(time.Now().UnixNano()))

	length := 5
	charset := "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

	b := make([]byte, length)
	for i := range b {
		b[i] = charset[seededRand.Intn(len(charset))]
	}
	return "key" + string(b)
}

// IsIPUnique - checks if an IP is unique
func IsIPUnique(network string, ip string, tableName string, isIpv6 bool) bool {

	isunique := true
	collection, err := database.FetchRecords(tableName)

	if err != nil {
		return isunique
	}

	for _, value := range collection { // filter
		var node models.Node
		if err = json.Unmarshal([]byte(value), &node); err != nil {
			continue
		}
		if isIpv6 {
			if node.Address6 == ip && node.Network == network {
				return false
			}
		} else {
			if node.Address == ip && node.Network == network {
				return false
			}
		}
	}

	return isunique
}

//called once key has been used by createNode
//reduces value by one and deletes if necessary
// DecrimentKey - decriments key uses
func DecrimentKey(networkName string, keyvalue string) {

	var network models.Network

	network, err := GetParentNetwork(networkName)
	if err != nil {
		return
	}

	for i := len(network.AccessKeys) - 1; i >= 0; i-- {

		currentkey := network.AccessKeys[i]
		if currentkey.Value == keyvalue {
			network.AccessKeys[i].Uses--
			if network.AccessKeys[i].Uses < 1 {
				network.AccessKeys = append(network.AccessKeys[:i],
					network.AccessKeys[i+1:]...)
				break
			}
		}
	}

	if newNetworkData, err := json.Marshal(&network); err != nil {
		PrintUserLog(models.NODE_SERVER_NAME, "failed to decrement key", 2)
		return
	} else {
		database.Insert(network.NetID, string(newNetworkData), database.NETWORKS_TABLE_NAME)
	}
}

// DeleteKey - deletes a key
func DeleteKey(network models.Network, i int) {

	network.AccessKeys = append(network.AccessKeys[:i],
		network.AccessKeys[i+1:]...)

	if networkData, err := json.Marshal(&network); err != nil {
		return
	} else {
		database.Insert(network.NetID, string(networkData), database.NETWORKS_TABLE_NAME)
	}
}

// Inc - increments an IP
func Inc(ip net.IP) {
	for j := len(ip) - 1; j >= 0; j-- {
		ip[j]++
		if ip[j] > 0 {
			break
		}
	}
}
