package alp

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

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/ethclient"
	db "github.com/gothinkster/golang-gin-realworld-example-app/common"
	"github.com/gothinkster/golang-gin-realworld-example-app/config"
	"github.com/gothinkster/golang-gin-realworld-example-app/models"
)

const (
	ytPoolManagerAddress = "0xb11824eAA659F8A4648711709dA60720d5Cdabd2"
	usdyAddress          = "0x29774970556407fAE16BC07e87704fE0E9559BC4"
	alpSnapshotInterval  = 1 * time.Hour
)

const poolManagerABIJSON = `[
	{
		"inputs":[{"internalType":"bool","name":"_maximise","type":"bool"}],
		"name":"getAumInUsdy",
		"outputs":[{"internalType":"uint256","name":"","type":"uint256"}],
		"stateMutability":"view","type":"function"
	},
	{
		"inputs":[{"internalType":"bool","name":"_maximise","type":"bool"}],
		"name":"getPrice",
		"outputs":[{"internalType":"uint256","name":"","type":"uint256"}],
		"stateMutability":"view","type":"function"
	}
]`

const erc20TotalSupplyABIJSON = `[{
	"inputs":[],
	"name":"totalSupply",
	"outputs":[{"internalType":"uint256","name":"","type":"uint256"}],
	"stateMutability":"view","type":"function"
}]`

// StartALPSnapshot starts the ALP pool snapshot service (interval: 1h).
func StartALPSnapshot(cfg *config.Config) {
	log.Println("=== ALP Snapshot Service Started (interval: 1h) ===")
	runALPSnapshot(cfg)
	ticker := time.NewTicker(alpSnapshotInterval)
	defer ticker.Stop()
	for range ticker.C {
		runALPSnapshot(cfg)
	}
}

func runALPSnapshot(cfg *config.Config) {
	start := time.Now()
	log.Printf("[ALPSnapshot] Starting at %s", start.Format("2006-01-02 15:04:05"))

	client, err := ethclient.Dial(cfg.BSCTestnetRPC)
	if err != nil {
		log.Printf("[ALPSnapshot] RPC connect error: %v", err)
		return
	}
	defer client.Close()

	poolValue, alpPrice, err := fetchPoolStats(client)
	if err != nil {
		log.Printf("[ALPSnapshot] fetchPoolStats error: %v", err)
		return
	}

	usdySupply, err := fetchUSDYSupply(client)
	if err != nil {
		log.Printf("[ALPSnapshot] fetchUSDYSupply error: %v", err)
		return
	}

	feeSurplus := poolValue - usdySupply

	snap := models.ALPSnapshot{
		PoolValue:    poolValue,
		UsdySupply:   usdySupply,
		FeeSurplus:   feeSurplus,
		ALPPrice:     alpPrice,
		SnapshotTime: time.Now().UTC(),
	}
	if err := db.GetDB().Create(&snap).Error; err != nil {
		log.Printf("[ALPSnapshot] DB save error: %v", err)
		return
	}

	log.Printf("[ALPSnapshot] poolValue=%.6f USDY, usdySupply=%.6f, feeSurplus=%.6f, alpPrice=%.8f | done in %v",
		poolValue, usdySupply, feeSurplus, alpPrice, time.Since(start))
}

func fetchPoolStats(client *ethclient.Client) (poolValue float64, alpPrice float64, err error) {
	pmABI, err := abi.JSON(strings.NewReader(poolManagerABIJSON))
	if err != nil {
		return 0, 0, fmt.Errorf("parse poolManager ABI: %w", err)
	}

	pmAddr := common.HexToAddress(ytPoolManagerAddress)
	ctx := context.Background()

	// getAumInUsdy(true)
	callData, err := pmABI.Pack("getAumInUsdy", true)
	if err != nil {
		return 0, 0, fmt.Errorf("pack getAumInUsdy: %w", err)
	}
	result, err := client.CallContract(ctx, ethereum.CallMsg{To: &pmAddr, Data: callData}, nil)
	if err != nil {
		return 0, 0, fmt.Errorf("call getAumInUsdy: %w", err)
	}
	decoded, err := pmABI.Unpack("getAumInUsdy", result)
	if err != nil || len(decoded) == 0 {
		return 0, 0, fmt.Errorf("unpack getAumInUsdy: %w", err)
	}
	poolValue = bigToFloat(decoded[0].(*big.Int), 18)

	// getPrice(false)
	callData, err = pmABI.Pack("getPrice", false)
	if err != nil {
		return 0, 0, fmt.Errorf("pack getPrice: %w", err)
	}
	result, err = client.CallContract(ctx, ethereum.CallMsg{To: &pmAddr, Data: callData}, nil)
	if err != nil {
		return 0, 0, fmt.Errorf("call getPrice: %w", err)
	}
	decoded, err = pmABI.Unpack("getPrice", result)
	if err != nil || len(decoded) == 0 {
		return 0, 0, fmt.Errorf("unpack getPrice: %w", err)
	}
	alpPrice = bigToFloat(decoded[0].(*big.Int), 18)

	return poolValue, alpPrice, nil
}

func fetchUSDYSupply(client *ethclient.Client) (float64, error) {
	supplyABI, err := abi.JSON(strings.NewReader(erc20TotalSupplyABIJSON))
	if err != nil {
		return 0, fmt.Errorf("parse totalSupply ABI: %w", err)
	}

	usdyAddr := common.HexToAddress(usdyAddress)
	callData, err := supplyABI.Pack("totalSupply")
	if err != nil {
		return 0, fmt.Errorf("pack totalSupply: %w", err)
	}
	result, err := client.CallContract(context.Background(), ethereum.CallMsg{To: &usdyAddr, Data: callData}, nil)
	if err != nil {
		return 0, fmt.Errorf("call totalSupply: %w", err)
	}
	decoded, err := supplyABI.Unpack("totalSupply", result)
	if err != nil || len(decoded) == 0 {
		return 0, fmt.Errorf("unpack totalSupply: %w", err)
	}
	return bigToFloat(decoded[0].(*big.Int), 18), nil
}

func bigToFloat(n *big.Int, decimals int64) float64 {
	divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(decimals), nil)
	f, _ := new(big.Float).SetPrec(256).Quo(
		new(big.Float).SetPrec(256).SetInt(n),
		new(big.Float).SetPrec(256).SetInt(divisor),
	).Float64()
	return f
}
