package jess

import (
	"crypto"
	"errors"
	"fmt"
	"io"
	"time"

	"github.com/mr-tron/base58"
	uuid "github.com/satori/go.uuid"

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

// Special signet types.
const (
	SignetSchemePassword = "pw"
	SignetSchemeKey      = "key"
)

// Signet describes a cryptographic key pair. Passwords and Keys may also be wrapped in a Signet for easier integration.
type Signet struct { //nolint:maligned // TODO
	Version uint8
	ID      string
	Scheme  string

	Key        []byte
	Public     bool      `json:",omitempty"` // key is the public part of a key pair
	Protection *Envelope `json:",omitempty"` // key is a serialized letter

	// Metadata about Signet
	Info *SignetInfo `json:",omitempty"`

	// Signature of Version, Scheme, Key, Public, Protected, Info
	Signature *Letter `json:",omitempty"`

	// cache
	tool             *tools.Tool
	loadedPublicKey  crypto.PublicKey
	loadedPrivateKey crypto.PrivateKey
}

// SignetInfo holds human readable meta information about a signet.
type SignetInfo struct {
	Name    string
	Owner   string
	Created time.Time
	Expires time.Time

	Details [][2]string
}

// NewSignetBase creates a new signet base without a key.
func NewSignetBase(tool *tools.Tool) *Signet {
	return &Signet{
		Version: 1,
		Scheme:  tool.Info.Name,
		tool:    tool,
	}
}

// GenerateSignet returns a new signet with a freshly generated key.
func GenerateSignet(toolID string, securityLevel int) (*Signet, error) {
	tool, err := tools.Get(toolID)
	if err != nil {
		return nil, err
	}

	// generate signet
	signet := NewSignetBase(tool)
	err = signet.GenerateKey()
	if err != nil {
		return nil, err
	}

	return signet, nil
}

// GenerateKey generates a new key. Will not operate if key is already present.
func (signet *Signet) GenerateKey() error {
	// check if there already is a key
	if len(signet.Key) > 0 ||
		signet.loadedPrivateKey != nil ||
		signet.loadedPublicKey != nil {
		return errors.New("cannot generate key: key already present")
	}

	// load tool
	err := signet.loadTool()
	if err != nil {
		return err
	}

	// check if tool support Signets
	switch signet.tool.Info.Purpose {
	case tools.PurposeKeyExchange,
		tools.PurposeKeyEncapsulation,
		tools.PurposeSigning:
		// uses signets!
	default:
		return fmt.Errorf("tool %s does not use signets", signet.tool.Info.Name)
	}

	// generate key
	return signet.tool.StaticLogic.GenerateKey(signet)
}

// GetStoredKey returns the stored key and whether it is public.
func (signet *Signet) GetStoredKey() (key []byte, public bool) {
	return signet.Key, signet.Public
}

// SetStoredKey sets a new stored key and whether it is public.
func (signet *Signet) SetStoredKey(key []byte, public bool) {
	signet.Key = key
	signet.Public = public
}

// PublicKey returns the public key.
func (signet *Signet) PublicKey() crypto.PublicKey {
	return signet.loadedPublicKey
}

// PrivateKey returns the private key or nil, if there is none.
func (signet *Signet) PrivateKey() crypto.PrivateKey {
	return signet.loadedPrivateKey
}

// SetLoadedKeys sets the loaded public and private keys.
func (signet *Signet) SetLoadedKeys(pubKey crypto.PublicKey, privKey crypto.PrivateKey) {
	signet.loadedPublicKey = pubKey
	signet.loadedPrivateKey = privKey
}

// AsRecipient returns a public version of the Signet.
func (signet *Signet) AsRecipient() (*Signet, error) {
	// Check special signet schemes.
	switch signet.Scheme {
	case SignetSchemeKey:
		return nil, errors.New("keys cannot be a recipient")
	case SignetSchemePassword:
		return nil, errors.New("passwords cannot be a recipient")
	}

	// load so we can split keys
	err := signet.LoadKey()
	if err != nil {
		return nil, err
	}

	return &Signet{
		Version:          signet.Version,
		ID:               signet.ID,
		Scheme:           signet.Scheme,
		Key:              nil,  // do not copy serialized key
		Public:           true, // mark explicitly as public
		Protection:       nil,  // remove protection
		Info:             signet.Info,
		Signature:        nil, // remove signature, as it would be invalid
		tool:             signet.tool,
		loadedPublicKey:  signet.loadedPublicKey,
		loadedPrivateKey: nil, // remove private key
	}, nil
}

// LoadKey loads the serialized key pair.
func (signet *Signet) LoadKey() error {
	// check if already loaded
	if signet.loadedPublicKey != nil {
		return nil
	}

	// check if protected
	if signet.Protection != nil {
		return tools.ErrProtected
	}

	// load tool
	err := signet.loadTool()
	if err != nil {
		return err
	}

	return signet.tool.StaticLogic.LoadKey(signet)
}

// Tool returns the tool of the signet.
func (signet *Signet) Tool() (*tools.Tool, error) {
	// load tool
	err := signet.loadTool()
	if err != nil {
		return nil, err
	}

	return signet.tool, nil
}

// loadTool gets and caches the tool for the signet.
func (signet *Signet) loadTool() error {
	if signet.tool != nil {
		return nil
	}

	tool, err := tools.Get(signet.Scheme)
	if err != nil {
		return err
	}

	signet.tool = tool
	return nil
}

// StoreKey serializes the loaded key pair.
func (signet *Signet) StoreKey() error {
	// check if already stored
	if len(signet.Key) != 0 {
		return nil
	}

	// load tool
	err := signet.loadTool()
	if err != nil {
		return err
	}

	return signet.tool.StaticLogic.StoreKey(signet)
}

// Verify verifies the signature of the signet.
func (signet *Signet) Verify() error {
	// TODO
	return errors.New("signet verification not yet implemented")
}

// Burn destroys all the key material and renders the Signet unusable. This is currently ineffective, see known issues in the project's README.
func (signet *Signet) Burn() error {
	// load tool
	err := signet.loadTool()
	if err != nil {
		return err
	}

	return signet.tool.StaticLogic.BurnKey(signet)
}

// AssignUUID generates a (new) UUID for the Signet.
func (signet *Signet) AssignUUID() error {
	// generate UUID v4
	u := uuid.UUID{}
	_, err := io.ReadFull(Random(), u[:])
	if err != nil {
		return err
	}

	u.SetVersion(uuid.V4)
	u.SetVariant(uuid.VariantRFC4122)
	signet.ID = u.String()
	return nil
}

// ToBytes serializes the Signet to a byte slice.
func (signet *Signet) ToBytes() ([]byte, error) {
	// Make sure the key is stored in the serializable format.
	if err := signet.StoreKey(); err != nil {
		return nil, fmt.Errorf("failed to serialize the key: %w", err)
	}

	// Serialize Signet.
	data, err := dsd.Dump(signet, dsd.CBOR)
	if err != nil {
		return nil, fmt.Errorf("failed to serialize the signet: %w", err)
	}

	return data, nil
}

// SignetFromBytes parses and loads a serialized signet.
func SignetFromBytes(data []byte) (*Signet, error) {
	signet := &Signet{}

	// Parse Signet from data.
	if _, err := dsd.Load(data, signet); err != nil {
		return nil, fmt.Errorf("failed to parse data format: %w", err)
	}

	// Load the key.
	if err := signet.LoadKey(); err != nil {
		return nil, fmt.Errorf("failed to parse key: %w", err)
	}

	return signet, nil
}

// ToBase58 serializes the signet and encodes it with base58.
func (signet *Signet) ToBase58() (string, error) {
	// Serialize Signet.
	data, err := signet.ToBytes()
	if err != nil {
		return "", err
	}

	// Encode and return.
	return base58.Encode(data), nil
}

// SignetFromBase58 parses and loads a base58 encoded serialized signet.
func SignetFromBase58(base58Encoded string) (*Signet, error) {
	// Decode string.
	data, err := base58.Decode(base58Encoded)
	if err != nil {
		return nil, fmt.Errorf("failed to decode base58: %w", err)
	}

	// Parse and return.
	return SignetFromBytes(data)
}