package crypto

import (
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"io"

	"golang.org/x/crypto/curve25519"
	"golang.org/x/crypto/nacl/secretbox"
)

const encryptionRounds = 3

var (
	ErrNoKeysSet        = errors.New("encryption keys not set")
	ErrInvalidKeySize   = errors.New("invalid key size")
	ErrDecryptionFailed = errors.New("decryption failed")
)

var (
	x25519PublicKey  *[32]byte
	x25519PrivateKey *[32]byte
)

func init() {
	x25519PublicKey = new([32]byte)
	x25519PrivateKey = new([32]byte)
}

// SetKeys derives the X25519 key material used for local secretbox encryption
// from the daemon Ed25519 public/private key pair.
func SetKeys(edPubKey ed25519.PublicKey, edPriKey ed25519.PrivateKey) error {
	if len(edPubKey) != ed25519.PublicKeySize || len(edPriKey) != ed25519.PrivateKeySize {
		return ErrInvalidKeySize
	}

	seed := edPriKey.Seed()

	xPub, err := curve25519.X25519(seed, curve25519.Basepoint)
	if err != nil {
		return fmt.Errorf("derive x25519 public key: %w", err)
	}

	copy(x25519PublicKey[:], xPub)
	copy(x25519PrivateKey[:], seed)

	return nil
}

// KeysSet reports whether encryption key material has been initialized.
func KeysSet() bool {
	var zero [32]byte
	return *x25519PublicKey != zero || *x25519PrivateKey != zero
}

// ResetKeys clears process-global encryption key material. It is primarily used
// by tests that need isolated crypto state.
func ResetKeys() {
	x25519PublicKey = new([32]byte)
	x25519PrivateKey = new([32]byte)
}

// Encrypt seals plaintext for local storage and returns a base64-encoded value.
func Encrypt(plaintext string) (string, error) {
	if !KeysSet() {
		return "", ErrNoKeysSet
	}

	data := []byte(plaintext)
	for i := 0; i < encryptionRounds; i++ {
		encrypted, err := seal(data)
		if err != nil {
			return "", fmt.Errorf("round %d: %w", i+1, err)
		}
		data = encrypted
	}

	return base64.StdEncoding.EncodeToString(data), nil
}

// Decrypt opens a value produced by Encrypt.
func Decrypt(ciphertext string) (string, error) {
	if !KeysSet() {
		return "", ErrNoKeysSet
	}

	data, err := base64.StdEncoding.DecodeString(ciphertext)
	if err != nil {
		return "", fmt.Errorf("base64 decode: %w", err)
	}

	for i := 0; i < encryptionRounds; i++ {
		decrypted, err := open(data)
		if err != nil {
			return "", fmt.Errorf("round %d: %w", i+1, err)
		}
		data = decrypted
	}

	return string(data), nil
}

// IsEncrypted performs a best-effort encoding check for values written by
// Encrypt. It only verifies base64 shape; Decrypt performs authenticated
// secretbox validation.
func IsEncrypted(value string) bool {
	if value == "" {
		return false
	}
	_, err := base64.StdEncoding.DecodeString(value)
	return err == nil
}

func seal(plaintext []byte) ([]byte, error) {
	var ephemeralSeed [32]byte
	if _, err := io.ReadFull(rand.Reader, ephemeralSeed[:]); err != nil {
		return nil, err
	}

	ephemeralPub, err := curve25519.X25519(ephemeralSeed[:], curve25519.Basepoint)
	if err != nil {
		return nil, err
	}

	shared, err := curve25519.X25519(ephemeralSeed[:], x25519PublicKey[:])
	if err != nil {
		return nil, err
	}

	var sharedKey [32]byte
	copy(sharedKey[:], shared)

	var nonce [24]byte
	if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
		return nil, err
	}

	encrypted := secretbox.Seal(nil, plaintext, &nonce, &sharedKey)

	result := make([]byte, 32+24+len(encrypted))
	copy(result[:32], ephemeralPub)
	copy(result[32:56], nonce[:])
	copy(result[56:], encrypted)

	return result, nil
}

func open(ciphertext []byte) ([]byte, error) {
	if len(ciphertext) < 56 {
		return nil, ErrDecryptionFailed
	}

	var ephemeralPub [32]byte
	var nonce [24]byte
	copy(ephemeralPub[:], ciphertext[:32])
	copy(nonce[:], ciphertext[32:56])

	shared, err := curve25519.X25519(x25519PrivateKey[:], ephemeralPub[:])
	if err != nil {
		return nil, err
	}

	var sharedKey [32]byte
	copy(sharedKey[:], shared)

	decrypted, ok := secretbox.Open(nil, ciphertext[56:], &nonce, &sharedKey)
	if !ok {
		return nil, ErrDecryptionFailed
	}

	return decrypted, nil
}
