package core

import (
	"context"
	"errors"
	"io"
	"time"

	"github.com/openbao/openbao/sdk/v2/logical"
)

var (
	// ErrBarrierSealed is returned if an operation is performed on
	// a sealed barrier. No operation is expected to succeed before unsealing
	//nolint:staticcheck // Warden is a proper name
	ErrBarrierSealed = errors.New("Warden is sealed")

	// ErrBarrierAlreadyInit is returned if the barrier is already
	// initialized. This prevents a re-initialization.
	//nolint:staticcheck // Warden is a proper name
	ErrBarrierAlreadyInit = errors.New("Warden is already initialized")

	// ErrBarrierNotInit is returned if a non-initialized barrier
	// is attempted to be unsealed.
	//nolint:staticcheck // Warden is a proper name
	ErrBarrierNotInit = errors.New("Warden is not initialized")

	// ErrBarrierInvalidKey is returned if the Unseal key is invalid
	ErrBarrierInvalidKey = errors.New("unseal failed, invalid key")

	// ErrPlaintextTooLarge is returned if a plaintext is offered for encryption
	// that is too large to encrypt in memory
	ErrPlaintextTooLarge = errors.New("plaintext value too large")
)

const (
	// keyringPath is the location of the keyring data. This is encrypted
	// by the root key.
	keyringPath = "core/keyring"

	// keyringUpgradePrefix is the path used to store keyring update entries.
	// When running in HA mode, the active instance will install the new key
	// and re-write the keyring. Standby instances need an upgrade path from
	// key N to N+1. They cannot just use the root key because in the event
	// of a rotation, that root key can no longer decrypt the keyring.
	// When key N+1 is installed, we create an entry at "prefix/N" which uses
	// encryption key N to provide the N+1 key. The standby instances scan
	// for this periodically and refresh their keyring. The upgrade keys
	// are deleted after a few minutes, but this provides enough time for the
	// standby instances to upgrade without causing any disruption.
	keyringUpgradePrefix = "core/upgrade/"

	// rootKeyPath is the location of the root key. This is encrypted
	// by the latest key in the keyring. This is only used by standby instances
	// to handle the case of a rotation. If the active instance does a rotation,
	// the standby instances can no longer reload the keyring since they
	// have the old root key. This key can be decrypted if you have the
	// keyring to discover the new root key. The new root key is then
	// used to reload the keyring itself.
	rootKeyPath = "core/root-key"

	// shamirKekPath is used with Shamir in v1.3+ to store a copy of the
	// unseal key behind the barrier. As with rootKeyPath this is primarily
	// used by standbys to handle rotations.
	shamirKekPath = "core/shamir-kek"
)

type SecurityBarrierCore interface {
	// Initialized checks if the barrier has been initialized
	// and has a root key set.
	Initialized(ctx context.Context) (bool, error)

	// Initialize works only if the barrier has not been initialized
	// and makes use of the given root key.  When sealKey is provided
	// it's because we're using a new-style Shamir seal, and rootKey
	// is to be stored using sealKey to encrypt it.
	Initialize(ctx context.Context, rootKey []byte, sealKey []byte, random io.Reader) error

	// GenerateKey is used to generate a new key
	GenerateKey(io.Reader) ([]byte, error)

	// KeyLength is used to sanity check a key
	KeyLength() (int, int)

	// Sealed checks if the barrier has been unlocked yet. The Barrier
	// is not expected to be able to perform any CRUD until it is unsealed.
	Sealed() (bool, error)

	// Unseal is used to provide the unseal key which permits the barrier
	// to be unsealed. If the key is not correct, the barrier remains sealed.
	Unseal(ctx context.Context, key []byte) error

	// VerifyRoot is used to check if the given key matches the root key
	VerifyRoot(key []byte) error

	// SetRootKey is used to directly set a new root key. This is used in
	// replicated scenarios due to the chicken and egg problem of reloading the
	// keyring from disk before we have the root key to decrypt it.
	SetRootKey(key []byte) error

	// ReloadKeyring is used to re-read the underlying keyring.
	// This is used for HA deployments to ensure the latest keyring
	// is present in the leader.
	ReloadKeyring(ctx context.Context) error

	// ReloadRootKey is used to re-read the underlying root key.
	// This is used for HA deployments to ensure the latest root key
	// is available for keyring reloading.
	ReloadRootKey(ctx context.Context) error

	// Seal is used to re-seal the barrier. This requires the barrier to
	// be unsealed again to perform any further operations.
	Seal() error

	// Rotate is used to create a new encryption key. All future writes
	// should use the new key, while old values should still be decryptable.
	Rotate(ctx context.Context, reader io.Reader) (uint32, error)

	// CreateUpgrade creates an upgrade path key to the given term from the previous term
	CreateUpgrade(ctx context.Context, term uint32) error

	// DestroyUpgrade destroys the upgrade path key to the given term
	DestroyUpgrade(ctx context.Context, term uint32) error

	// CheckUpgrade looks for an upgrade to the current term and installs it
	CheckUpgrade(ctx context.Context) (bool, uint32, error)

	// ActiveKeyInfo is used to inform details about the active key
	ActiveKeyInfo() (*KeyInfo, error)

	// RotationConfig returns the auto-rotation config for the barrier key
	RotationConfig() (KeyRotationConfig, error)

	// SetRotationConfig updates the auto-rotation config for the barrier key
	SetRotationConfig(ctx context.Context, config KeyRotationConfig) error

	// RotateRootKey is used to change the root key used to protect the keyring
	RotateRootKey(context.Context, []byte) error

	// Keyring returns the current encryption keyring
	Keyring() (*Keyring, error)

	// ConsumeEncryptionCount handles updating local encryption counts if the consumer succeeds.
	ConsumeEncryptionCount(consumer func(int64) error) error

	// AddRemoteEncryptions adds encryption counts from an external source
	AddRemoteEncryptions(encryptions int64)

	// Check whether an automatic rotation is due
	CheckBarrierAutoRotate(ctx context.Context) (string, error)

	// SecurityBarrier must provide the encryption APIs
	BarrierEncryptor

	// SetReadOnly allows marking storage as read-only; this is useful for
	// HA mode but could be more broadly useful.
	SetReadOnly(readOnly bool)
}

// SecurityBarrier is a critical component of Warden. It is used to wrap
// an untrusted physical backend and provide a single point of encryption,
// decryption and checksum verification. The goal is to ensure that any
// data written to the barrier is confidential and that integrity is preserved.
// As a real-world analogy, this is the steel and concrete wrapper around
// a Warden. The barrier should only be Unlockable given its key.
type SecurityBarrier interface {
	SecurityBarrierCore

	// SecurityBarrier must provide the storage APIs
	logical.Storage
}

// TransactionalSecurityBarrier is an implementation of SecureBarrier that
// additionally allows for the use of transactions. Note that these
// transactions are purely storage-level transactions and cannot be used to
// update the security barrier itself in a transaction-aware manner (i.e., to
// do key rotations or the such inside a transaction without affecting other
// consumers until committed).
type TransactionalSecurityBarrier interface {
	SecurityBarrierCore

	// SecurityBarrier must provide the transactional storage APIs
	logical.TransactionalStorage
}

// SecurityBarrierTransaction doesn't provide the same SecurityBarrierCore
// functions as they are not transaction-aware.
type SecurityBarrierTransaction interface {
	// SecurityBarrier must provide the transactional storage APIs
	logical.Transaction
}

// BarrierEncryptor is the in memory only interface that does not actually
// use the underlying barrier. It is used for lower level modules like the
// Write-Ahead-Log and Merkle index to allow them to use the barrier.
type BarrierEncryptor interface {
	Encrypt(ctx context.Context, key string, plaintext []byte) ([]byte, error)
	Decrypt(ctx context.Context, key string, ciphertext []byte) ([]byte, error)
}

// KeyInfo is used to convey information about the encryption key
type KeyInfo struct {
	Term        int
	InstallTime time.Time
	Encryptions int64
}
