// Package garage contains helper functions and types which are useful for
// setting up garage configs, processes, and deployments.
package garagesrv

import (
	"crypto/ed25519"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strconv"
)

func nodeKeyPath(metaDirPath string) string {
	return filepath.Join(metaDirPath, "node_key")
}

func nodeKeyPubPath(metaDirPath string) string {
	return filepath.Join(metaDirPath, "node_key.pub")
}

func nodeRPCPortPath(metaDirPath string) string {
	return filepath.Join(metaDirPath, "isle", "rpc_port")
}

// loadAllocID returns the peer ID (ie the public key) of the node at the given
// meta directory.
func loadAllocID(metaDirPath string) (string, error) {
	nodeKeyPubPath := nodeKeyPubPath(metaDirPath)

	pubKey, err := os.ReadFile(nodeKeyPubPath)
	if err != nil {
		return "", fmt.Errorf("reading %q: %w", nodeKeyPubPath, err)
	}

	return hex.EncodeToString(pubKey), nil
}

// generatePeerKey generates and returns a public/private key pair for a garage
// instance.
func generatePeerKey() (pubKey, privKey []byte) {
	pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}

	return pubKey, privKey
}

// InitAlloc initializes the meta directory and keys for a particular
// allocation, if it hasn't been done so already. It returns the peer ID (ie the
// public key) and the rpc port in any case.
func InitAlloc(metaDirPath string, initRPCPort int) (string, int, error) {

	initDirFor := func(path string) error {
		dir := filepath.Dir(path)
		return os.MkdirAll(dir, 0750)
	}

	var err error

	exists := func(path string) bool {

		if err != nil {
			return false

		} else if _, err = os.Stat(path); errors.Is(err, fs.ErrNotExist) {
			err = nil
			return false

		} else if err != nil {
			err = fmt.Errorf("checking if %q exists: %w", path, err)
			return false
		}

		return true
	}

	nodeKeyPath := nodeKeyPath(metaDirPath)
	nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
	nodeRPCPortPath := nodeRPCPortPath(metaDirPath)

	nodeKeyPathExists := exists(nodeKeyPath)
	nodeKeyPubPathExists := exists(nodeKeyPubPath)
	nodeRPCPortPathExists := exists(nodeRPCPortPath)

	if err != nil {
		return "", 0, err

	} else if nodeKeyPubPathExists != nodeKeyPathExists {
		return "", 0, fmt.Errorf("%q or %q exist without the other existing", nodeKeyPath, nodeKeyPubPath)

	}

	var (
		pubKeyStr string
		rpcPort   int
	)

	if nodeKeyPathExists {

		if pubKeyStr, err = loadAllocID(metaDirPath); err != nil {
			return "", 0, fmt.Errorf("reading node public key file: %w", err)
		}

	} else {

		if err := initDirFor(nodeKeyPath); err != nil {
			return "", 0, fmt.Errorf("creating directory for %q: %w", nodeKeyPath, err)
		}

		pubKey, privKey := generatePeerKey()

		if err := os.WriteFile(nodeKeyPath, privKey, 0400); err != nil {
			return "", 0, fmt.Errorf("writing private key to %q: %w", nodeKeyPath, err)

		} else if err := os.WriteFile(nodeKeyPubPath, pubKey, 0440); err != nil {
			return "", 0, fmt.Errorf("writing public key to %q: %w", nodeKeyPubPath, err)
		}

		pubKeyStr = hex.EncodeToString(pubKey)
	}

	if nodeRPCPortPathExists {

		if rpcPortStr, err := os.ReadFile(nodeRPCPortPath); err != nil {
			return "", 0, fmt.Errorf("reading rpc port from %q: %w", nodeRPCPortPath, err)

		} else if rpcPort, err = strconv.Atoi(string(rpcPortStr)); err != nil {
			return "", 0, fmt.Errorf("parsing rpc port %q from %q: %w", rpcPortStr, nodeRPCPortPath, err)
		}

	} else {

		if err := initDirFor(nodeRPCPortPath); err != nil {
			return "", 0, fmt.Errorf("creating directory for %q: %w", nodeRPCPortPath, err)
		}

		rpcPortStr := strconv.Itoa(initRPCPort)

		if err := os.WriteFile(nodeRPCPortPath, []byte(rpcPortStr), 0440); err != nil {
			return "", 0, fmt.Errorf("writing rpc port %q to %q: %w", rpcPortStr, nodeRPCPortPath, err)
		}

		rpcPort = initRPCPort
	}

	return pubKeyStr, rpcPort, nil
}