// Copyright 2024 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package txpool

import (
	"bytes"
	"context"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/kv"
	"github.com/erigontech/erigon/db/kv/order"
	"github.com/erigontech/erigon/execution/chain"
	"github.com/erigontech/erigon/polygon/bor/borcfg"
)

var PoolChainConfigKey = []byte("chain_config")
var PoolLastSeenBlockKey = []byte("last_seen_block")
var PoolPendingBaseFeeKey = []byte("pending_base_fee")
var PoolPendingBlobFeeKey = []byte("pending_blob_fee")
var PoolStateVersion = []byte("state_version")

func getExecutionProgress(db kv.Getter) (uint64, error) {
	data, err := db.GetOne(kv.SyncStageProgress, []byte("Execution"))
	if err != nil {
		return 0, err
	}

	if len(data) == 0 {
		return 0, nil
	}

	if len(data) < 8 {
		return 0, fmt.Errorf("value must be at least 8 bytes, got %d", len(data))
	}

	return binary.BigEndian.Uint64(data[:8]), nil
}

func LastSeenBlock(tx kv.Getter) (uint64, error) {
	v, err := tx.GetOne(kv.PoolInfo, PoolLastSeenBlockKey)
	if err != nil {
		return 0, err
	}
	if len(v) == 0 {
		return 0, nil
	}
	return binary.BigEndian.Uint64(v), nil
}

func PutLastSeenBlock(tx kv.Putter, n uint64, buf []byte) error {
	buf = common.EnsureEnoughSize(buf, 8)
	binary.BigEndian.PutUint64(buf, n)
	return tx.Put(kv.PoolInfo, PoolLastSeenBlockKey, buf)
}

// LoadSenderLastActivity reads the persisted senderID→lastActivityBlock map from the pool DB.
// Returns an empty map (not an error) when the table is absent or empty — e.g. on the first
// run after this feature was introduced.
func LoadSenderLastActivity(tx kv.Tx) (map[uint64]uint64, error) {
	result := make(map[uint64]uint64)
	it, err := tx.Range(kv.SenderLastActivity, nil, nil, order.Asc, kv.Unlim)
	if err != nil {
		return result, err
	}
	for it.HasNext() {
		k, v, err := it.Next()
		if err != nil {
			return result, err
		}
		if len(k) == 8 && len(v) == 8 {
			result[binary.BigEndian.Uint64(k)] = binary.BigEndian.Uint64(v)
		}
	}
	return result, nil
}

// SaveSenderLastActivity persists the senderID→lastActivityBlock map to the pool DB,
// replacing any previously stored entries.
func SaveSenderLastActivity(tx kv.RwTx, m map[uint64]uint64) error {
	if err := tx.ClearTable(kv.SenderLastActivity); err != nil {
		return err
	}
	key := make([]byte, 8)
	val := make([]byte, 8)
	for senderID, blockNum := range m {
		binary.BigEndian.PutUint64(key, senderID)
		binary.BigEndian.PutUint64(val, blockNum)
		if err := tx.Put(kv.SenderLastActivity, key, val); err != nil {
			return err
		}
	}
	return nil
}

func ChainConfig(tx kv.Getter) (*chain.Config, error) {
	v, err := tx.GetOne(kv.PoolInfo, PoolChainConfigKey)
	if err != nil {
		return nil, err
	}
	if len(v) == 0 {
		return nil, nil
	}
	var config chain.Config
	if err := json.Unmarshal(v, &config); err != nil {
		return nil, fmt.Errorf("invalid chain config JSON in pool db: %w", err)
	}
	return &config, nil
}

func PutChainConfig(tx kv.Putter, cc *chain.Config, buf []byte) error {
	wr := bytes.NewBuffer(buf)
	if err := json.NewEncoder(wr).Encode(cc); err != nil {
		return fmt.Errorf("invalid chain config JSON in pool db: %w", err)
	}
	return tx.Put(kv.PoolInfo, PoolChainConfigKey, wr.Bytes())
}

func initBor(cc *chain.Config) *chain.Config {
	if cc.Bor == nil && cc.BorJSON != nil {
		borConfig := &borcfg.BorConfig{}
		err := json.Unmarshal(cc.BorJSON, borConfig)
		if err != nil {
			panic(fmt.Errorf("Could not parse 'bor' chainspec: %w", err))
		}
		cc.Bor = borConfig
	}
	return cc
}

func SaveChainConfigIfNeed(
	ctx context.Context,
	coreDB kv.RoDB,
	poolDB kv.RwDB,
	logger log.Logger,
) (cc *chain.Config, blockNum uint64, err error) {
	if err = poolDB.View(ctx, func(tx kv.Tx) error {
		cc, err = ChainConfig(tx)
		if err != nil {
			return err
		}
		blockNum, err = LastSeenBlock(tx)
		if err != nil {
			return err
		}
		return nil
	}); err != nil {
		return nil, 0, err
	}

	for {
		if err = coreDB.View(ctx, func(tx kv.Tx) error {
			cc, err = chain.GetConfig(tx, nil)
			if err != nil {
				return err
			}
			n, err := chain.CurrentBlockNumber(tx)
			if err != nil {
				return err
			}
			if n != nil {
				blockNum = *n
			}
			return nil
		}); err != nil {
			logger.Error("cant read chain config from core db", "err", err)
			time.Sleep(5 * time.Second)
			continue
		} else if cc == nil {
			logger.Error("cant read chain config from core db")
			time.Sleep(5 * time.Second)
			continue
		}
		break
	}

	if err = poolDB.Update(ctx, func(tx kv.RwTx) error {
		if err = PutChainConfig(tx, cc, nil); err != nil {
			return err
		}
		if err = PutLastSeenBlock(tx, blockNum, nil); err != nil {
			return err
		}
		return nil
	}); err != nil {
		return nil, 0, err
	}
	if cc.ChainID.Sign() == 0 {
		return nil, 0, errors.New("wrong chain config")
	}
	return initBor(cc), blockNum, nil
}
