package steward

import (
	"bytes"
	"crypto/sha256"
	"fmt"
	"log"
	"path/filepath"
	"sort"
	"sync"

	"github.com/fxamacker/cbor/v2"
	bolt "go.etcd.io/bbolt"
)

// centralAuth holds the logic related to handling public keys and auth maps.
type centralAuth struct {
	// acl and authorization level related data and methods.
	accessLists *accessLists
	// public key distribution related data and methods.
	pki           *pki
	configuration *Configuration
	errorKernel   *errorKernel
}

// newCentralAuth will return a new and prepared *centralAuth
func newCentralAuth(configuration *Configuration, errorKernel *errorKernel) *centralAuth {
	c := centralAuth{
		configuration: configuration,
		errorKernel:   errorKernel,
	}
	c.pki = newPKI(configuration, errorKernel)
	c.accessLists = newAccessLists(c.pki, errorKernel, configuration)

	c.generateACLsForAllNodes()

	return &c
}

// nodesAcked is the structure that holds all the keys that we have
// acknowledged, and that are allowed to be distributed within the
// system. It also contains a hash of all those keys.
type nodesAcked struct {
	mu          sync.Mutex
	keysAndHash *keysAndHash
}

// newNodesAcked will return a prepared *nodesAcked structure.
func newNodesAcked() *nodesAcked {
	n := nodesAcked{
		keysAndHash: newKeysAndHash(),
	}

	return &n
}

// pki holds the data and method relevant to key handling and distribution.
type pki struct {
	nodesAcked             *nodesAcked
	nodeNotAckedPublicKeys *nodeNotAckedPublicKeys
	configuration          *Configuration
	db                     *bolt.DB
	bucketNamePublicKeys   string
	errorKernel            *errorKernel
}

// newKeys will return a prepared *keys with input values set.
func newPKI(configuration *Configuration, errorKernel *errorKernel) *pki {
	p := pki{
		// schema:           make(map[Node]map[argsString]signatureBase32),
		nodesAcked:             newNodesAcked(),
		nodeNotAckedPublicKeys: newNodeNotAckedPublicKeys(configuration),
		configuration:          configuration,
		bucketNamePublicKeys:   "publicKeys",
		errorKernel:            errorKernel,
	}

	databaseFilepath := filepath.Join(configuration.DatabaseFolder, "auth.db")

	// Open the database file for persistent storage of public keys.
	db, err := bolt.Open(databaseFilepath, 0660, nil)
	if err != nil {
		er := fmt.Errorf("newPKI: error: failed to open db: %v", err)
		errorKernel.logDebug(er, configuration)
		return &p
	}

	p.db = db

	// Get public keys from db storage.
	keys, err := p.dbDumpPublicKey()
	if err != nil {
		er := fmt.Errorf("newPKI: dbPublicKeyDump failed, probably empty db: %v", err)
		errorKernel.logDebug(er, configuration)
	}

	// Only assign from storage to in memory map if the storage contained any values.
	if keys != nil {
		p.nodesAcked.keysAndHash.Keys = keys
		for k, v := range keys {
			er := fmt.Errorf("newPKI: public keys db contains: %v, %v", k, []byte(v))
			errorKernel.logDebug(er, configuration)
		}
	}

	// Get the current hash from db if one exists.
	hash, err := p.dbViewHash()
	if err != nil {
		log.Printf("debug: dbViewHash failed: %v\n", err)
	}

	if hash != nil {
		var h [32]byte
		copy(h[:], hash)
		p.nodesAcked.keysAndHash.Hash = h
	}

	return &p
}

// addPublicKey to the db if the node do not exist, or if it is a new value.
func (c *centralAuth) addPublicKey(proc process, msg Message) {

	// Check if a key for the current node already exists in the map.
	c.pki.nodesAcked.mu.Lock()
	existingKey, ok := c.pki.nodesAcked.keysAndHash.Keys[msg.FromNode]
	c.pki.nodesAcked.mu.Unlock()

	if ok && bytes.Equal(existingKey, msg.Data) {
		er := fmt.Errorf("info: public key value for REGISTERED node %v is the same, doing nothing", msg.FromNode)
		proc.errorKernel.logDebug(er, proc.configuration)
		return
	}

	c.pki.nodeNotAckedPublicKeys.mu.Lock()
	existingNotAckedKey, ok := c.pki.nodeNotAckedPublicKeys.KeyMap[msg.FromNode]
	// We only want to send one notification to the error kernel about new key detection,
	// so we check if the values are the same as the one we already got before we continue
	// with registering and logging for the the new key.
	if ok && bytes.Equal(existingNotAckedKey, msg.Data) {
		c.pki.nodeNotAckedPublicKeys.mu.Unlock()
		return
	}

	c.pki.nodeNotAckedPublicKeys.KeyMap[msg.FromNode] = msg.Data
	c.pki.nodeNotAckedPublicKeys.mu.Unlock()

	er := fmt.Errorf("info: detected new public key for node: %v. This key will need to be authorized by operator to be allowed into the system", msg.FromNode)
	c.pki.errorKernel.infoSend(proc, msg, er)
	c.pki.errorKernel.logDebug(er, c.pki.configuration)
}

// deletePublicKeys to the db if the node do not exist, or if it is a new value.
func (c *centralAuth) deletePublicKeys(proc process, msg Message, nodes []string) {

	// Check if a key for the current node already exists in the map.
	func() {
		c.pki.nodesAcked.mu.Lock()
		defer c.pki.nodesAcked.mu.Unlock()

		for _, n := range nodes {
			delete(c.pki.nodesAcked.keysAndHash.Keys, Node(n))
		}
	}()

	err := c.pki.dbDeletePublicKeys(c.pki.bucketNamePublicKeys, nodes)
	if err != nil {
		proc.errorKernel.errSend(proc, msg, err, logWarning)
	}

	er := fmt.Errorf("info: detected new public key for node: %v. This key will need to be authorized by operator to be allowed into the system", msg.FromNode)
	proc.errorKernel.logDebug(er, proc.configuration)
	c.pki.errorKernel.infoSend(proc, msg, er)
}

// // dbGetPublicKey will look up and return a specific value if it exists for a key in a bucket in a DB.
// func (c *centralAuth) dbGetPublicKey(node string) ([]byte, error) {
// 	var value []byte
// 	// View is a help function to get values out of the database.
// 	err := c.db.View(func(tx *bolt.Tx) error {
// 		//Open a bucket to get key's and values from.
// 		bu := tx.Bucket([]byte(c.bucketNamePublicKeys))
// 		if bu == nil {
// 			log.Printf("info: no db bucket exist: %v\n", c.bucketNamePublicKeys)
// 			return nil
// 		}
//
// 		v := bu.Get([]byte(node))
// 		if len(v) == 0 {
// 			log.Printf("info: view: key not found\n")
// 			return nil
// 		}
//
// 		value = v
//
// 		return nil
// 	})
//
// 	return value, err
// }

// dbUpdatePublicKey will update the public key for a node in the db.
func (p *pki) dbUpdatePublicKey(node string, value []byte) error {
	err := p.db.Update(func(tx *bolt.Tx) error {
		//Create a bucket
		bu, err := tx.CreateBucketIfNotExists([]byte(p.bucketNamePublicKeys))
		if err != nil {
			return fmt.Errorf("error: CreateBuckerIfNotExists failed: %v", err)
		}

		//Put a value into the bucket.
		if err := bu.Put([]byte(node), []byte(value)); err != nil {
			return err
		}

		//If all was ok, we should return a nil for a commit to happen. Any error
		// returned will do a rollback.
		return nil
	})
	return err
}

// dbDeletePublicKeys will delete the specified key from the specified
// bucket if it exists.
func (p *pki) dbDeletePublicKeys(bucket string, nodes []string) error {
	err := p.db.Update(func(tx *bolt.Tx) error {
		bu := tx.Bucket([]byte(bucket))

		for _, n := range nodes {
			err := bu.Delete([]byte(n))
			if err != nil {
				er := fmt.Errorf("error: delete key in bucket %v failed: %v", bucket, err)
				p.errorKernel.logDebug(er, p.configuration)
				return er
			}
		}

		return nil
	})

	return err
}

// dbUpdateHash will update the public key for a node in the db.
func (p *pki) dbUpdateHash(hash []byte) error {
	err := p.db.Update(func(tx *bolt.Tx) error {
		//Create a bucket
		bu, err := tx.CreateBucketIfNotExists([]byte("hash"))
		if err != nil {
			return fmt.Errorf("error: CreateBuckerIfNotExists failed: %v", err)
		}

		//Put a value into the bucket.
		if err := bu.Put([]byte("hash"), []byte(hash)); err != nil {
			return err
		}

		//If all was ok, we should return a nil for a commit to happen. Any error
		// returned will do a rollback.
		return nil
	})
	return err
}

func (c *centralAuth) updateHash(proc process, message Message) {
	c.pki.nodesAcked.mu.Lock()
	defer c.pki.nodesAcked.mu.Unlock()

	type NodesAndKeys struct {
		Node Node
		Key  []byte
	}

	// Create a slice of all the map keys, and its value.
	sortedNodesAndKeys := []NodesAndKeys{}

	// Range the map, and add each k/v to the sorted slice, to be sorted later.
	for k, v := range c.pki.nodesAcked.keysAndHash.Keys {
		nk := NodesAndKeys{
			Node: k,
			Key:  v,
		}

		sortedNodesAndKeys = append(sortedNodesAndKeys, nk)
	}

	// sort the slice based on the node name.
	// Sort all the commands.
	sort.SliceStable(sortedNodesAndKeys, func(i, j int) bool {
		return sortedNodesAndKeys[i].Node < sortedNodesAndKeys[j].Node
	})

	// Then create a hash based on the sorted slice.

	b, err := cbor.Marshal(sortedNodesAndKeys)
	if err != nil {
		er := fmt.Errorf("error: methodREQKeysAllow, failed to marshal slice, and will not update hash for public keys:  %v", err)
		c.pki.errorKernel.errSend(proc, message, er, logError)

		return
	}

	// Store the key in the key value map.
	hash := sha256.Sum256(b)
	c.pki.nodesAcked.keysAndHash.Hash = hash

	// Store the key to the db for persistence.
	c.pki.dbUpdateHash(hash[:])
	if err != nil {
		er := fmt.Errorf("error: methodREQKeysAllow, failed to store the hash into the db:  %v", err)
		c.pki.errorKernel.errSend(proc, message, er, logError)

		return
	}

}

// dbViewHash will look up and return a specific value if it exists for a key in a bucket in a DB.
func (p *pki) dbViewHash() ([]byte, error) {
	var value []byte
	// View is a help function to get values out of the database.
	err := p.db.View(func(tx *bolt.Tx) error {
		//Open a bucket to get key's and values from.
		bu := tx.Bucket([]byte("hash"))
		if bu == nil {
			er := fmt.Errorf("info: no db hash bucket exist")
			p.errorKernel.logWarn(er, p.configuration)
			return nil
		}

		v := bu.Get([]byte("hash"))
		if len(v) == 0 {
			er := fmt.Errorf("info: view: hash key not found")
			p.errorKernel.logWarn(er, p.configuration)
			return nil
		}

		value = v

		return nil
	})

	return value, err

}

// // deleteKeyFromBucket will delete the specified key from the specified
// // bucket if it exists.
// func (c *centralAuth) dbDeletePublicKey(key string) error {
// 	err := c.db.Update(func(tx *bolt.Tx) error {
// 		bu := tx.Bucket([]byte(c.bucketNamePublicKeys))
//
// 		err := bu.Delete([]byte(key))
// 		if err != nil {
// 			log.Printf("error: delete key in bucket %v failed: %v\n", c.bucketNamePublicKeys, err)
// 		}
//
// 		return nil
// 	})
//
// 	return err
// }

// dumpBucket will dump out all they keys and values in the
// specified bucket, and return a sorted []samDBValue
func (p *pki) dbDumpPublicKey() (map[Node][]byte, error) {
	m := make(map[Node][]byte)

	err := p.db.View(func(tx *bolt.Tx) error {
		bu := tx.Bucket([]byte(p.bucketNamePublicKeys))
		if bu == nil {
			return fmt.Errorf("error: dumpBucket: tx.bucket returned nil")
		}

		// For each element found in the DB, print it.
		bu.ForEach(func(k, v []byte) error {
			m[Node(k)] = v
			return nil
		})

		return nil
	})

	if err != nil {
		return nil, err
	}

	return m, nil
}

// --- HERE

// nodeNotAckedPublicKeys holds all the gathered but not acknowledged public
// keys of nodes in the system.
type nodeNotAckedPublicKeys struct {
	mu     sync.RWMutex
	KeyMap map[Node][]byte
}

// newNodeNotAckedPublicKeys will return a prepared type of nodePublicKeys.
func newNodeNotAckedPublicKeys(configuration *Configuration) *nodeNotAckedPublicKeys {
	n := nodeNotAckedPublicKeys{
		KeyMap: make(map[Node][]byte),
	}

	return &n
}
