package main

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"math/big"
	"strings"
	"time"

	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
	"github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http"
	"github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron"
	"github.com/smartcontractkit/cre-sdk-go/cre"

	"rebalancer/contracts/evm/src/generated/delta_neutral_vault"
)

// lpEpoch is the funding window per LP report; lpSchedule must fire at this cadence.
const lpEpoch = time.Hour

// gatewayPageLimit is GET /funding's row cap (store.sqlFunding LIMIT).
const gatewayPageLimit = 100

// parseReceived sums funding the trader received (payment < 0) with since <= time(ms) < until.
func parseReceived(body []byte, since, until int64) (*big.Int, error) {
	var rows []struct {
		Payment string `json:"payment"` // > 0 paid by trader, USDC 6dp
		Time    int64  `json:"time"`    // unix ms
	}
	if err := json.Unmarshal(body, &rows); err != nil {
		return nil, fmt.Errorf("gateway funding: %w", err)
	}
	// newest first: a full page whose oldest row is still in the window may hide rows
	if len(rows) >= gatewayPageLimit && rows[len(rows)-1].Time >= since {
		return nil, fmt.Errorf("gateway funding: page full inside window")
	}
	sum := new(big.Rat)
	for _, r := range rows {
		p, ok := new(big.Rat).SetString(r.Payment)
		if !ok {
			return nil, fmt.Errorf("gateway funding: bad payment %q", r.Payment)
		}
		if r.Time >= since && r.Time < until && p.Sign() < 0 {
			sum.Sub(sum, p)
		}
	}
	return new(big.Int).Quo(sum.Num(), sum.Denom()), nil // floor (sum >= 0)
}

// lpSplit mirrors vault._lpEpoch: fromFunding = min(received*shareBps/1e4, Σvol*refFee*capMult/1e8);
// shares ∝ vol (even if Σvol = 0), floor remainder to market 0 so Σshares = accrued + fromFunding.
func lpSplit(vol [markets]*big.Int, accrued, received *big.Int, shareBps, refFeeBps, capMultBps uint64) ([markets]*big.Int, *big.Int) {
	total := new(big.Int)
	for _, v := range vol {
		total.Add(total, v)
	}
	bps := big.NewInt(10_000)
	limit := new(big.Int).Mul(total, new(big.Int).SetUint64(refFeeBps))
	limit.Mul(limit, new(big.Int).SetUint64(capMultBps)).Quo(limit, new(big.Int).Mul(bps, bps))
	from := new(big.Int).Mul(received, new(big.Int).SetUint64(shareBps))
	from.Quo(from, bps)
	if from.Cmp(limit) > 0 {
		from = limit
	}
	pot := new(big.Int).Add(accrued, from)
	var shares [markets]*big.Int
	rest := new(big.Int).Set(pot)
	for m := range markets {
		if total.Sign() == 0 {
			shares[m] = new(big.Int).Quo(pot, big.NewInt(markets))
		} else {
			shares[m] = new(big.Int).Quo(new(big.Int).Mul(pot, vol[m]), total)
		}
		rest.Sub(rest, shares[m])
	}
	shares[0].Add(shares[0], rest)
	return shares, from
}

var (
	tU8, _     = abi.NewType("uint8", "", nil)
	tU64, _    = abi.NewType("uint64", "", nil)
	tU256, _   = abi.NewType("uint256", "", nil)
	tU256x3, _ = abi.NewType("uint256[3]", "", nil)
	tB32, _    = abi.NewType("bytes32", "", nil)
	tBytes, _  = abi.NewType("bytes", "", nil)
	reportArgs = abi.Arguments{{Type: tU8}, {Type: tBytes}}
	lpArgs     = abi.Arguments{{Type: tU64}, {Type: tU256x3}, {Type: tU256}, {Type: tB32}}
)

// wrapReport is the vault's onReport body: abi.encode(uint8 kind, bytes payload).
func wrapReport(kind uint8, payload []byte) ([]byte, error) {
	return reportArgs.Pack(kind, payload)
}

// encodeLpReport is kind 1: abi.encode(uint64 epoch, uint256[3] shares, uint256 fromFunding, bytes32 ref).
func encodeLpReport(epoch uint64, shares [markets]*big.Int, from *big.Int, ref common.Hash) ([]byte, error) {
	p, err := lpArgs.Pack(epoch, shares, from, [32]byte(ref))
	if err != nil {
		return nil, err
	}
	return wrapReport(1, p)
}

func fundingURL(base, trader string) string {
	return fmt.Sprintf("%s/funding?trader=%s", strings.TrimRight(base, "/"), strings.ToLower(trader))
}

func onLpCron(cfg *Config, rt cre.Runtime, trig *cron.Payload) (string, error) {
	log := rt.Logger()
	until := trig.ScheduledExecutionTime.AsTime()
	since := until.Add(-lpEpoch)
	sel, _ := evm.ChainSelectorFromName(cfg.ChainName)
	vault, err := delta_neutral_vault.NewDeltaNeutralVault(&evm.Client{ChainSelector: sel}, common.HexToAddress(cfg.Vault), nil)
	if err != nil {
		return "", err
	}
	latest := big.NewInt(-2)
	var volP [markets]cre.Promise[*big.Int]
	for m := range markets {
		volP[m] = vault.EpochVolume(rt, delta_neutral_vault.EpochVolumeInput{Market: big.NewInt(int64(m))}, latest)
	}
	accP, lastP, cfgP := vault.LpAccrued(rt, latest), vault.LastLpEpoch(rt, latest), vault.Config(rt, latest)
	recvP := http.SendRequest(cfg, rt, &http.Client{}, func(cfg *Config, _ *slog.Logger, sr *http.SendRequester) (string, error) {
		res, err := sr.SendRequest(&http.Request{Method: "GET", Url: fundingURL(cfg.GatewayURL, cfg.Vault)}).Await()
		if err != nil {
			return "", err
		}
		if res.StatusCode != 200 {
			return "", fmt.Errorf("funding: http %d", res.StatusCode)
		}
		r, err := parseReceived(res.Body, since.UnixMilli(), until.UnixMilli())
		if err != nil {
			return "", err
		}
		return r.String(), nil
	}, cre.ConsensusIdenticalAggregation[string]())

	var vol [markets]*big.Int
	for m := range markets {
		if vol[m], err = volP[m].Await(); err != nil {
			return "", fmt.Errorf("epochVolume(%d): %w", m, err)
		}
	}
	accrued, err := accP.Await()
	if err != nil {
		return "", fmt.Errorf("lpAccrued: %w", err)
	}
	last, err := lastP.Await()
	if err != nil {
		return "", fmt.Errorf("lastLpEpoch: %w", err)
	}
	vc, err := cfgP.Await()
	if err != nil {
		return "", fmt.Errorf("config: %w", err)
	}
	rs, err := recvP.Await()
	if err != nil {
		return "", fmt.Errorf("funding: %w", err)
	}
	received, _ := new(big.Int).SetString(rs, 10)

	shares, from := lpSplit(vol, accrued, received, cfg.fundingShareBps(), vc.RefFeeBps.Uint64(), vc.CapMultBps.Uint64())
	pot := new(big.Int).Add(accrued, from)
	log.Info("lp epoch", "vol", vol, "accrued", accrued, "received", received, "fromFunding", from, "shares", shares)
	if pot.Sign() == 0 {
		log.Info("empty pot, no report")
		return "lp: empty pot", nil
	}
	epoch := last + 1
	ref := crypto.Keccak256Hash([]byte(fmt.Sprintf("lp:%d:%d:%d", epoch, since.Unix(), until.Unix())))
	payload, err := encodeLpReport(epoch, shares, from, ref)
	if err != nil {
		return "", err
	}
	tx, err := writeReport(rt, vault, cfg, payload)
	if err != nil {
		return "", err
	}
	log.Info("lp report written", "epoch", epoch, "ref", ref.Hex(), "tx", tx)
	return fmt.Sprintf("lp epoch %d pot %s shares %v tx %s", epoch, pot, shares, tx), nil
}
