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"
)

// Markets 0..2 = BTC, ETH, HYPE (vault market ids).
const markets = 3

type Config struct {
	Schedule      string `json:"schedule"`      // 6-field cron
	Window        string `json:"window"`        // Go duration, default 3h
	FundingSource string `json:"fundingSource"` // arkiv | gateway
	GatewayURL    string `json:"gatewayUrl"`
	ArkivURL      string `json:"arkivUrl"`
	ArkivOwner    string `json:"arkivOwner"` // recorder wallet; other owners' entities ignored
	ChainName     string `json:"chainName"`
	Vault         string `json:"vault"`
	GasLimit      uint64 `json:"gasLimit"`
	LpSchedule    string `json:"lpSchedule"`      // 6-field cron, hourly (lpEpoch); "" = LP step off
	FundingShare  uint64 `json:"fundingShareBps"` // share of vault funding received paid to LPs, default 1000
}

func (c *Config) fundingShareBps() uint64 {
	if c.FundingShare == 0 {
		return 1000
	}
	return c.FundingShare
}

func (c *Config) window() time.Duration {
	d, _ := time.ParseDuration(c.Window)
	if d <= 0 {
		return 3 * time.Hour
	}
	return d
}

func InitWorkflow(cfg *Config, _ *slog.Logger, _ cre.SecretsProvider) (cre.Workflow[*Config], error) {
	if cfg.Window != "" {
		if d, err := time.ParseDuration(cfg.Window); err != nil || d <= 0 {
			return nil, fmt.Errorf("bad window %q", cfg.Window)
		}
	}
	switch {
	case cfg.FundingSource == "gateway" && cfg.GatewayURL != "":
	case cfg.FundingSource == "arkiv" && cfg.ArkivURL != "" && common.IsHexAddress(cfg.ArkivOwner):
	default:
		return nil, fmt.Errorf("fundingSource must be gateway (gatewayUrl) or arkiv (arkivUrl + arkivOwner)")
	}
	if !common.IsHexAddress(cfg.Vault) {
		return nil, fmt.Errorf("bad vault %q", cfg.Vault)
	}
	if _, err := evm.ChainSelectorFromName(cfg.ChainName); err != nil {
		return nil, err
	}
	if cfg.FundingShare > 10_000 {
		return nil, fmt.Errorf("fundingShareBps > 1e4")
	}
	w := cre.Workflow[*Config]{cre.Handler(cron.Trigger(&cron.Config{Schedule: cfg.Schedule}), onCron)}
	if cfg.LpSchedule != "" {
		if cfg.GatewayURL == "" {
			return nil, fmt.Errorf("lpSchedule needs gatewayUrl (vault funding payments)")
		}
		w = append(w, cre.Handler(cron.Trigger(&cron.Config{Schedule: cfg.LpSchedule}), onLpCron))
	}
	return w, nil
}

// fetcher returns market m's window as canonical JSON, so nodes agree via identical consensus.
func fetcher(m int, since int64) func(*Config, *slog.Logger, *http.SendRequester) (string, error) {
	return func(cfg *Config, _ *slog.Logger, sr *http.SendRequester) (string, error) {
		req := &http.Request{Method: "GET", Url: gatewayURL(cfg.GatewayURL, m, cfg.window())}
		if cfg.FundingSource == "arkiv" {
			req = &http.Request{Method: "POST", Url: cfg.ArkivURL, Body: arkivBody(m, since, cfg.ArkivOwner),
				MultiHeaders: map[string]*http.HeaderValues{"Content-Type": {Values: []string{"application/json"}}}}
		}
		res, err := sr.SendRequest(req).Await()
		if err != nil {
			return "", err
		}
		if res.StatusCode != 200 {
			return "", fmt.Errorf("market %d: http %d", m, res.StatusCode)
		}
		parse := parseGateway
		if cfg.FundingSource == "arkiv" {
			parse = parseArkiv
		}
		ps, err := parse(res.Body)
		if err != nil {
			return "", err
		}
		b, err := json.Marshal(ps)
		return string(b), err
	}
}

func onCron(cfg *Config, rt cre.Runtime, trig *cron.Payload) (string, error) {
	log := rt.Logger()
	since := trig.ScheduledExecutionTime.AsTime().Add(-cfg.window()).Unix()
	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
	}

	var modeP [markets]cre.Promise[uint8]
	var winP [markets]cre.Promise[string]
	for m := range markets {
		modeP[m] = vault.Mode(rt, delta_neutral_vault.ModeInput{Market: big.NewInt(int64(m))}, big.NewInt(-2)) // latest
		winP[m] = http.SendRequest(cfg, rt, &http.Client{}, fetcher(m, since), cre.ConsensusIdenticalAggregation[string]())
	}
	var modes, next [markets]uint8
	var used []string
	for m := range markets {
		if modes[m], err = modeP[m].Await(); err != nil {
			return "", fmt.Errorf("mode(%d): %w", m, err)
		}
		s, err := winP[m].Await()
		if err != nil {
			return "", fmt.Errorf("funding(%d): %w", m, err)
		}
		var ps []Print
		if err := json.Unmarshal([]byte(s), &ps); err != nil {
			return "", err
		}
		next[m] = Decide(modes[m], ps)
		log.Info("decision", "market", m, "mode", modes[m], "next", next[m], "prints", len(ps))
		if next[m] != modes[m] {
			for _, p := range ps {
				used = append(used, p.Key)
			}
		}
	}
	if next == modes {
		log.Info("no change, no report")
		return fmt.Sprintf("no change %v", modes), nil
	}

	ref := crypto.Keccak256Hash([]byte(strings.Join(used, ",")))
	payload, err := encodeReport(next, ref)
	if err != nil {
		return "", err
	}
	tx, err := writeReport(rt, vault, cfg, payload)
	if err != nil {
		return "", err
	}
	log.Info("report written", "modes", next, "reasonRef", ref.Hex(), "tx", tx)
	return fmt.Sprintf("modes %v -> %v tx %s", modes, next, tx), nil
}

// writeReport signs payload and sends it to vault.onReport; returns tx hash.
func writeReport(rt cre.Runtime, vault *delta_neutral_vault.DeltaNeutralVault, cfg *Config, payload []byte) (string, error) {
	rep, err := rt.GenerateReport(&cre.ReportRequest{EncodedPayload: payload, EncoderName: "evm", SigningAlgo: "ecdsa", HashingAlgo: "keccak256"}).Await()
	if err != nil {
		return "", err
	}
	res, err := vault.WriteReport(rt, rep, &evm.GasConfig{GasLimit: cfg.GasLimit}).Await()
	if err != nil {
		return "", err
	}
	tx := common.BytesToHash(res.TxHash).Hex()
	if res.TxStatus != evm.TxStatus_TX_STATUS_SUCCESS ||
		res.GetReceiverContractExecutionStatus() != evm.ReceiverContractExecutionStatus_RECEIVER_CONTRACT_EXECUTION_STATUS_SUCCESS {
		return "", fmt.Errorf("write %s: status %v receiver %v %s", tx, res.TxStatus, res.GetReceiverContractExecutionStatus(), res.GetErrorMessage())
	}
	return tx, nil
}

// encodeReport is the kind-0 mode report: payload abi.encode(uint8[3] modes, bytes32 reasonRef).
func encodeReport(modes [markets]uint8, ref common.Hash) ([]byte, error) {
	u8x3, _ := abi.NewType("uint8[3]", "", nil)
	p, err := abi.Arguments{{Type: u8x3}, {Type: tB32}}.Pack(modes, [32]byte(ref))
	if err != nil {
		return nil, err
	}
	return wrapReport(0, p)
}
