package analytics

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

	"github.com/jackc/pgx/v5/pgxpool"
)

// TokenDistribution provides token distribution analytics
type TokenDistribution struct {
	db      *pgxpool.Pool
	chainID int
}

// NewTokenDistribution creates a new token distribution analyzer
func NewTokenDistribution(db *pgxpool.Pool, chainID int) *TokenDistribution {
	return &TokenDistribution{
		db:      db,
		chainID: chainID,
	}
}

// DistributionStats represents token distribution statistics
type DistributionStats struct {
	Contract     string
	Symbol       string
	TotalSupply  string
	Holders      int
	Distribution map[string]string
	TopHolders   []HolderInfo
}

// HolderInfo represents holder information
type HolderInfo struct {
	Address    string
	Balance    string
	Percentage string
}

// GetTokenDistribution gets token distribution for a contract
func (td *TokenDistribution) GetTokenDistribution(ctx context.Context, contract string, topN int) (*DistributionStats, error) {
	// Refresh the materialized view. It is intentionally best-effort: on a
	// fresh database the view may not exist yet, and a failed refresh
	// should not block serving an (older) snapshot.
	if _, err := td.db.Exec(ctx, `REFRESH MATERIALIZED VIEW CONCURRENTLY token_distribution`); err != nil {
		_ = err
	}

	// Get distribution from materialized view
	query := `
		SELECT holder_count, total_balance
		FROM token_distribution
		WHERE token_contract = $1 AND chain_id = $2
	`

	var holders int
	var totalSupply string
	if err := td.db.QueryRow(ctx, query, contract, td.chainID).Scan(&holders, &totalSupply); err != nil {
		return nil, fmt.Errorf("failed to get distribution: %w", err)
	}

	// Get top holders
	topHoldersQuery := `
		SELECT address, balance
		FROM token_balances
		WHERE token_contract = $1 AND chain_id = $2 AND balance > 0
		ORDER BY balance DESC
		LIMIT $3
	`

	rows, err := td.db.Query(ctx, topHoldersQuery, contract, td.chainID, topN)
	if err != nil {
		return nil, fmt.Errorf("failed to get top holders: %w", err)
	}
	defer rows.Close()

	topHolders := []HolderInfo{}
	totalSupplyRat, ok := parseNumericString(totalSupply)
	if !ok || totalSupplyRat.Sign() <= 0 {
		totalSupplyRat = big.NewRat(0, 1)
	}
	for rows.Next() {
		var holder HolderInfo
		if err := rows.Scan(&holder.Address, &holder.Balance); err != nil {
			continue
		}
		holder.Percentage = formatPercentage(holder.Balance, totalSupplyRat, 4)
		topHolders = append(topHolders, holder)
	}

	stats := &DistributionStats{
		Contract:     contract,
		Holders:      holders,
		TotalSupply:  totalSupply,
		Distribution: make(map[string]string),
		TopHolders:   topHolders,
	}

	balances, err := td.loadHolderBalances(ctx, contract)
	if err != nil {
		return nil, fmt.Errorf("failed to compute holder metrics: %w", err)
	}

	stats.Distribution["top_10_percent"] = concentrationPercent(balances, totalSupplyRat, 0.10)
	stats.Distribution["top_1_percent"] = concentrationPercent(balances, totalSupplyRat, 0.01)
	stats.Distribution["gini_coefficient"] = giniCoefficient(balances)

	return stats, nil
}

func (td *TokenDistribution) loadHolderBalances(ctx context.Context, contract string) ([]*big.Rat, error) {
	rows, err := td.db.Query(ctx, `
		SELECT balance
		FROM token_balances
		WHERE token_contract = $1 AND chain_id = $2 AND balance > 0
		ORDER BY balance DESC
	`, contract, td.chainID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	balances := make([]*big.Rat, 0)
	for rows.Next() {
		var raw string
		if err := rows.Scan(&raw); err != nil {
			continue
		}
		if balance, ok := parseNumericString(raw); ok && balance.Sign() > 0 {
			balances = append(balances, balance)
		}
	}

	return balances, nil
}

func parseNumericString(raw string) (*big.Rat, bool) {
	value, ok := new(big.Rat).SetString(raw)
	return value, ok
}

func formatPercentage(raw string, total *big.Rat, decimals int) string {
	value, ok := parseNumericString(raw)
	if !ok {
		return "0"
	}
	return formatRatioAsPercent(value, total, decimals)
}

func concentrationPercent(balances []*big.Rat, total *big.Rat, percentile float64) string {
	if len(balances) == 0 {
		return "0"
	}

	count := int(math.Ceil(float64(len(balances)) * percentile))
	if count < 1 {
		count = 1
	}
	if count > len(balances) {
		count = len(balances)
	}

	sum := new(big.Rat)
	for i := 0; i < count; i++ {
		sum.Add(sum, balances[i])
	}

	return formatRatioAsPercent(sum, total, 4)
}

func formatRatioAsPercent(value, total *big.Rat, decimals int) string {
	if value == nil || total == nil || total.Sign() <= 0 {
		return "0"
	}

	percent := new(big.Rat).Quo(value, total)
	percent.Mul(percent, big.NewRat(100, 1))
	return formatRat(percent, decimals)
}

func giniCoefficient(balances []*big.Rat) string {
	if len(balances) == 0 {
		return "0"
	}

	total := new(big.Rat)
	for _, balance := range balances {
		total.Add(total, balance)
	}
	if total.Sign() <= 0 {
		return "0"
	}

	weightedSum := new(big.Rat)
	n := len(balances)
	for i := range balances {
		index := n - i
		weighted := new(big.Rat).Mul(balances[i], big.NewRat(int64(index), 1))
		weightedSum.Add(weightedSum, weighted)
	}

	nRat := big.NewRat(int64(n), 1)
	numerator := new(big.Rat).Mul(weightedSum, big.NewRat(2, 1))
	denominator := new(big.Rat).Mul(nRat, total)
	gini := new(big.Rat).Quo(numerator, denominator)
	gini.Sub(gini, new(big.Rat).Quo(big.NewRat(int64(n+1), 1), nRat))

	if gini.Sign() < 0 {
		return "0"
	}

	return formatRat(gini, 6)
}

func formatRat(value *big.Rat, decimals int) string {
	if value == nil {
		return "0"
	}
	text := new(big.Float).SetPrec(256).SetRat(value).Text('f', decimals)
	for len(text) > 1 && text[len(text)-1] == '0' {
		text = text[:len(text)-1]
	}
	if len(text) > 1 && text[len(text)-1] == '.' {
		text = text[:len(text)-1]
	}
	return text
}
