package lnwire

import (
	"bytes"
	"io"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/lightningnetwork/lnd/fn"
	"github.com/lightningnetwork/lnd/tlv"
)

const (
	CRDynHeight tlv.Type = 20
)

// DynHeight is a newtype wrapper to get the proper RecordProducer instance
// to smoothly integrate with the ChannelReestablish Message instance.
type DynHeight uint64

// Record implements the RecordProducer interface, allowing a full tlv.Record
// object to be constructed from a DynHeight.
func (d *DynHeight) Record() tlv.Record {
	return tlv.MakePrimitiveRecord(CRDynHeight, (*uint64)(d))
}

// ChannelReestablish is a message sent between peers that have an existing
// open channel upon connection reestablishment. This message allows both sides
// to report their local state, and their current knowledge of the state of the
// remote commitment chain. If a deviation is detected and can be recovered
// from, then the necessary messages will be retransmitted. If the level of
// desynchronization is irreconcilable, then the channel will be force closed.
type ChannelReestablish struct {
	// ChanID is the channel ID of the channel state we're attempting to
	// synchronize with the remote party.
	ChanID ChannelID

	// NextLocalCommitHeight is the next local commitment height of the
	// sending party. If the height of the sender's commitment chain from
	// the receiver's Pov is one less that this number, then the sender
	// should re-send the *exact* same proposed commitment.
	//
	// In other words, the receiver should re-send their last sent
	// commitment iff:
	//
	//  * NextLocalCommitHeight == remoteCommitChain.Height
	//
	// This covers the case of a lost commitment which was sent by the
	// sender of this message, but never received by the receiver of this
	// message.
	NextLocalCommitHeight uint64

	// RemoteCommitTailHeight is the height of the receiving party's
	// unrevoked commitment from the PoV of the sender of this message. If
	// the height of the receiver's commitment is *one more* than this
	// value, then their prior RevokeAndAck message should be
	// retransmitted.
	//
	// In other words, the receiver should re-send their last sent
	// RevokeAndAck message iff:
	//
	//  * localCommitChain.tail().Height == RemoteCommitTailHeight + 1
	//
	// This covers the case of a lost revocation, wherein the receiver of
	// the message sent a revocation for a prior state, but the sender of
	// the message never fully processed it.
	RemoteCommitTailHeight uint64

	// LastRemoteCommitSecret is the last commitment secret that the
	// receiving node has sent to the sending party. This will be the
	// secret of the last revoked commitment transaction. Including this
	// provides proof that the sending node at least knows of this state,
	// as they couldn't have produced it if it wasn't sent, as the value
	// can be authenticated by querying the shachain or the receiving
	// party.
	LastRemoteCommitSecret [32]byte

	// LocalUnrevokedCommitPoint is the commitment point used in the
	// current un-revoked commitment transaction of the sending party.
	LocalUnrevokedCommitPoint *btcec.PublicKey

	// LocalNonce is an optional field that stores a local musig2 nonce.
	// This will only be populated if the simple taproot channels type was
	// negotiated.
	//
	LocalNonce OptMusig2NonceTLV

	// DynHeight is an optional field that stores the dynamic commitment
	// negotiation height that is incremented upon successful completion of
	// a dynamic commitment negotiation
	DynHeight fn.Option[DynHeight]

	// ExtraData is the set of data that was appended to this message to
	// fill out the full maximum transport message size. These fields can
	// be used to specify optional data such as custom TLV fields.
	ExtraData ExtraOpaqueData
}

// A compile time check to ensure ChannelReestablish implements the
// lnwire.Message interface.
var _ Message = (*ChannelReestablish)(nil)

// Encode serializes the target ChannelReestablish into the passed io.Writer
// observing the protocol version specified.
//
// This is part of the lnwire.Message interface.
func (a *ChannelReestablish) Encode(w *bytes.Buffer, pver uint32) error {
	if err := WriteChannelID(w, a.ChanID); err != nil {
		return err
	}

	if err := WriteUint64(w, a.NextLocalCommitHeight); err != nil {
		return err
	}

	if err := WriteUint64(w, a.RemoteCommitTailHeight); err != nil {
		return err
	}

	// If the commit point wasn't sent, then we won't write out any of the
	// remaining fields as they're optional.
	if a.LocalUnrevokedCommitPoint == nil {
		// However, we'll still write out the extra data if it's
		// present.
		//
		// NOTE: This is here primarily for the quickcheck tests, in
		// practice, we'll always populate this field.
		return WriteBytes(w, a.ExtraData)
	}

	// Otherwise, we'll write out the remaining elements.
	if err := WriteBytes(w, a.LastRemoteCommitSecret[:]); err != nil {
		return err
	}

	if err := WritePublicKey(w, a.LocalUnrevokedCommitPoint); err != nil {
		return err
	}

	recordProducers := make([]tlv.RecordProducer, 0, 1)
	a.LocalNonce.WhenSome(func(localNonce Musig2NonceTLV) {
		recordProducers = append(recordProducers, &localNonce)
	})
	a.DynHeight.WhenSome(func(h DynHeight) {
		recordProducers = append(recordProducers, &h)
	})

	err := EncodeMessageExtraData(&a.ExtraData, recordProducers...)
	if err != nil {
		return err
	}

	return WriteBytes(w, a.ExtraData)
}

// Decode deserializes a serialized ChannelReestablish stored in the passed
// io.Reader observing the specified protocol version.
//
// This is part of the lnwire.Message interface.
func (a *ChannelReestablish) Decode(r io.Reader, pver uint32) error {
	err := ReadElements(r,
		&a.ChanID,
		&a.NextLocalCommitHeight,
		&a.RemoteCommitTailHeight,
	)
	if err != nil {
		return err
	}

	// This message has currently defined optional fields. As a result,
	// we'll only proceed if there's still bytes remaining within the
	// reader.
	//
	// We'll manually parse out the optional fields in order to be able to
	// still utilize the io.Reader interface.

	// We'll first attempt to read the optional commit secret, if we're at
	// the EOF, then this means the field wasn't included so we can exit
	// early.
	var buf [32]byte
	_, err = io.ReadFull(r, buf[:32])
	if err == io.EOF {
		// If there aren't any more bytes, then we'll emplace an empty
		// extra data to make our quickcheck tests happy.
		a.ExtraData = make([]byte, 0)
		return nil
	} else if err != nil {
		return err
	}

	// If the field is present, then we'll copy it over and proceed.
	copy(a.LastRemoteCommitSecret[:], buf[:])

	// We'll conclude by parsing out the commitment point. We don't check
	// the error in this case, as it has included the commit secret, then
	// they MUST also include the commit point.
	if err = ReadElement(r, &a.LocalUnrevokedCommitPoint); err != nil {
		return err
	}

	var tlvRecords ExtraOpaqueData
	if err := ReadElements(r, &tlvRecords); err != nil {
		return err
	}

	var (
		dynHeight  DynHeight
		localNonce = a.LocalNonce.Zero()
	)
	typeMap, err := tlvRecords.ExtractRecords(
		&localNonce, &dynHeight,
	)
	if err != nil {
		return err
	}

	if val, ok := typeMap[a.LocalNonce.TlvType()]; ok && val == nil {
		a.LocalNonce = tlv.SomeRecordT(localNonce)
	}
	if val, ok := typeMap[CRDynHeight]; ok && val == nil {
		a.DynHeight = fn.Some(dynHeight)
	}

	if len(tlvRecords) != 0 {
		a.ExtraData = tlvRecords
	}

	return nil
}

// MsgType returns the integer uniquely identifying this message type on the
// wire.
//
// This is part of the lnwire.Message interface.
func (a *ChannelReestablish) MsgType() MessageType {
	return MsgChannelReestablish
}
