package sources

import (
	"context"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/log"
	nodev1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/node/v1"
	"github.com/exchain/go-exchain/op-node/rollup"
	"github.com/exchain/go-exchain/op-node/rollup/derive"
	"github.com/exchain/go-exchain/op-service/client"
	"github.com/exchain/go-exchain/op-service/eth"
	"github.com/exchain/go-exchain/op-service/sources/caching"
	"github.com/pkg/errors"
	"math/big"
)

type L2ClientConfig struct {
	L2BlockRefsCacheSize int
	L1ConfigsCacheSize   int

	RollupCfg *rollup.Config
}

func L2ClientDefaultConfig(config *rollup.Config, trustRPC bool) *L2ClientConfig {
	// Cache 3/2 worth of sequencing window of payloads, block references, receipts and txs
	span := int(config.SeqWindowSize) * 3 / 2
	// Estimate number of L2 blocks in this span of L1 blocks
	// (there's always one L2 block per L1 block, L1 is thus the minimum, even if block time is very high)
	if config.BlockTime < 12 && config.BlockTime > 0 {
		span *= 12
		span /= int(config.BlockTime)
	}
	fullSpan := span
	if span > 1000 { // sanity cap. If a large sequencing window is configured, do not make the cache too large
		span = 1000
	}
	return &L2ClientConfig{
		// Not bounded by span, to cover find-sync-start range fully for speedy recovery after errors.
		L2BlockRefsCacheSize: fullSpan,
		L1ConfigsCacheSize:   span,
		RollupCfg:            config,
	}
}

// L2Client extends EthClient with functions to fetch and cache eth.L2BlockRef values.
type L2Client struct {
	nodev1.NodeClient

	rollupCfg *rollup.Config

	// cache L2BlockRef by hash
	// common.Hash -> eth.L2BlockRef
	l2BlockRefsCache *caching.LRUCache[common.Hash, eth.L2BlockRef]

	// cache SystemConfig by L2 hash
	// common.Hash -> eth.SystemConfig
	systemConfigsCache *caching.LRUCache[common.Hash, eth.SystemConfig]
}

func (s *L2Client) InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error) {
	//TODO implement me
	panic("implement me")
}

func (s *L2Client) GetProof(ctx context.Context, address common.Address, storage []common.Hash, blockTag string) (*eth.AccountResult, error) {
	//TODO implement me
	panic("implement me")
}

// NewL2Client constructs a new L2Client instance. The L2Client is a thin wrapper around the EthClient with added functions
// for fetching and caching eth.L2BlockRef values. This includes fetching an L2BlockRef by block number, label, or hash.
// See: [L2BlockRefByLabel], [L2BlockRefByNumber], [L2BlockRefByHash]
func NewL2Client(client client.RPC, log log.Logger, metrics caching.Metrics, config *L2ClientConfig) (*L2Client, error) {
	return &L2Client{
		rollupCfg:          config.RollupCfg,
		l2BlockRefsCache:   caching.NewLRUCache[common.Hash, eth.L2BlockRef](metrics, "blockrefs", config.L2BlockRefsCacheSize),
		systemConfigsCache: caching.NewLRUCache[common.Hash, eth.SystemConfig](metrics, "systemconfigs", config.L1ConfigsCacheSize),
	}, nil
}

func (s *L2Client) ChainID(ctx context.Context) (*big.Int, error) {
	return big.NewInt(38824), nil
}

func (s *L2Client) RollupConfig() *rollup.Config {
	return s.rollupCfg
}

// L2BlockRefByNumber returns the [eth.L2BlockRef] for the given block number.
func (s *L2Client) L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error) {
	// todo: vicotor implement this.
	res, err := s.GetBlockByNumber(ctx, &nodev1.GetBlockRequest{
		Number: num,
	})
	if err != nil {
		return eth.L2BlockRef{}, err
	}

	payload := eth.NewExecutePayload(res.Block)

	ref, err := derive.PayloadToBlockRef(s.rollupCfg, payload)
	if err != nil {
		return eth.L2BlockRef{}, err
	}
	s.l2BlockRefsCache.Add(ref.Hash, ref)
	return ref, nil
}

// L2BlockRefByHash returns the [eth.L2BlockRef] for the given block hash.
// The returned BlockRef may not be in the canonical chain.
func (s *L2Client) L2BlockRefByHash(ctx context.Context, hash common.Hash) (eth.L2BlockRef, error) {
	if ref, ok := s.l2BlockRefsCache.Get(hash); ok {
		return ref, nil
	}

	// todo: vicotor implement this.
	res, err := s.GetBlockByNumber(ctx, &nodev1.GetBlockRequest{
		Number: uint64(0),
	})
	if err != nil {
		return eth.L2BlockRef{}, err
	}

	payload := eth.NewExecutePayload(res.Block)

	ref, err := derive.PayloadToBlockRef(s.rollupCfg, payload)
	if err != nil {
		return eth.L2BlockRef{}, err
	}
	s.l2BlockRefsCache.Add(ref.Hash, ref)
	return ref, nil
}

// SystemConfigByL2Hash returns the [eth.SystemConfig] (matching the config updates up to and including the L1 origin) for the given L2 block hash.
// The returned [eth.SystemConfig] may not be in the canonical chain when the hash is not canonical.
func (s *L2Client) SystemConfigByL2Hash(ctx context.Context, hash common.Hash) (eth.SystemConfig, error) {
	if ref, ok := s.systemConfigsCache.Get(hash); ok {
		return ref, nil
	}

	// todo: vicotor implement this.
	res, err := s.GetBlockByNumber(ctx, &nodev1.GetBlockRequest{
		Number: uint64(0),
	})
	if err != nil {
		return eth.SystemConfig{}, err
	}

	payload := eth.NewExecutePayload(res.Block)
	cfg, err := derive.PayloadToSystemConfig(s.rollupCfg, payload)
	if err != nil {
		return eth.SystemConfig{}, err
	}
	s.systemConfigsCache.Add(hash, cfg)
	return cfg, nil
}

func (s *L2Client) OutputV0AtBlockNumber(ctx context.Context, blockNum uint64) (*eth.OutputV0, error) {
	return nil, errors.New("not implemented")
}
func (s *L2Client) OutputV0AtBlock(ctx context.Context, blockHash common.Hash) (*eth.OutputV0, error) {
	return nil, errors.New("not implemented")
}
