package evm

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"math"
	"math/big"
	"strconv"
	"strings"
	"sync"
)

// Hex conversion utilities

// Pool for reusing byte slices during hex decoding to reduce allocations
var hexDecodePool = sync.Pool{
	New: func() interface{} {
		// Pre-allocate for common sizes (32 bytes for hashes, 20 bytes for addresses)
		// Using 32 as capacity since that's the most common case (block hashes, tx hashes, etc.)
		return make([]byte, 0, 32)
	},
}

// HexToBytes converts a hex string to bytes, removing the 0x prefix if present
func HexToBytes(s string) ([]byte, error) {
	s = RemoveHexPrefix(s)
	// Normalize odd-length hex strings by padding with leading zero
	// This handles cases where RPC nodes return values like "0x1" instead of "0x01"
	if len(s)%2 != 0 {
		s = "0" + s
	}

	// For very common sizes, use the pool to reduce allocations
	expectedLen := len(s) / 2
	if expectedLen <= 64 { // Common for hashes (32), addresses (20), etc.
		poolBuf := hexDecodePool.Get().([]byte)
		defer hexDecodePool.Put(poolBuf[:0]) // Reset length but keep capacity

		// Ensure buffer has enough capacity
		if cap(poolBuf) < expectedLen {
			poolBuf = make([]byte, 0, expectedLen*2)
		}

		// Decode into pool buffer
		decoded := poolBuf[:expectedLen]
		n, err := hex.Decode(decoded, []byte(s))
		if err != nil {
			return nil, err
		}

		// Copy to final result to avoid holding pool buffer reference
		result := make([]byte, n)
		copy(result, decoded[:n])
		return result, nil
	}

	// Fall back to standard decoding for larger values
	return hex.DecodeString(s)
}

// MustHexToBytes converts a hex string to bytes, panicking on error
// Useful for hardcoded values in tests or known-good constants
func MustHexToBytes(s string) []byte {
	b, err := HexToBytes(s)
	if err != nil {
		panic(fmt.Sprintf("invalid hex string: %s", s))
	}
	return b
}

// HexToAddress converts a hex string to an Address
func HexToAddress(s string) (Address, error) {
	b, err := HexToBytes(s)
	if err != nil {
		return nil, err
	}
	return NewAddress(b), nil
}

// MustHexToAddress converts a hex string to an Address, panicking on error
func MustHexToAddress(s string) Address {
	addr, err := HexToAddress(s)
	if err != nil {
		panic(fmt.Sprintf("invalid address hex: %s", s))
	}
	return addr
}

func HexToUint32(hex string) (uint32, error) {
	if len(hex) < 2 || hex[:2] != "0x" {
		return 0, fmt.Errorf("invalid hex string: %s", hex)
	}
	var result uint32
	_, err := fmt.Sscanf(hex, "0x%x", &result)
	return result, err
}

func HexToUint64(hex string) (uint64, error) {
	if len(hex) < 2 || hex[:2] != "0x" {
		return 0, fmt.Errorf("invalid hex string: %s", hex)
	}
	var result uint64
	_, err := fmt.Sscanf(hex, "0x%x", &result)
	return result, err
}

func MustHexToUint64(hex string) uint64 {
	result, err := HexToUint64(hex)
	if err != nil {
		panic(fmt.Sprintf("invalid hex string: %s", hex))
	}
	return result
}

func MustHexToUint32(hex string) uint32 {
	result, err := HexToUint32(hex)
	if err != nil {
		panic(fmt.Sprintf("invalid hex string: %s", hex))
	}
	return result
}

// HexToInt64 parses a 0x-prefixed hex QUANTITY into int64.
// Returns error on invalid input or if the value exceeds MaxInt64.
func HexToInt64(hex string) (int64, error) {
	u, err := HexToUint64(hex)
	if err != nil {
		return 0, err
	}
	// guard overflow
	if u > uint64(math.MaxInt64) {
		return 0, fmt.Errorf("hex quantity overflows int64: %s", hex)
	}
	return int64(u), nil
}

func NumberishToUint64(s string) (uint64, error) {
	if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
		return HexToUint64(s)
	}
	return strconv.ParseUint(s, 10, 64)
}

func NumberishToUint32(s string) (uint32, error) {
	if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
		return HexToUint32(s)
	}
	u, err := strconv.ParseUint(s, 10, 32)
	if err != nil {
		return 0, err
	}
	return uint32(u), nil
}

// HexToHash converts a hex string to a Hash
func HexToHash(s string) (Hash, error) {
	b, err := HexToBytes(s)
	if err != nil {
		return nil, err
	}
	return NewHash(b), nil
}

// MustHexToHash converts a hex string to a Hash, panicking on error
func MustHexToHash(s string) Hash {
	h, err := HexToHash(s)
	if err != nil {
		panic(fmt.Sprintf("invalid hash hex: %s", s))
	}
	return h
}

// HexToTopic converts a hex string to a Topic
func HexToTopic(s string) (Topic, error) {
	b, err := HexToBytes(s)
	if err != nil {
		return nil, err
	}
	return NewTopic(b), nil
}

// MustHexToTopic converts a hex string to a Topic, panicking on error
func MustHexToTopic(s string) Topic {
	t, err := HexToTopic(s)
	if err != nil {
		panic(fmt.Sprintf("invalid topic hex: %s", s))
	}
	return t
}

// BytesToHex converts bytes to a hex string with 0x prefix
func BytesToHex(b []byte) string {
	return "0x" + hex.EncodeToString(b)
}

// BytesToHexFixed left-pads the input with zero bytes to the desired size (in bytes)
// and returns a 0x-prefixed hex string. If b is longer than size, it is returned as-is.
// Use for DATA fields that have canonical fixed widths (e.g. r/s = 32 bytes, addresses = 20 bytes).
func BytesToHexFixed(b []byte, size int) string {
	if b == nil {
		return "0x"
	}
	if len(b) >= size {
		return BytesToHex(b)
	}
	out := make([]byte, size)
	copy(out[size-len(b):], b)
	return BytesToHex(out)
}

// RemoveHexPrefix removes the 0x prefix from a hex string if present
func RemoveHexPrefix(s string) string {
	if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
		return s[2:]
	}
	return s
}

// AddHexPrefix adds the 0x prefix to a hex string if not present
func AddHexPrefix(s string) string {
	if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
		return s
	}
	return "0x" + s
}

// DecimalStringToHex normalizes a numeric string (decimal or hex) to a canonical
// QUANTITY per Ethereum JSON-RPC: 0x-prefixed, no leading zeros; zero is 0x0.
// If the input has a 0x prefix, it is parsed as hex and re-encoded canonically.
// Otherwise it is parsed as base-10 and encoded canonically.
func DecimalStringToHex(s string) (string, error) {
	if s == "" {
		return "0x0", nil
	}
	s = strings.TrimSpace(s)
	var z big.Int
	if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
		hexStr := RemoveHexPrefix(s)
		if hexStr == "" {
			return "0x0", nil
		}
		if _, ok := z.SetString(hexStr, 16); !ok {
			return "", fmt.Errorf("invalid hex string: %s", s)
		}
	} else {
		if _, ok := z.SetString(s, 10); !ok {
			return "", fmt.Errorf("invalid decimal string: %s", s)
		}
	}
	if z.Sign() == 0 {
		return "0x0", nil
	}
	return AddHexPrefix(z.Text(16)), nil
}

// BytesToQuantityHex encodes bytes as a JSON-RPC QUANTITY (no leading zeros; 0x0 for zero).
func BytesToQuantityHex(b []byte) string {
	if len(b) == 0 {
		return "0x0"
	}
	n := new(big.Int).SetBytes(b)
	if n.Sign() == 0 {
		return "0x0"
	}
	return AddHexPrefix(n.Text(16))
}

// NormalizeHex canonicalizes QUANTITY inputs per EIP-1474.
// - If value is a string: treat as decimal or 0x-hex and return canonical QUANTITY; non-numeric strings are returned as-is (e.g., "latest").
// - If value is a number: formats as canonical 0x-prefixed hex without leading zeros.
func NormalizeHex(value interface{}) (string, error) {
	if s, ok := value.(string); ok {
		if out, err := DecimalStringToHex(s); err == nil {
			return out, nil
		}
		// likely a tag like "latest"/"pending"; return unchanged
		return s, nil
	}
	switch v := value.(type) {
	case int:
		return fmt.Sprintf("0x%x", v), nil
	case int8, int16, int32, int64:
		return fmt.Sprintf("0x%x", v), nil
	case uint, uint8, uint16, uint32, uint64:
		return fmt.Sprintf("0x%x", v), nil
	default:
		return "", fmt.Errorf("value is not a string or number: %+v", v)
	}
}

// NumberishString is a string-like type that can unmarshal from either a JSON
// string (hex or decimal) or a JSON number. Internally it stores the canonical
// 0x-prefixed QUANTITY form for numeric inputs; non-numeric strings (e.g., tags
// like "latest") are preserved as-is.
type NumberishString string

// UnmarshalJSON accepts numbers or strings. For numeric inputs (number literal,
// decimal string, or 0x-hex string) it stores the canonical 0x-hex QUANTITY.
// For null it stores empty string. For non-numeric strings it stores as-is.
func (ns *NumberishString) UnmarshalJSON(data []byte) error {
	// null -> empty string
	if bytes.Equal(data, []byte("null")) {
		*ns = NumberishString("")
		return nil
	}
	// If quoted: try to normalize string numerics; if not numeric, keep raw
	if len(data) >= 2 && data[0] == '"' && data[len(data)-1] == '"' {
		var s string
		if err := json.Unmarshal(data, &s); err != nil {
			return err
		}
		if hexStr, err := DecimalStringToHex(s); err == nil {
			*ns = NumberishString(hexStr)
			return nil
		}
		// Not a numeric string (e.g., "latest"), keep as-is
		*ns = NumberishString(s)
		return nil
	}
	// Unquoted token: treat as JSON number; store canonical 0x hex
	normalized, err := NormalizeHex(string(data))
	if err != nil {
		return err
	}
	*ns = NumberishString(normalized)
	return nil
}

// ToUint64 converts the stored value to uint64 when it represents a numeric
// quantity (either canonical 0x-hex or decimal). Returns an error when the
// value is empty or a non-numeric string like "latest".
func (ns NumberishString) ToUint64() (uint64, error) {
	s := string(ns)
	if s == "" {
		return 0, fmt.Errorf("empty NumberishString")
	}
	// If already canonical 0x quantity, use hex path; else try decimal
	if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
		return HexToUint64(s)
	}
	return strconv.ParseUint(s, 10, 64)
}

// Pointer helper functions

// Uint32Ptr returns a pointer to the given uint32 value
func Uint32Ptr(v uint32) *uint32 {
	return &v
}

// Uint64Ptr returns a pointer to the given uint64 value
func Uint64Ptr(v uint64) *uint64 {
	return &v
}

// StringPtr returns a pointer to the given string value
func StringPtr(s string) *string {
	return &s
}

// BoolPtr returns a pointer to the given bool value
func BoolPtr(b bool) *bool {
	return &b
}

// BytesPtr returns a pointer to the given byte slice
func BytesPtr(b []byte) *[]byte {
	return &b
}

// Topic filter helpers

// NewTopicFilter creates a TopicFilter with the given values
func NewTopicFilter(values ...string) (*TopicFilter, error) {
	filter := &TopicFilter{
		Values: make([][]byte, 0, len(values)),
	}

	for _, v := range values {
		b, err := HexToBytes(v)
		if err != nil {
			return nil, fmt.Errorf("invalid topic value %s: %w", v, err)
		}
		filter.Values = append(filter.Values, b)
	}

	return filter, nil
}

// MustNewTopicFilter creates a TopicFilter with the given values, panicking on error
func MustNewTopicFilter(values ...string) *TopicFilter {
	filter, err := NewTopicFilter(values...)
	if err != nil {
		panic(err)
	}
	return filter
}

// Common query builders

// BuildTransferLogQuery builds a GetLogsRequest for ERC20/ERC721 Transfer events
func BuildTransferLogQuery(fromBlock, toBlock uint64, contractAddresses []string) (*GetLogsRequest, error) {
	req := &GetLogsRequest{
		FromBlock: &fromBlock,
		ToBlock:   &toBlock,
		Addresses: make([][]byte, 0, len(contractAddresses)),
	}

	// Add contract addresses
	for _, addr := range contractAddresses {
		b, err := HexToBytes(addr)
		if err != nil {
			return nil, fmt.Errorf("invalid contract address %s: %w", addr, err)
		}
		req.Addresses = append(req.Addresses, b)
	}

	// Add Transfer event signature as first topic
	transferTopic, err := NewTopicFilter(TransferEventSignature)
	if err != nil {
		return nil, err
	}
	req.Topics = []*TopicFilter{transferTopic}

	return req, nil
}

// IsZeroAddress checks if an address is the zero address
func IsZeroAddress(addr Address) bool {
	for _, b := range addr {
		if b != 0 {
			return false
		}
	}
	return len(addr) == AddressLength
}

// IsZeroHash checks if a hash is the zero hash
func IsZeroHash(h Hash) bool {
	for _, b := range h {
		if b != 0 {
			return false
		}
	}
	return len(h) == HashLength
}
