package main

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"math/rand"
	"os"
	"syscall"
	"time"

	"github.com/gagliardetto/solana-go"
	"github.com/gagliardetto/solana-go/rpc"
	"github.com/gagliardetto/solana-go/rpc/ws"
	"github.com/gagliardetto/solana-go/text"
	envv1 "go.firedancer.io/radiance/proto/env/v1"
	"k8s.io/klog/v2"
)

var (
	flagPingerLog = flag.String("pinger_log", "", "JSON log file for pinger")
)

const (
	confirmPollInterval = time.Second * 5
	confirmRetries      = 10
)

type pingData struct {
	Slot uint64    `json:"slot"`
	Ts   time.Time `json:"ts"`
}

type logEntry struct {
	Slot      uint64           `json:"slot"`
	SendDelay time.Duration    `json:"send_delay"`
	Ts        time.Time        `json:"ts"`
	Signature solana.Signature `json:"signature"`
	Leader    solana.PublicKey `json:"leader"`

	Confirmed     bool          `json:"confirmed"`
	ConfirmedSlot uint64        `json:"confirmed_slot,omitempty"`
	SlotDelay     *int          `json:"slot_delay,omitempty"`
	TimeDelay     time.Duration `json:"time_delay,omitempty"`

	Timeout time.Duration `json:"timeout,omitempty"`
}

func buildTransaction(slot uint64, now time.Time, blockhash solana.Hash, feePayer solana.PublicKey) *solana.Transaction {
	payload := &pingData{Slot: slot, Ts: now}
	b, err := json.Marshal(payload)
	if err != nil {
		panic(err)
	}

	ins := solana.NewInstruction(solana.MemoProgramID, solana.AccountMetaSlice{}, b)

	tx, err := solana.NewTransaction(
		[]solana.Instruction{ins}, blockhash, solana.TransactionPayer(feePayer))
	if err != nil {
		panic(err)
	}

	return tx
}

func sendPing(ctx context.Context, m *ws.SlotsUpdatesResult, b solana.Hash, signer solana.PrivateKey, g *rpc.GetClusterNodesResult, nodes []*envv1.RPCNode, c map[string]*rpc.Client) {
	tx := buildTransaction(m.Slot, time.Now(), b, signer.PublicKey())
	sigs, err := tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
		if key != signer.PublicKey() {
			panic("no private key for unknown signer " + key.String())
		}
		return &signer
	})
	if err != nil {
		panic(err)
	}

	if klog.V(2).Enabled() {
		tx.EncodeTree(text.NewTreeEncoder(os.Stdout, "Ping memo"))
	}

	txb, err := tx.MarshalBinary()
	if err != nil {
		panic(err)
	}

	klog.Infof("Sending tx %s", sigs[0].String())
	klog.V(2).Infof("tx: %s", hex.EncodeToString(txb))

	sendUDP(*g.TPU, txb, 20)

	go waitForConfirmation(ctx, m, nodes, c, sigs, g)
}

func waitForConfirmation(ctx context.Context, m *ws.SlotsUpdatesResult, nodes []*envv1.RPCNode, c map[string]*rpc.Client, sigs []solana.Signature, g *rpc.GetClusterNodesResult) {
	for i := 0; i < confirmRetries; i++ {
		select {
		case <-ctx.Done():
			return
		case <-time.After(confirmPollInterval):
			// pick random node to query status
			node := nodes[rand.Intn(len(nodes))]
			st, err := c[node.Name].GetSignatureStatuses(ctx, false, sigs[0])
			if err != nil {
				klog.Errorf("Failed to fetch signature status: %v", err)
				continue
			}

			for _, s := range st.Value {
				if s == nil || s.ConfirmationStatus != rpc.ConfirmationStatusConfirmed {
					continue
				}
				delay := int(s.Slot) - int(m.Slot)
				klog.Infof("%s confirmed in slot %d (offset %d)", sigs[0], s.Slot, delay)
				if *flagPingerLog != "" {
					log(logEntry{
						Slot:          m.Slot,
						SendDelay:     time.Duration(0),
						Ts:            time.Now(),
						Signature:     sigs[0],
						Leader:        g.Pubkey,
						Confirmed:     true,
						ConfirmedSlot: s.Slot,
						SlotDelay:     &delay,
						TimeDelay:     0, // TODO - we need precise slot timings for this first
					})
					return
				}
			}
		}
	}
	klog.Infof("%s failed to confirm after %v",
		sigs[0].String(), confirmPollInterval*confirmRetries)

	if *flagPingerLog != "" {
		log(logEntry{
			Slot:      m.Slot,
			SendDelay: time.Duration(0),
			Ts:        time.Now(),
			Signature: sigs[0],
			Leader:    g.Pubkey,
			Confirmed: false,
			Timeout:   confirmPollInterval * confirmRetries,
		})
	}
}

func log(entry logEntry) {
	f, err := os.OpenFile(*flagPingerLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		klog.Errorf("failed to open %s: %v", *flagPingerLog, err)
		return
	}
	defer f.Close()

	if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
		panic(fmt.Sprintf("failed to lock %s: %v", *flagPingerLog, err))
	}

	if err := json.NewEncoder(f).Encode(entry); err != nil {
		klog.Errorf("failed to write to %s: %v", *flagPingerLog, err)
	}

	if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
		panic(fmt.Sprintf("failed to unlock %s: %v", *flagPingerLog, err))
	}
}
