// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package primary

import (
	"context"
	"fmt"
	"math/big"
	"strings"

	gethcommon "github.com/luxfi/geth/common"
	"github.com/luxfi/geth/ethclient"

	"github.com/luxfi/codec"
	"github.com/luxfi/constants"
	"github.com/luxfi/ids"
	"github.com/luxfi/math/set"
	lux "github.com/luxfi/utxo"
	"github.com/luxfi/node/vms/platformvm"
	"github.com/luxfi/node/wallet/chain/p"
	"github.com/luxfi/node/wallet/chain/x"
	"github.com/luxfi/rpc"
	"github.com/luxfi/sdk/info"

	ethcommon "github.com/luxfi/geth/common"
	ptxs "github.com/luxfi/node/vms/platformvm/txs"
	pbuilder "github.com/luxfi/node/wallet/chain/p/builder"
	xbuilder "github.com/luxfi/node/wallet/chain/x/builder"
	walletcommon "github.com/luxfi/node/wallet/network/primary/common"
)

// EthAccount represents an Ethereum account's state
type EthAccount struct {
	Balance *big.Int
	Nonce   uint64
}

const (
	MainnetAPIURI = "https://api.lux.network"
	TestnetAPIURI = "https://api.lux-test.network"
	LocalAPIURI   = "http://localhost:9630"

	fetchLimit = 1024
)

// perform their own assertions.
var (
	_ UTXOClient = (*platformvm.Client)(nil)
	_ UTXOClient = (*XClient)(nil)
)

type UTXOClient interface {
	GetAtomicUTXOs(
		ctx context.Context,
		addrs []ids.ShortID,
		sourceChain string,
		limit uint32,
		startAddress ids.ShortID,
		startUTXOID ids.ID,
		options ...rpc.Option,
	) ([][]byte, ids.ShortID, ids.ID, error)
}

// XClient is a client for interacting with the X-Chain
type XClient struct {
	requester rpc.EndpointRequester
}

// NewXClient returns a new X-Chain client
func NewXClient(uri, chainAlias string) *XClient {
	return &XClient{
		requester: rpc.NewEndpointRequester(
			fmt.Sprintf("%s/ext/bc/%s", uri, chainAlias),
		),
	}
}

// GetAtomicUTXOs implements UTXOClient.
// For local/dev networks where X-chain atomic operations aren't needed,
// this returns empty to allow P-chain operations to proceed.
func (c *XClient) GetAtomicUTXOs(
	ctx context.Context,
	addrs []ids.ShortID,
	sourceChain string,
	limit uint32,
	startAddress ids.ShortID,
	startUTXOID ids.ID,
	options ...rpc.Option,
) ([][]byte, ids.ShortID, ids.ID, error) {
	// Return empty for X-chain since atomic UTXOs are rarely needed
	return nil, ids.ShortEmpty, ids.Empty, nil
}

type LUXState struct {
	PClient *platformvm.Client
	PCTX    *pbuilder.Context
	XClient *XClient
	XCTX    *xbuilder.Context
	// CClient evm.Client // Implementation note
	// CCTX    *c.Context
	UTXOs walletcommon.UTXOs
}

func FetchState(
	ctx context.Context,
	uri string,
	addrs set.Set[ids.ShortID],
) (
	*LUXState,
	error,
) {
	infoClient := info.NewClient(uri)
	pClient := platformvm.NewClient(uri)
	// cClient := evm.NewCChainClient(uri) // Implementation note

	pCTX, err := p.NewContextFromClients(ctx, infoClient, pClient)
	if err != nil {
		return nil, err
	}

	// Set network ID on pClient for proper bech32 address formatting
	pClient.SetNetworkID(pCTX.NetworkID)

	// X-chain fees are derived from the P-chain context. The fee values
	// here match the primary network defaults and are overridden at the
	// chain level when dynamic fees are enabled.
	utxoAssetID := pCTX.UTXOAssetID
	baseTxFee := uint64(1000000)         // 0.001 LUX
	createAssetTxFee := uint64(10000000) // 0.01 LUX

	// X-Chain is opt-in: post-coreth networks (e.g. test+dev primary that
	// bake only P + EVM genesis chains) run in P-only mode where the X
	// alias is not registered. We fail-soft to an empty XCTX in that case
	// — XCTX.BlockchainID == ids.Empty becomes a stable sentinel, and the
	// UTXO scan below treats it as "no X-chain UTXOs to fetch" because the
	// XClient.GetAtomicUTXOs returns empty. This lets MakeWallet succeed
	// in P-only mode without leaking X-chain queries into wallet callers.
	xCTX, err := x.NewContextFromClients(ctx, infoClient, utxoAssetID, baseTxFee, createAssetTxFee)
	if err != nil {
		// One specific class of failure is expected: "no ID with alias X".
		// Any other error (network, codec, etc.) is fatal and must surface.
		if !isXChainNotEnabled(err) {
			return nil, err
		}
		xCTX = &xbuilder.Context{
			NetworkID:        pCTX.NetworkID,
			BlockchainID:     ids.Empty, // sentinel: X-Chain disabled in this network
			UTXOAssetID:      utxoAssetID,
			BaseTxFee:        baseTxFee,
			CreateAssetTxFee: createAssetTxFee,
		}
	}

	// Create X-chain client using standard "X" alias
	xClient := NewXClient(uri, "X")

	// cCTX, err := c.NewContextFromClients(ctx, infoClient, xClient)
	// if err != nil {
	// 	return nil, err
	// }

	utxos := walletcommon.NewUTXOs()
	addrList := addrs.List()
	chains := []struct {
		id     ids.ID
		client UTXOClient
		codec  codec.Manager
	}{
		{
			id:     constants.PlatformChainID,
			client: pClient,
			codec:  ptxs.Codec,
		},
	}
	// Only include the X-chain entry when the network actually serves an
	// X-Chain. In P-only mode (xCTX.BlockchainID == ids.Empty) skipping
	// the pair avoids spurious `platform.getUTXOs` calls with sourceChain
	// = zero ID, which the P-chain API rejects.
	if xCTX.BlockchainID != ids.Empty {
		chains = append(chains, struct {
			id     ids.ID
			client UTXOClient
			codec  codec.Manager
		}{
			id:     xCTX.BlockchainID,
			client: xClient,
			codec:  codec.NewDefaultManager(),
		})
	}
	// {
	// 	id:     cCTX.BlockchainID,
	// 	client: cClient,
	// 	codec:  evm.Codec,
	// },
	for _, destinationChain := range chains {
		for _, sourceChain := range chains {
			err = AddAllUTXOs(
				ctx,
				utxos,
				destinationChain.client,
				destinationChain.codec,
				sourceChain.id,
				destinationChain.id,
				addrList,
			)
			if err != nil {
				return nil, err
			}
		}
	}
	return &LUXState{
		PClient: pClient,
		PCTX:    pCTX,
		XClient: xClient,
		XCTX:    xCTX,
		// CClient: cClient,
		// CCTX:    cCTX,
		UTXOs: utxos,
	}, nil
}

type EthState struct {
	Client   *ethclient.Client
	Accounts map[ethcommon.Address]*EthAccount
}

func FetchEthState(
	ctx context.Context,
	uri string,
	addrs set.Set[ethcommon.Address],
) (*EthState, error) {
	path := fmt.Sprintf(
		"%s/ext/%s/C/rpc",
		uri,
		constants.ChainAliasPrefix,
	)
	client, err := ethclient.Dial(path)
	if err != nil {
		return nil, err
	}

	accounts := make(map[ethcommon.Address]*EthAccount, addrs.Len())
	for addr := range addrs {
		// Convert ethereum address to geth address
		gethAddr := gethcommon.Address(addr)
		balance, err := client.BalanceAt(ctx, gethAddr, nil)
		if err != nil {
			return nil, err
		}
		nonce, err := client.NonceAt(ctx, gethAddr, nil)
		if err != nil {
			return nil, err
		}
		accounts[addr] = &EthAccount{
			Balance: balance,
			Nonce:   nonce,
		}
	}
	return &EthState{
		Client:   client,
		Accounts: accounts,
	}, nil
}

// AddAllUTXOs fetches all the UTXOs referenced by [addresses] that were sent
// from [sourceChainID] to [destinationChainID] from the [client]. It then uses
// [codec] to parse the returned UTXOs and it adds them into [utxos]. If [ctx]
// expires, then the returned error will be immediately reported.
func AddAllUTXOs(
	ctx context.Context,
	utxos walletcommon.UTXOs,
	client UTXOClient,
	codec codec.Manager,
	sourceChainID ids.ID,
	destinationChainID ids.ID,
	addrs []ids.ShortID,
) error {
	var (
		// When source == destination, use empty string to fetch native UTXOs
		// Otherwise use the chain ID to fetch atomic UTXOs
		sourceChainIDStr string
		startAddr        ids.ShortID
		startUTXO        ids.ID
	)
	if sourceChainID != destinationChainID {
		sourceChainIDStr = sourceChainID.String()
	}
	for {
		utxosBytes, endAddr, endUTXO, err := client.GetAtomicUTXOs(
			ctx,
			addrs,
			sourceChainIDStr,
			fetchLimit,
			startAddr,
			startUTXO,
		)
		if err != nil {
			return err
		}

		for _, utxoBytes := range utxosBytes {
			var utxo lux.UTXO
			_, err := codec.Unmarshal(utxoBytes, &utxo)
			if err != nil {
				return err
			}

			if err := utxos.AddUTXO(ctx, sourceChainID, destinationChainID, &utxo); err != nil {
				return err
			}
		}

		if len(utxosBytes) < fetchLimit {
			break
		}

		// Update the vars to query the next page of UTXOs.
		startAddr = endAddr
		startUTXO = endUTXO
	}
	return nil
}

// isXChainNotEnabled detects the canonical "info.getBlockchainID returns
// no-such-alias for X" error that surfaces when running against a P-only
// network (one whose platform genesis does not include an XVM chain).
// We match by substring of the JSON-RPC error message rather than a typed
// sentinel because the upstream info-service emits a free-form string;
// re-evaluate this matcher if/when info.getBlockchainID gains a typed
// error path.
func isXChainNotEnabled(err error) bool {
	if err == nil {
		return false
	}
	msg := strings.ToLower(err.Error())
	return strings.Contains(msg, "there is no id with alias: x") ||
		strings.Contains(msg, "no chain with alias")
}
