package arkiv

import (
	"context"
	"crypto/ecdsa"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/ethereum/go-ethereum/rpc"
)

// Sender sends calldata to Address and returns once mined OK. Seam for tests.
type Sender interface {
	Send(ctx context.Context, data []byte) error
}

// Client writes batches (retrying) and queries entities.
type Client struct {
	rpc      *rpc.Client
	send     Sender
	Attempts int           // per Write; default 5
	Backoff  time.Duration // doubles per attempt; default 2s
}

// Dial connects. url already includes the access key path; key signs writes.
func Dial(ctx context.Context, url string, key *ecdsa.PrivateKey) (*Client, error) {
	r, err := rpc.DialContext(ctx, url)
	if err != nil {
		return nil, err
	}
	eth := ethclient.NewClient(r)
	id, err := eth.ChainID(ctx)
	if err != nil {
		r.Close()
		return nil, err
	}
	auth, err := bind.NewKeyedTransactorWithChainID(key, id)
	if err != nil {
		r.Close()
		return nil, err
	}
	return &Client{rpc: r, send: &ethSender{eth, auth}, Attempts: 5, Backoff: 2 * time.Second}, nil
}

// Close releases the rpc conn.
func (c *Client) Close() { c.rpc.Close() }

// Write sends es as ONE batch tx, retrying whole batch: nothing partial, so no gaps.
// ponytail: tx may land yet ack be lost -> retry dupes entities; readers dedupe on (market,ts).
func (c *Client) Write(ctx context.Context, es []Entity) error {
	if len(es) == 0 {
		return nil
	}
	data, err := EncodeBatch(es)
	if err != nil {
		return err
	}
	return c.exec(ctx, data)
}

// Exec sends raw ops (PatchOp/DeleteOp) in one all-or-nothing tx.
func (c *Client) Exec(ctx context.Context, ops ...op) error {
	data, err := EncodeOps(ops...)
	if err != nil {
		return err
	}
	return c.exec(ctx, data)
}

func (c *Client) exec(ctx context.Context, data []byte) error {
	var err error
	wait := c.Backoff
	for i := 1; ; i++ {
		err = c.send.Send(ctx, data)
		if err == nil {
			return nil
		}
		if i >= c.Attempts || ctx.Err() != nil {
			return fmt.Errorf("arkiv: write after %d tries: %w", i, err)
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(wait):
		}
		wait *= 2
	}
}

// Result is the arkiv_query reply.
type Result struct {
	Data        []Found `json:"data"`
	BlockNumber string  `json:"blockNumber"`
}

// Found is one matched entity (select: key + attributes).
type Found struct {
	Key        string `json:"key"`
	Attributes []struct {
		Name  string          `json:"name"`
		Type  string          `json:"type"`
		Value json.RawMessage `json:"value"`
	} `json:"attributes"`
}

// Query runs arkiv_query, e.g. `arkiv.schema = str('perpvault.FundingRate') AND market = str('BTC')`.
// Reply shape verified live on Rabdi; limit 1..100.
func (c *Client) Query(ctx context.Context, q string, limit int) (Result, error) {
	var res Result
	opts := map[string]any{"select": map[string]bool{"key": true, "attributes": true}, "limit": fmt.Sprintf("0x%x", limit)}
	err := c.rpc.CallContext(ctx, &res, "arkiv_query", q, opts)
	return res, err
}

type ethSender struct {
	eth  *ethclient.Client
	auth *bind.TransactOpts
}

func (s *ethSender) Send(ctx context.Context, data []byte) error {
	opts := *s.auth
	opts.Context = ctx
	// processor has no code (node-handled); bind's estimator rejects that, so estimate here
	gas, err := s.eth.EstimateGas(ctx, ethereum.CallMsg{From: opts.From, To: &Address, Data: data})
	if err != nil {
		return err
	}
	opts.GasLimit = gas * 12 / 10
	tx, err := bind.NewBoundContract(Address, abi.ABI{}, s.eth, s.eth, s.eth).RawTransact(&opts, data)
	if err != nil {
		return err
	}
	rc, err := bind.WaitMined(ctx, s.eth, tx)
	if err != nil {
		return err
	}
	if rc.Status != types.ReceiptStatusSuccessful {
		return errors.New("tx reverted " + tx.Hash().Hex())
	}
	return nil
}
