package rest

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/explorer/backend/api/freshness"
)

type explorerStats struct {
	TotalBlocks                  int64                         `json:"total_blocks"`
	TotalTransactions            int64                         `json:"total_transactions"`
	TotalAddresses               int64                         `json:"total_addresses"`
	LatestBlock                  int64                         `json:"latest_block"`
	AverageBlockTime             *float64                      `json:"average_block_time,omitempty"`
	GasPrices                    *explorerGasPrices            `json:"gas_prices,omitempty"`
	NetworkUtilizationPercentage *float64                      `json:"network_utilization_percentage,omitempty"`
	TransactionsToday            *int64                        `json:"transactions_today,omitempty"`
	Freshness                    freshness.Snapshot            `json:"freshness"`
	Completeness                 freshness.SummaryCompleteness `json:"completeness"`
	Sampling                     freshness.Sampling            `json:"sampling"`
	Diagnostics                  freshness.Diagnostics         `json:"diagnostics"`
}

type explorerGasPrices struct {
	Average *float64 `json:"average,omitempty"`
}

type statsQueryFunc = freshness.QueryRowFunc

func queryNullableFloat64(ctx context.Context, queryRow statsQueryFunc, query string, args ...any) (*float64, error) {
	var value sql.NullFloat64
	if err := queryRow(ctx, query, args...).Scan(&value); err != nil {
		return nil, err
	}
	if !value.Valid {
		return nil, nil
	}
	return &value.Float64, nil
}

func queryNullableInt64(ctx context.Context, queryRow statsQueryFunc, query string, args ...any) (*int64, error) {
	var value sql.NullInt64
	if err := queryRow(ctx, query, args...).Scan(&value); err != nil {
		return nil, err
	}
	if !value.Valid {
		return nil, nil
	}
	return &value.Int64, nil
}

func loadExplorerStats(ctx context.Context, chainID int, queryRow statsQueryFunc) (explorerStats, error) {
	var stats explorerStats
	_ = chainID

	if err := queryRow(ctx,
		`SELECT COUNT(*) FROM blocks`,
	).Scan(&stats.TotalBlocks); err != nil {
		return explorerStats{}, fmt.Errorf("query total blocks: %w", err)
	}

	if err := queryRow(ctx,
		`SELECT COUNT(*) FROM transactions WHERE block_hash IS NOT NULL`,
	).Scan(&stats.TotalTransactions); err != nil {
		return explorerStats{}, fmt.Errorf("query total transactions: %w", err)
	}

	if err := queryRow(ctx,
		`SELECT COUNT(*) FROM (
			SELECT from_address_hash AS address
			FROM transactions
			WHERE from_address_hash IS NOT NULL
			UNION
			SELECT to_address_hash AS address
			FROM transactions
			WHERE to_address_hash IS NOT NULL
		) unique_addresses`,
	).Scan(&stats.TotalAddresses); err != nil {
		return explorerStats{}, fmt.Errorf("query total addresses: %w", err)
	}

	if err := queryRow(ctx,
		`SELECT COALESCE(MAX(number), 0) FROM blocks`,
	).Scan(&stats.LatestBlock); err != nil {
		return explorerStats{}, fmt.Errorf("query latest block: %w", err)
	}

	statsIssues := map[string]string{}

	averageBlockTime, err := queryNullableFloat64(ctx, queryRow,
		`SELECT CASE
		     WHEN COUNT(*) >= 2
		       THEN (EXTRACT(EPOCH FROM (MAX(timestamp) - MIN(timestamp))) * 1000.0) / NULLIF(COUNT(*) - 1, 0)
		     ELSE NULL
		   END
		 FROM (
		   SELECT timestamp
		   FROM blocks
		   ORDER BY number DESC
		   LIMIT 100
		 ) recent_blocks`,
	)
	if err != nil {
		statsIssues["average_block_time"] = err.Error()
	} else {
		stats.AverageBlockTime = averageBlockTime
	}

	averageGasPrice, err := queryNullableFloat64(ctx, queryRow,
		`SELECT AVG(gas_price_wei)::double precision / 1000000000.0
		 FROM (
		   SELECT gas_price AS gas_price_wei
		   FROM transactions
		   WHERE block_hash IS NOT NULL
		     AND gas_price IS NOT NULL
		   ORDER BY block_number DESC, "index" DESC
		   LIMIT 1000
		 ) recent_transactions`,
	)
	if err != nil {
		statsIssues["average_gas_price"] = err.Error()
	} else if averageGasPrice != nil {
		stats.GasPrices = &explorerGasPrices{Average: averageGasPrice}
	}

	networkUtilization, err := queryNullableFloat64(ctx, queryRow,
		`SELECT AVG((gas_used::double precision / NULLIF(gas_limit, 0)) * 100.0)
		 FROM (
		   SELECT gas_used, gas_limit
		   FROM blocks
		   WHERE gas_limit IS NOT NULL
		     AND gas_limit > 0
		   ORDER BY number DESC
		   LIMIT 100
		 ) recent_blocks`,
	)
	if err != nil {
		statsIssues["network_utilization_percentage"] = err.Error()
	} else {
		stats.NetworkUtilizationPercentage = networkUtilization
	}

	transactionsToday, err := queryNullableInt64(ctx, queryRow,
		`SELECT COUNT(*)::bigint
		 FROM transactions t
		 JOIN blocks b
		   ON b.number = t.block_number
		 WHERE b.timestamp >= NOW() - INTERVAL '24 hours'`,
	)
	if err != nil {
		statsIssues["transactions_today"] = err.Error()
	} else {
		stats.TransactionsToday = transactionsToday
	}

	rpcURL := strings.TrimSpace(os.Getenv("RPC_URL"))
	snapshot, completeness, sampling, diagnostics, err := freshness.BuildSnapshot(
		ctx,
		chainID,
		queryRow,
		func(ctx context.Context) (*freshness.Reference, error) {
			return freshness.ProbeChainHead(ctx, rpcURL)
		},
		time.Now().UTC(),
		averageGasPrice,
		networkUtilization,
	)
	if err != nil {
		return explorerStats{}, fmt.Errorf("build freshness snapshot: %w", err)
	}
	if len(statsIssues) > 0 {
		if sampling.Issues == nil {
			sampling.Issues = map[string]string{}
		}
		for key, value := range statsIssues {
			sampling.Issues[key] = value
		}
	}
	stats.Freshness = snapshot
	stats.Completeness = completeness
	stats.Sampling = sampling
	stats.Diagnostics = diagnostics

	return stats, nil
}

// handleStats handles GET /api/v2/stats
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		writeMethodNotAllowed(w)
		return
	}
	if !s.requireDB(w) {
		return
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	stats, err := loadExplorerStats(ctx, s.chainID, s.db.QueryRow)
	if err != nil {
		writeError(w, http.StatusServiceUnavailable, "service_unavailable", "explorer stats are temporarily unavailable")
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(stats)
}
