package loopdb

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/wire"
)

// LoopOutContract contains the data that is serialized to persistent storage
// for pending swaps.
type LoopOutContract struct {
	// SwapContract contains basic information pertaining to this swap.
	// Each swap type has a base contract, then swap specific information
	// on top of it.
	SwapContract

	// DestAddr is the destination address of the loop out swap.
	DestAddr btcutil.Address

	// IsExternalAddr indicates whether the destination address does not
	// belong to the backing lnd node.
	IsExternalAddr bool

	// SwapInvoice is the invoice that is to be paid by the client to
	// initiate the loop out swap.
	SwapInvoice string

	// MaxSwapRoutingFee is the maximum off-chain fee in msat that may be
	// paid for the swap payment to the server.
	MaxSwapRoutingFee btcutil.Amount

	// SweepConfTarget specifies the targeted confirmation target for the
	// client sweep tx.
	SweepConfTarget int32

	// HtlcConfirmations is the number of confirmations we require the on
	// chain htlc to have before proceeding with the swap.
	HtlcConfirmations uint32

	// OutgoingChanSet is the set of short ids of channels that may be used.
	// If empty, any channel may be used.
	OutgoingChanSet ChannelSet

	// PrepayInvoice is the invoice that the client should pay to the
	// server that will be returned if the swap is complete.
	PrepayInvoice string

	// MaxPrepayRoutingFee is the maximum off-chain fee in msat that may be
	// paid for the prepayment to the server.
	MaxPrepayRoutingFee btcutil.Amount

	// SwapPublicationDeadline is a timestamp that the server commits to
	// have the on-chain swap published by. It is set by the client to
	// allow the server to delay the publication in exchange for possibly
	// lower fees.
	SwapPublicationDeadline time.Time

	// PaymentTimeout is the timeout for any individual off-chain payment
	// attempt.
	PaymentTimeout time.Duration

	// AssetSwapInfo contains information, should the loop out swpa be
	// paid via an asset channel.
	AssetSwapInfo *LoopOutAssetSwap
}

type LoopOutAssetSwap struct {
	// AssetId is the optional asset id that is used to pay the swap invoice.
	AssetId []byte

	// PrepayRfqId is the rfq id that is used to pay the prepay invoice.
	PrepayRfqId []byte

	// SwapRfqId is the rfq id that is used to pay the swap invoice.
	SwapRfqId []byte

	// PrepayPaidAmt is the asset amount that was paid for the prepay
	// invoice.
	PrepayPaidAmt uint64

	// SwapPaidAmt is the asset amount that was paid for the swap invoice.
	SwapPaidAmt uint64
}

// ChannelSet stores a set of channels.
type ChannelSet []uint64

// String returns the human-readable representation of a channel set.
func (c ChannelSet) String() string {
	channelStrings := make([]string, len(c))
	for i, chanID := range c {
		channelStrings[i] = strconv.FormatUint(chanID, 10)
	}
	return strings.Join(channelStrings, ",")
}

// NewChannelSet instantiates a new channel set and verifies that there are no
// duplicates present.
func NewChannelSet(set []uint64) (ChannelSet, error) {
	// Check channel set for duplicates.
	chanSet := make(map[uint64]struct{})
	for _, chanID := range set {
		if _, exists := chanSet[chanID]; exists {
			return nil, fmt.Errorf("duplicate chan in set: id=%v",
				chanID)
		}
		chanSet[chanID] = struct{}{}
	}

	return ChannelSet(set), nil
}

// LoopOut is a combination of the contract and the updates.
type LoopOut struct {
	Loop

	// Contract is the active contract for this swap. It describes the
	// precise details of the swap including the final fee, CLTV value,
	// etc.
	Contract *LoopOutContract
}

// LastUpdateTime returns the last update time of this swap.
func (s *LoopOut) LastUpdateTime() time.Time {
	lastUpdate := s.LastUpdate()
	if lastUpdate == nil {
		return s.Contract.InitiationTime
	}

	return lastUpdate.Time
}

func deserializeLoopOutContract(value []byte, chainParams *chaincfg.Params) (
	*LoopOutContract, error) {

	r := bytes.NewReader(value)

	contract := LoopOutContract{}
	var err error
	var unixNano int64
	if err := binary.Read(r, byteOrder, &unixNano); err != nil {
		return nil, err
	}
	contract.InitiationTime = time.Unix(0, unixNano)

	if err := binary.Read(r, byteOrder, &contract.Preimage); err != nil {
		return nil, err
	}

	err = binary.Read(r, byteOrder, &contract.AmountRequested)
	if err != nil {
		return nil, err
	}

	contract.PrepayInvoice, err = wire.ReadVarString(r, 0)
	if err != nil {
		return nil, err
	}

	n, err := r.Read(contract.HtlcKeys.SenderScriptKey[:])
	if err != nil {
		return nil, err
	}
	if n != keyLength {
		return nil, fmt.Errorf("sender key has invalid length")
	}

	n, err = r.Read(contract.HtlcKeys.ReceiverScriptKey[:])
	if err != nil {
		return nil, err
	}
	if n != keyLength {
		return nil, fmt.Errorf("receiver key has invalid length")
	}

	if err := binary.Read(r, byteOrder, &contract.CltvExpiry); err != nil {
		return nil, err
	}
	if err := binary.Read(r, byteOrder, &contract.MaxMinerFee); err != nil {
		return nil, err
	}

	if err := binary.Read(r, byteOrder, &contract.MaxSwapFee); err != nil {
		return nil, err
	}

	if err := binary.Read(r, byteOrder, &contract.MaxPrepayRoutingFee); err != nil {
		return nil, err
	}
	if err := binary.Read(r, byteOrder, &contract.InitiationHeight); err != nil {
		return nil, err
	}

	addr, err := wire.ReadVarString(r, 0)
	if err != nil {
		return nil, err
	}
	contract.DestAddr, err = btcutil.DecodeAddress(addr, chainParams)
	if err != nil {
		return nil, err
	}

	contract.SwapInvoice, err = wire.ReadVarString(r, 0)
	if err != nil {
		return nil, err
	}

	if err := binary.Read(r, byteOrder, &contract.SweepConfTarget); err != nil {
		return nil, err
	}

	if err := binary.Read(r, byteOrder, &contract.MaxSwapRoutingFee); err != nil {
		return nil, err
	}

	var unchargeChannel uint64
	if err := binary.Read(r, byteOrder, &unchargeChannel); err != nil {
		return nil, err
	}
	if unchargeChannel != 0 {
		contract.OutgoingChanSet = ChannelSet{unchargeChannel}
	}

	var deadlineNano int64
	err = binary.Read(r, byteOrder, &deadlineNano)
	if err != nil {
		return nil, err
	}
	contract.SwapPublicationDeadline = time.Unix(0, deadlineNano)

	return &contract, nil
}

func serializeLoopOutContract(swap *LoopOutContract) (
	[]byte, error) {

	var b bytes.Buffer

	if err := binary.Write(&b, byteOrder, swap.InitiationTime.UnixNano()); err != nil {
		return nil, err
	}

	if err := binary.Write(&b, byteOrder, swap.Preimage); err != nil {
		return nil, err
	}

	if err := binary.Write(&b, byteOrder, swap.AmountRequested); err != nil {
		return nil, err
	}

	if err := wire.WriteVarString(&b, 0, swap.PrepayInvoice); err != nil {
		return nil, err
	}

	n, err := b.Write(swap.HtlcKeys.SenderScriptKey[:])
	if err != nil {
		return nil, err
	}
	if n != keyLength {
		return nil, fmt.Errorf("sender key has invalid length")
	}

	n, err = b.Write(swap.HtlcKeys.ReceiverScriptKey[:])
	if err != nil {
		return nil, err
	}
	if n != keyLength {
		return nil, fmt.Errorf("receiver key has invalid length")
	}

	if err := binary.Write(&b, byteOrder, swap.CltvExpiry); err != nil {
		return nil, err
	}

	if err := binary.Write(&b, byteOrder, swap.MaxMinerFee); err != nil {
		return nil, err
	}

	if err := binary.Write(&b, byteOrder, swap.MaxSwapFee); err != nil {
		return nil, err
	}

	if err := binary.Write(&b, byteOrder, swap.MaxPrepayRoutingFee); err != nil {
		return nil, err
	}

	if err := binary.Write(&b, byteOrder, swap.InitiationHeight); err != nil {
		return nil, err
	}

	addr := swap.DestAddr.String()
	if err := wire.WriteVarString(&b, 0, addr); err != nil {
		return nil, err
	}

	if err := wire.WriteVarString(&b, 0, swap.SwapInvoice); err != nil {
		return nil, err
	}

	if err := binary.Write(&b, byteOrder, swap.SweepConfTarget); err != nil {
		return nil, err
	}

	if err := binary.Write(&b, byteOrder, swap.MaxSwapRoutingFee); err != nil {
		return nil, err
	}

	// Always write no outgoing channel. This field is replaced by an
	// outgoing channel set.
	unchargeChannel := uint64(0)
	if err := binary.Write(&b, byteOrder, unchargeChannel); err != nil {
		return nil, err
	}

	err = binary.Write(&b, byteOrder, swap.SwapPublicationDeadline.UnixNano())
	if err != nil {
		return nil, err
	}

	return b.Bytes(), nil
}
