package lnwire

import (
	"bytes"
	"fmt"
	"io"

	"github.com/lightningnetwork/lnd/fn"
	"github.com/lightningnetwork/lnd/tlv"
)

// ExtraOpaqueData is the set of data that was appended to this message, some
// of which we may not actually know how to iterate or parse. By holding onto
// this data, we ensure that we're able to properly validate the set of
// signatures that cover these new fields, and ensure we're able to make
// upgrades to the network in a forwards compatible manner.
type ExtraOpaqueData []byte

// NewExtraOpaqueData creates a new ExtraOpaqueData instance from a tlv.TypeMap.
func NewExtraOpaqueData(tlvMap tlv.TypeMap) (ExtraOpaqueData, error) {
	// If the tlv map is empty, we'll want to mirror the behavior of
	// decoding an empty extra opaque data field (see Decode method).
	if len(tlvMap) == 0 {
		return make([]byte, 0), nil
	}

	// Convert the TLV map into a slice of records.
	records := TlvMapToRecords(tlvMap)

	// Encode the records into the extra data byte slice.
	return EncodeRecords(records)
}

// Encode attempts to encode the raw extra bytes into the passed io.Writer.
func (e *ExtraOpaqueData) Encode(w *bytes.Buffer) error {
	eBytes := []byte((*e)[:])
	if err := WriteBytes(w, eBytes); err != nil {
		return err
	}

	return nil
}

// Decode attempts to unpack the raw bytes encoded in the passed-in io.Reader as
// a set of extra opaque data.
func (e *ExtraOpaqueData) Decode(r io.Reader) error {
	// First, we'll attempt to read a set of bytes contained within the
	// passed io.Reader (if any exist).
	rawBytes, err := io.ReadAll(r)
	if err != nil {
		return err
	}

	// If we _do_ have some bytes, then we'll swap out our backing pointer.
	// This ensures that any struct that embeds this type will properly
	// store the bytes once this method exits.
	if len(rawBytes) > 0 {
		*e = rawBytes
	} else {
		*e = make([]byte, 0)
	}

	return nil
}

// PackRecords attempts to encode the set of tlv records into the target
// ExtraOpaqueData instance. The records will be encoded as a raw TLV stream
// and stored within the backing slice pointer.
func (e *ExtraOpaqueData) PackRecords(
	recordProducers ...tlv.RecordProducer) error {

	// Assemble all the records passed in series, then encode them.
	records := ProduceRecordsSorted(recordProducers...)
	encoded, err := EncodeRecords(records)
	if err != nil {
		return err
	}

	*e = encoded

	return nil
}

// ExtractRecords attempts to decode any types in the internal raw bytes as if
// it were a tlv stream. The set of raw parsed types is returned, and any
// passed records (if found in the stream) will be parsed into the proper
// tlv.Record.
func (e *ExtraOpaqueData) ExtractRecords(
	recordProducers ...tlv.RecordProducer) (tlv.TypeMap, error) {

	// First, assemble all the records passed in series.
	records := ProduceRecordsSorted(recordProducers...)
	extraBytesReader := bytes.NewReader(*e)

	// Since ExtraOpaqueData is provided by a potentially malicious peer,
	// pass it into the P2P decoding variant.
	return DecodeRecordsP2P(extraBytesReader, records...)
}

// RecordProducers parses ExtraOpaqueData into a slice of TLV record producers
// by interpreting it as a TLV map.
func (e *ExtraOpaqueData) RecordProducers() ([]tlv.RecordProducer, error) {
	var recordProducers []tlv.RecordProducer

	// If the instance is nil or empty, return an empty slice.
	if e == nil || len(*e) == 0 {
		return recordProducers, nil
	}

	// Parse the extra opaque data as a TLV map.
	tlvMap, err := e.ExtractRecords()
	if err != nil {
		return nil, err
	}

	// Convert the TLV map into a slice of record producers.
	records := TlvMapToRecords(tlvMap)

	return RecordsAsProducers(records), nil
}

// EncodeMessageExtraData encodes the given recordProducers into the given
// extraData.
func EncodeMessageExtraData(extraData *ExtraOpaqueData,
	recordProducers ...tlv.RecordProducer) error {

	// Treat extraData as a mutable reference.
	if extraData == nil {
		return fmt.Errorf("extra data cannot be nil")
	}

	// Pack in the series of TLV records into this message. The order we
	// pass them in doesn't matter, as the method will ensure that things
	// are all properly sorted.
	return extraData.PackRecords(recordProducers...)
}

// ParseAndExtractCustomRecords parses the given extra data into the passed-in
// records, then returns any remaining records split into custom records and
// extra data.
func ParseAndExtractCustomRecords(allExtraData ExtraOpaqueData,
	knownRecords ...tlv.RecordProducer) (CustomRecords,
	fn.Set[tlv.Type], ExtraOpaqueData, error) {

	extraDataTlvMap, err := allExtraData.ExtractRecords(knownRecords...)
	if err != nil {
		return nil, nil, nil, err
	}

	// Remove the known and now extracted records from the leftover extra
	// data map.
	parsedKnownRecords := make(fn.Set[tlv.Type], len(knownRecords))
	for _, producer := range knownRecords {
		r := producer.Record()

		// Only remove the records if it was parsed (remainder is nil).
		// We'll just store the type so we can tell the caller which
		// records were actually parsed fully.
		val, ok := extraDataTlvMap[r.Type()]
		if ok && val == nil {
			parsedKnownRecords.Add(r.Type())
			delete(extraDataTlvMap, r.Type())
		}
	}

	// Any records from the extra data TLV map which are in the custom
	// records TLV type range will be included in the custom records field
	// and removed from the extra data field.
	customRecordsTlvMap := make(tlv.TypeMap, len(extraDataTlvMap))
	for k, v := range extraDataTlvMap {
		// Skip records that are not in the custom records TLV type
		// range.
		if k < MinCustomRecordsTlvType {
			continue
		}

		// Include the record in the custom records map.
		customRecordsTlvMap[k] = v

		// Now that the record is included in the custom records map,
		// we can remove it from the extra data TLV map.
		delete(extraDataTlvMap, k)
	}

	// Set the custom records field to the custom records specific TLV
	// record map.
	customRecords, err := NewCustomRecords(customRecordsTlvMap)
	if err != nil {
		return nil, nil, nil, err
	}

	// Encode the remaining records back into the extra data field. These
	// records are not in the custom records TLV type range and do not
	// have associated fields in the struct that produced the records.
	extraData, err := NewExtraOpaqueData(extraDataTlvMap)
	if err != nil {
		return nil, nil, nil, err
	}

	// Help with unit testing where we might have the empty value (nil) for
	// the extra data instead of the default that's returned by the
	// constructor (empty slice).
	if len(extraData) == 0 {
		extraData = nil
	}

	return customRecords, parsedKnownRecords, extraData, nil
}

// MergeAndEncode merges the known records with the extra data and custom
// records, then encodes the merged records into raw bytes.
func MergeAndEncode(knownRecords []tlv.RecordProducer,
	extraData ExtraOpaqueData, customRecords CustomRecords) ([]byte,
	error) {

	// Construct a slice of all the records that we should include in the
	// message extra data field. We will start by including any records from
	// the extra data field.
	mergedRecords, err := extraData.RecordProducers()
	if err != nil {
		return nil, err
	}

	// Merge the known and extra data records.
	mergedRecords = append(mergedRecords, knownRecords...)

	// Include custom records in the extra data wire field if they are
	// present. Ensure that the custom records are validated before encoding
	// them.
	if err := customRecords.Validate(); err != nil {
		return nil, fmt.Errorf("custom records validation error: %w",
			err)
	}

	// Extend the message extra data records slice with TLV records from the
	// custom records field.
	mergedRecords = append(
		mergedRecords, customRecords.RecordProducers()...,
	)

	// Now we can sort the records and make sure there are no records with
	// the same type that would collide when encoding.
	sortedRecords := ProduceRecordsSorted(mergedRecords...)
	if err := AssertUniqueTypes(sortedRecords); err != nil {
		return nil, err
	}

	return EncodeRecords(sortedRecords)
}
