package main

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"time"

	arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
	"github.com/ark-network/ark/common"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/urfave/cli/v2"
	"github.com/vulpemventures/go-elements/psetv2"
)

type receiver struct {
	To     string `json:"to"`
	Amount uint64 `json:"amount"`
}

var (
	receiversFlag = cli.StringFlag{
		Name:     "receivers",
		Usage:    "receivers of the send transaction, JSON encoded: '[{\"to\": \"<...>\", \"amount\": <...>}, ...]'",
		Value:    "",
		Required: true,
	}
)

var sendCommand = cli.Command{
	Name:   "send",
	Usage:  "Send VTXOs to a list of addresses",
	Action: sendAction,
	Flags:  []cli.Flag{&receiversFlag},
}

func sendAction(ctx *cli.Context) error {
	receivers := ctx.String("receivers")

	// parse json encoded receivers
	var receiversJSON []receiver
	if err := json.Unmarshal([]byte(receivers), &receiversJSON); err != nil {
		return fmt.Errorf("invalid receivers: %s", err)
	}

	if len(receiversJSON) <= 0 {
		return fmt.Errorf("no receivers specified")
	}

	aspPubKey, err := getServiceProviderPublicKey()
	if err != nil {
		return err
	}

	receiversOutput := make([]*arkv1.Output, 0)
	sumOfReceivers := uint64(0)

	for _, receiver := range receiversJSON {
		_, userKey, aspKey, err := common.DecodeAddress(receiver.To)
		if err != nil {
			return fmt.Errorf("invalid receiver address: %s", err)
		}

		if !bytes.Equal(aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed()) {
			return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver.To)
		}

		if receiver.Amount <= 0 {
			return fmt.Errorf("invalid amount: %d", receiver.Amount)
		}

		encodedKey := hex.EncodeToString(userKey.SerializeCompressed())
		receiversOutput = append(receiversOutput, &arkv1.Output{
			Pubkey: encodedKey,
			Amount: uint64(receiver.Amount),
		})
		sumOfReceivers += receiver.Amount
	}
	client, close, err := getArkClient(ctx)
	if err != nil {
		return err
	}
	defer close()

	vtxos, err := getVtxos(ctx, client)
	if err != nil {
		return err
	}

	selectedCoins, changeAmount, err := coinSelect(vtxos, sumOfReceivers)
	if err != nil {
		return err
	}

	if changeAmount > 0 {
		walletPrvKey, err := privateKeyFromPassword()
		if err != nil {
			return err
		}

		walletPubKey := walletPrvKey.PubKey()
		encodedPubKey := hex.EncodeToString(walletPubKey.SerializeCompressed())

		changeReceiver := &arkv1.Output{
			Pubkey: encodedPubKey,
			Amount: changeAmount,
		}
		receiversOutput = append(receiversOutput, changeReceiver)
	}

	inputs := make([]*arkv1.Input, 0, len(selectedCoins))

	for _, coin := range selectedCoins {
		inputs = append(inputs, &arkv1.Input{
			Txid: coin.txid,
			Vout: coin.vout,
		})
	}

	registerResponse, err := client.RegisterPayment(ctx.Context, &arkv1.RegisterPaymentRequest{
		Inputs: inputs,
	})
	if err != nil {
		return err
	}

	_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{
		Id:      registerResponse.GetId(),
		Outputs: receiversOutput,
	})
	if err != nil {
		return err
	}

	stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
	if err != nil {
		return err
	}

	pingStop := ping(ctx, client, &arkv1.PingRequest{
		PaymentId: registerResponse.GetId(),
	})

	for {
		event, err := stream.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}

		if event.GetRoundFailed() != nil {
			return fmt.Errorf("round failed: %s", event.GetRoundFailed().GetReason())
		}

		if event.GetRoundFinalization() != nil {
			pingStop()
			forfeits := event.GetRoundFinalization().GetForfeitTxs()
			signedForfeits := make([]string, 0)

			for _, forfeit := range forfeits {
				pset, err := psetv2.NewPsetFromBase64(forfeit)
				if err != nil {
					return err
				}

				// check if it contains one of the input to sign
				for _, input := range pset.Inputs {
					inputTxid := chainhash.Hash(input.PreviousTxid).String()

					for _, coin := range selectedCoins {
						if inputTxid == coin.txid {
							// TODO: sign the vtxo input
							signedForfeits = append(signedForfeits, forfeit)
						}
					}
				}
			}

			if len(signedForfeits) == 0 {
				continue
			}

			_, err := client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{
				SignedForfeitTxs: signedForfeits,
			})
			if err != nil {
				return err
			}

			continue
		}

		if event.GetRoundFinalized() != nil {
			return printJSON(map[string]interface{}{
				"paymentId": registerResponse.GetId(),
				"poolTxId":  event.GetRoundFinalized().GetPoolTxid(),
			})
		}
	}

	return nil
}

// send 1 ping message every 5 seconds to signal to the ark service that we are still alive
// returns a function that can be used to stop the pinging
func ping(ctx *cli.Context, client arkv1.ArkServiceClient, req *arkv1.PingRequest) func() {
	ticker := time.NewTicker(5 * time.Second)

	go func(t *time.Ticker) {
		for range t.C {
			_, err := client.Ping(ctx.Context, req)
			if err != nil {
				return
			}
		}
	}(ticker)

	return ticker.Stop
}
