package gostdlib

import (
	"crypto"
	"crypto/ed25519"
	"errors"

	"github.com/safing/jess/tools"
	"github.com/safing/structures/container"
)

func init() {
	tools.Register(&tools.Tool{
		Info: &tools.ToolInfo{
			Name:          "Ed25519",
			Purpose:       tools.PurposeSigning,
			Options:       []uint8{tools.OptionNeedsManagedHasher},
			SecurityLevel: 128,
			Comment:       "",
			Author:        "Daniel J. Bernstein, 2011",
		},
		Factory: func() tools.ToolLogic { return &Ed25519{} },
	})
}

// Ed25519 implements the cryptographic interface for Ed25519 signatures.
type Ed25519 struct {
	tools.ToolLogicBase
}

// Sign implements the ToolLogic interface.
func (ed *Ed25519) Sign(data, associatedData []byte, signet tools.SignetInt) ([]byte, error) {
	edPrivKey, ok := signet.PrivateKey().(ed25519.PrivateKey)
	if !ok {
		return nil, tools.ErrInvalidKey
	}
	if len(edPrivKey) != ed25519.PrivateKeySize {
		return nil, tools.ErrInvalidKey
	}

	hashsum, err := ed.ManagedHashSum()
	if err != nil {
		return nil, err
	}

	return ed25519.Sign(edPrivKey, hashsum), nil
}

// Verify implements the ToolLogic interface.
func (ed *Ed25519) Verify(data, associatedData, signature []byte, signet tools.SignetInt) error {
	edPubKey, ok := signet.PublicKey().(ed25519.PublicKey)
	if !ok {
		return tools.ErrInvalidKey
	}
	if len(edPubKey) != ed25519.PublicKeySize {
		return tools.ErrInvalidKey
	}

	hashsum, err := ed.ManagedHashSum()
	if err != nil {
		return err
	}

	if !ed25519.Verify(edPubKey, hashsum, signature) {
		return errors.New("signature invalid")
	}
	return nil
}

// LoadKey implements the ToolLogic interface.
func (ed *Ed25519) LoadKey(signet tools.SignetInt) error {
	var pubKey crypto.PublicKey
	var privKey ed25519.PrivateKey

	key, public := signet.GetStoredKey()
	c := container.New(key)

	// check serialization version
	version, err := c.GetNextN8()
	if err != nil || version != 1 {
		return tools.ErrInvalidKey
	}

	// load public key
	data := c.CompileData()

	// assign and check data
	if public {
		if len(data) != ed25519.PublicKeySize {
			return tools.ErrInvalidKey
		}
		pubKey = ed25519.PublicKey(data)
	} else {
		if len(data) != ed25519.PrivateKeySize {
			return tools.ErrInvalidKey
		}
		privKey = ed25519.PrivateKey(data)
		pubKey = privKey.Public()
	}

	signet.SetLoadedKeys(pubKey, privKey)
	return nil
}

// StoreKey implements the ToolLogic interface.
func (ed *Ed25519) StoreKey(signet tools.SignetInt) error {
	pubKey := signet.PublicKey()
	privKey := signet.PrivateKey()
	public := privKey == nil

	// create storage with serialization version
	c := container.New()
	c.AppendNumber(1)

	// store keys
	if public {
		pubKeyData, ok := pubKey.(ed25519.PublicKey)
		if !ok {
			return tools.ErrInvalidKey
		}
		c.Append(pubKeyData)
	} else {
		privKeyData, ok := privKey.(ed25519.PrivateKey)
		if !ok {
			return tools.ErrInvalidKey
		}
		c.Append(privKeyData)
	}

	signet.SetStoredKey(c.CompileData(), public)
	return nil
}

// GenerateKey implements the ToolLogic interface.
func (ed *Ed25519) GenerateKey(signet tools.SignetInt) error {
	// define variable types for API security
	var pubKey ed25519.PublicKey
	var privKey ed25519.PrivateKey
	var err error

	// generate keys
	pubKey, privKey, err = ed25519.GenerateKey(ed.Helper().Random())
	if err != nil {
		return err
	}

	signet.SetLoadedKeys(pubKey, privKey)
	return nil
}

// BurnKey implements the ToolLogic interface. This is currently ineffective, see known issues in the project's README.
func (ed *Ed25519) BurnKey(signet tools.SignetInt) error {
	pubKey := signet.PublicKey()
	privKey := signet.PrivateKey()

	// burn public key
	if pubKey != nil {
		data, ok := pubKey.([]byte)
		if ok {
			ed.Helper().Burn(data)
		}
	}

	// burn private key
	if privKey != nil {
		data, ok := privKey.([]byte)
		if ok {
			ed.Helper().Burn(data)
		}
	}

	return nil
}