package chanbackup

import (
	"bytes"
	"fmt"
	"io"

	"github.com/lightningnetwork/lnd/keychain"
	"github.com/lightningnetwork/lnd/lnencrypt"
	"github.com/lightningnetwork/lnd/lnwire"
)

// MultiBackupVersion denotes the version of the multi channel static channel
// backup. Based on this version, we know how to encode/decode packed/unpacked
// versions of multi backups.
type MultiBackupVersion byte

const (
	// DefaultMultiVersion is the default version of the multi channel
	// backup. The serialized format for this version is simply: version ||
	// numBackups || SCBs...
	DefaultMultiVersion = 0

	// NilMultiSizePacked is the size of a "nil" packed Multi (45 bytes).
	// This consists of the 24 byte chacha nonce, the 16 byte MAC, one byte
	// for the version, and 4 bytes to signal zero entries.
	NilMultiSizePacked = 24 + 16 + 1 + 4
)

// Multi is a form of static channel backup that is amenable to being
// serialized in a single file. Rather than a series of ciphertexts, a
// multi-chan backup is a single ciphertext of all static channel backups
// concatenated. This form factor gives users a single blob that they can use
// to safely copy/obtain at anytime to backup their channels.
type Multi struct {
	// Version is the version that should be observed when attempting to
	// pack the multi backup.
	Version MultiBackupVersion

	// StaticBackups is the set of single channel backups that this multi
	// backup is comprised of.
	StaticBackups []Single
}

// PackToWriter packs (encrypts+serializes) the target set of static channel
// backups into a single AEAD ciphertext into the passed io.Writer. This is the
// opposite of UnpackFromReader. The plaintext form of a multi-chan backup is
// the following: a 4 byte integer denoting the number of serialized static
// channel backups serialized, a series of serialized static channel backups
// concatenated. To pack this payload, we then apply our chacha20 AEAD to the
// entire payload, using the 24-byte nonce as associated data.
func (m Multi) PackToWriter(w io.Writer, keyRing keychain.KeyRing) error {
	// The only version that we know how to pack atm is version 0. Attempts
	// to pack any other version will result in an error.
	switch m.Version {
	case DefaultMultiVersion:
		break

	default:
		return fmt.Errorf("unable to pack unknown multi-version "+
			"of %v", m.Version)
	}

	var multiBackupBuffer bytes.Buffer

	// First, we'll write out the version of this multi channel baackup.
	err := lnwire.WriteElements(&multiBackupBuffer, byte(m.Version))
	if err != nil {
		return err
	}

	// Now that we've written out the version of this multi-pack format,
	// we'll now write the total number of backups to expect after this
	// point.
	numBackups := uint32(len(m.StaticBackups))
	err = lnwire.WriteElements(&multiBackupBuffer, numBackups)
	if err != nil {
		return err
	}

	// Next, we'll serialize the raw plaintext version of each of the
	// backup into the intermediate buffer.
	for _, chanBackup := range m.StaticBackups {
		err := chanBackup.Serialize(&multiBackupBuffer)
		if err != nil {
			return fmt.Errorf("unable to serialize backup "+
				"for %v: %v", chanBackup.FundingOutpoint, err)
		}
	}

	// With the plaintext multi backup assembled, we'll now encrypt it
	// directly to the passed writer.
	e, err := lnencrypt.KeyRingEncrypter(keyRing)
	if err != nil {
		return fmt.Errorf("unable to generate encrypt key %w", err)
	}

	return e.EncryptPayloadToWriter(multiBackupBuffer.Bytes(), w)
}

// UnpackFromReader attempts to unpack (decrypt+deserialize) a packed
// multi-chan backup form the passed io.Reader. If we're unable to decrypt the
// any portion of the multi-chan backup, an error will be returned.
func (m *Multi) UnpackFromReader(r io.Reader, keyRing keychain.KeyRing) error {
	// We'll attempt to read the entire packed backup, and also decrypt it
	// using the passed key ring which is expected to be able to derive the
	// encryption keys.
	e, err := lnencrypt.KeyRingEncrypter(keyRing)
	if err != nil {
		return fmt.Errorf("unable to generate encrypt key %w", err)
	}
	plaintextBackup, err := e.DecryptPayloadFromReader(r)
	if err != nil {
		return err
	}
	backupReader := bytes.NewReader(plaintextBackup)

	// Now that we've decrypted the payload successfully, we can parse out
	// each of the individual static channel backups.

	// First, we'll need to read the version of this multi-back up so we
	// can know how to unpack each of the individual SCB's.
	var multiVersion byte
	err = lnwire.ReadElements(backupReader, &multiVersion)
	if err != nil {
		return err
	}

	m.Version = MultiBackupVersion(multiVersion)
	switch m.Version {

	// The default version is simply a set of serialized SCB's with the
	// number of total SCB's prepended to the front of the byte slice.
	case DefaultMultiVersion:
		// First, we'll need to read out the total number of backups
		// that've been serialized into this multi-chan backup. Each
		// backup is the same size, so we can continue until we've
		// parsed out everything.
		var numBackups uint32
		err = lnwire.ReadElements(backupReader, &numBackups)
		if err != nil {
			return err
		}

		// We'll continue to parse out each backup until we've read all
		// that was indicated from the length prefix.
		for ; numBackups != 0; numBackups-- {
			// Attempt to parse out the net static channel backup,
			// if it's been malformed, then we'll return with an
			// error
			var chanBackup Single
			err := chanBackup.Deserialize(backupReader)
			if err != nil {
				return err
			}

			// Collect the next valid chan backup into the main
			// multi backup slice.
			m.StaticBackups = append(m.StaticBackups, chanBackup)
		}

	default:
		return fmt.Errorf("unable to unpack unknown multi-version "+
			"of %v", multiVersion)
	}

	return nil
}

// TODO(roasbeef): new key ring interface?
//  * just returns key given params?

// PackedMulti represents a raw fully packed (serialized+encrypted)
// multi-channel static channel backup.
type PackedMulti []byte

// Unpack attempts to unpack (decrypt+desrialize) the target packed
// multi-channel back up. If we're unable to fully unpack this back, then an
// error will be returned.
func (p *PackedMulti) Unpack(keyRing keychain.KeyRing) (*Multi, error) {
	var m Multi

	packedReader := bytes.NewReader(*p)
	if err := m.UnpackFromReader(packedReader, keyRing); err != nil {
		return nil, err
	}

	return &m, nil
}

// TODO(roasbsef): fuzz parsing
