package server

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net"
	"net/http"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"time"

	"git.eeqj.de/sneak/routewatch/internal/database"
	"git.eeqj.de/sneak/routewatch/internal/templates"
	asinfo "git.eeqj.de/sneak/routewatch/pkg/asinfo"
	"github.com/dustin/go-humanize"
	"github.com/go-chi/chi/v5"
)

const (
	// statsContextTimeout is the timeout for stats API operations.
	statsContextTimeout = 4 * time.Second

	// healthCheckTimeout is the timeout for health check operations.
	healthCheckTimeout = 2 * time.Second
)

// HealthCheckResponse represents the health check response.
type HealthCheckResponse struct {
	Status    string            `json:"status"`
	Timestamp string            `json:"timestamp"`
	Checks    map[string]string `json:"checks"`
}

// handleHealthCheck returns a handler that performs health checks.
// Returns 200 if healthy, 503 if any check fails.
// Uses lightweight checks to avoid timeout issues under load.
func (s *Server) handleHealthCheck() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx, cancel := context.WithTimeout(r.Context(), healthCheckTimeout)
		defer cancel()

		checks := make(map[string]string)
		healthy := true

		// Check database connectivity with lightweight ping
		err := s.db.Ping(ctx)
		if err != nil {
			checks["database"] = "error: " + err.Error()
			healthy = false
		} else {
			checks["database"] = "ok"
		}

		// Check streamer connection
		metrics := s.streamer.GetMetrics()
		if metrics.Connected {
			checks["ris_live"] = "ok"
		} else {
			checks["ris_live"] = "disconnected"
			healthy = false
		}

		// Build response
		status := "ok"
		if !healthy {
			status = "error"
		}

		response := HealthCheckResponse{
			Status:    status,
			Timestamp: time.Now().UTC().Format(time.RFC3339),
			Checks:    checks,
		}

		if !healthy {
			w.WriteHeader(http.StatusServiceUnavailable)
		}

		if err := writeJSONSuccess(w, response); err != nil {
			s.logger.Error("Failed to encode health check response", "error", err)
		}
	}
}

// handleIndex returns a handler that serves the home page.
func (s *Server) handleIndex() http.HandlerFunc {
	return func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")

		tmpl := templates.IndexTemplate()
		if err := tmpl.Execute(w, nil); err != nil {
			s.logger.Error("Failed to render index template", "error", err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		}
	}
}

// writeJSONError writes a standardized JSON error response with the given
// status code and error message.
func writeJSONError(w http.ResponseWriter, statusCode int, message string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statusCode)
	_ = json.NewEncoder(w).Encode(map[string]interface{}{
		"status": "error",
		"error": map[string]interface{}{
			"msg":  message,
			"code": statusCode,
		},
	})
}

// writeJSONSuccess writes a standardized JSON success response containing
// the provided data wrapped in a status envelope.
func writeJSONSuccess(w http.ResponseWriter, data interface{}) error {
	w.Header().Set("Content-Type", "application/json")

	return json.NewEncoder(w).Encode(map[string]interface{}{
		"status": "ok",
		"data":   data,
	})
}

// WHOISStatsInfo contains WHOIS fetcher statistics for the status page.
type WHOISStatsInfo struct {
	TotalASNs         int     `json:"total_asns"`
	FreshASNs         int     `json:"fresh_asns"`
	StaleASNs         int     `json:"stale_asns"`
	NeverFetched      int     `json:"never_fetched"`
	SuccessesLastHour int     `json:"successes_last_hour"`
	ErrorsLastHour    int     `json:"errors_last_hour"`
	CurrentInterval   string  `json:"current_interval"`
	ConsecutiveFails  int     `json:"consecutive_fails"`
	FreshPercent      float64 `json:"fresh_percent"`
}

// handleStatusJSON returns a handler that serves JSON statistics including
// uptime, message counts, database stats, and route information.
func (s *Server) handleStatusJSON() http.HandlerFunc {
	// Stats represents the statistics response
	type Stats struct {
		Uptime                 string                        `json:"uptime"`
		TotalMessages          uint64                        `json:"total_messages"`
		TotalBytes             uint64                        `json:"total_bytes"`
		TotalWireBytes         uint64                        `json:"total_wire_bytes"`
		MessagesPerSec         float64                       `json:"messages_per_sec"`
		MbitsPerSec            float64                       `json:"mbits_per_sec"`
		WireMbitsPerSec        float64                       `json:"wire_mbits_per_sec"`
		Connected              bool                          `json:"connected"`
		GoVersion              string                        `json:"go_version"`
		Goroutines             int                           `json:"goroutines"`
		MemoryUsage            string                        `json:"memory_usage"`
		ASNs                   int                           `json:"asns"`
		Prefixes               int                           `json:"prefixes"`
		IPv4Prefixes           int                           `json:"ipv4_prefixes"`
		IPv6Prefixes           int                           `json:"ipv6_prefixes"`
		Peerings               int                           `json:"peerings"`
		Peers                  int                           `json:"peers"`
		DatabaseSizeBytes      int64                         `json:"database_size_bytes"`
		LiveRoutes             int                           `json:"live_routes"`
		IPv4Routes             int                           `json:"ipv4_routes"`
		IPv6Routes             int                           `json:"ipv6_routes"`
		OldestRoute            *time.Time                    `json:"oldest_route,omitempty"`
		NewestRoute            *time.Time                    `json:"newest_route,omitempty"`
		IPv4UpdatesPerSec      float64                       `json:"ipv4_updates_per_sec"`
		IPv6UpdatesPerSec      float64                       `json:"ipv6_updates_per_sec"`
		IPv4PrefixDistribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
		IPv6PrefixDistribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
		WHOISStats             *WHOISStatsInfo               `json:"whois_stats,omitempty"`
	}

	return func(w http.ResponseWriter, r *http.Request) {
		// Create a 4 second timeout context for this request
		ctx, cancel := context.WithTimeout(r.Context(), statsContextTimeout)
		defer cancel()

		metrics := s.streamer.GetMetrics()

		// Get database stats with timeout
		statsChan := make(chan database.Stats)
		errChan := make(chan error)

		go func() {
			dbStats, err := s.db.GetStatsContext(ctx)
			if err != nil {
				s.logger.Debug("Database stats query failed", "error", err)
				errChan <- err

				return
			}
			statsChan <- dbStats
		}()

		var dbStats database.Stats
		select {
		case <-ctx.Done():
			s.logger.Error("Database stats timeout in status.json")
			writeJSONError(w, http.StatusRequestTimeout, "Database timeout")

			return
		case err := <-errChan:
			s.logger.Error("Failed to get database stats", "error", err)
			writeJSONError(w, http.StatusInternalServerError, err.Error())

			return
		case dbStats = <-statsChan:
			// Success
		}

		uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
		if metrics.ConnectedSince.IsZero() {
			uptime = "0s"
		}

		const bitsPerMegabit = 1000000.0

		// Get route counts from database
		ipv4Routes, ipv6Routes, err := s.db.GetLiveRouteCountsContext(ctx)
		if err != nil {
			s.logger.Warn("Failed to get live route counts", "error", err)
			// Continue with zero counts
		}

		// Get route update metrics
		routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()

		// Get memory stats
		var memStats runtime.MemStats
		runtime.ReadMemStats(&memStats)

		// Get WHOIS stats if fetcher is available
		var whoisStats *WHOISStatsInfo
		if s.asnFetcher != nil {
			whoisStats = s.getWHOISStats(ctx)
		}

		stats := Stats{
			Uptime:                 uptime,
			TotalMessages:          metrics.TotalMessages,
			TotalBytes:             metrics.TotalBytes,
			TotalWireBytes:         metrics.TotalWireBytes,
			MessagesPerSec:         metrics.MessagesPerSec,
			MbitsPerSec:            metrics.BitsPerSec / bitsPerMegabit,
			WireMbitsPerSec:        metrics.WireBitsPerSec / bitsPerMegabit,
			Connected:              metrics.Connected,
			GoVersion:              runtime.Version(),
			Goroutines:             runtime.NumGoroutine(),
			MemoryUsage:            humanize.Bytes(memStats.Alloc),
			ASNs:                   dbStats.ASNs,
			Prefixes:               dbStats.Prefixes,
			IPv4Prefixes:           dbStats.IPv4Prefixes,
			IPv6Prefixes:           dbStats.IPv6Prefixes,
			Peerings:               dbStats.Peerings,
			Peers:                  dbStats.Peers,
			DatabaseSizeBytes:      dbStats.FileSizeBytes,
			LiveRoutes:             dbStats.LiveRoutes,
			IPv4Routes:             ipv4Routes,
			IPv6Routes:             ipv6Routes,
			OldestRoute:            dbStats.OldestRoute,
			NewestRoute:            dbStats.NewestRoute,
			IPv4UpdatesPerSec:      routeMetrics.IPv4UpdatesPerSec,
			IPv6UpdatesPerSec:      routeMetrics.IPv6UpdatesPerSec,
			IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
			IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
			WHOISStats:             whoisStats,
		}

		if err := writeJSONSuccess(w, stats); err != nil {
			s.logger.Error("Failed to encode stats", "error", err)
		}
	}
}

// getWHOISStats builds WHOIS statistics from database and fetcher.
func (s *Server) getWHOISStats(ctx context.Context) *WHOISStatsInfo {
	// Get database WHOIS stats
	dbStats, err := s.db.GetWHOISStats(ctx, whoisStaleThreshold)
	if err != nil {
		s.logger.Warn("Failed to get WHOIS stats", "error", err)

		return nil
	}

	// Get fetcher stats
	fetcherStats := s.asnFetcher.GetStats()

	// Calculate fresh percentage
	var freshPercent float64
	if dbStats.TotalASNs > 0 {
		freshPercent = float64(dbStats.FreshASNs) / float64(dbStats.TotalASNs) * percentMultiplier
	}

	return &WHOISStatsInfo{
		TotalASNs:         dbStats.TotalASNs,
		FreshASNs:         dbStats.FreshASNs,
		StaleASNs:         dbStats.StaleASNs,
		NeverFetched:      dbStats.NeverFetched,
		SuccessesLastHour: fetcherStats.SuccessesLastHour,
		ErrorsLastHour:    fetcherStats.ErrorsLastHour,
		CurrentInterval:   fetcherStats.CurrentInterval.String(),
		ConsecutiveFails:  fetcherStats.ConsecutiveFails,
		FreshPercent:      freshPercent,
	}
}

// whoisStaleThreshold matches the fetcher's threshold for consistency.
const whoisStaleThreshold = 30 * 24 * time.Hour

// percentMultiplier converts a ratio to a percentage.
const percentMultiplier = 100

// handleStats returns a handler that serves API v1 statistics including
// detailed handler queue statistics and performance metrics.
func (s *Server) handleStats() http.HandlerFunc {
	// HandlerStatsInfo represents handler statistics in the API response
	type HandlerStatsInfo struct {
		Name               string  `json:"name"`
		QueueLength        int     `json:"queue_length"`
		QueueCapacity      int     `json:"queue_capacity"`
		QueueHighWaterMark int     `json:"queue_high_water_mark"`
		ProcessedCount     uint64  `json:"processed_count"`
		DroppedCount       uint64  `json:"dropped_count"`
		AvgProcessTimeMs   float64 `json:"avg_process_time_ms"`
		MinProcessTimeMs   float64 `json:"min_process_time_ms"`
		MaxProcessTimeMs   float64 `json:"max_process_time_ms"`
	}

	// GCStats represents garbage collection statistics
	type GCStats struct {
		NumGC          uint32  `json:"num_gc"`
		TotalPauseMs   uint64  `json:"total_pause_ms"`
		LastPauseMs    float64 `json:"last_pause_ms"`
		HeapAllocBytes uint64  `json:"heap_alloc_bytes"`
		HeapSysBytes   uint64  `json:"heap_sys_bytes"`
	}

	// StreamStats represents stream statistics including announcements/withdrawals
	type StreamStats struct {
		Announcements    uint64  `json:"announcements"`
		Withdrawals      uint64  `json:"withdrawals"`
		RouteChurnPerSec float64 `json:"route_churn_per_sec"`
		BGPPeerCount     int     `json:"bgp_peer_count"`
	}

	// StatsResponse represents the API statistics response
	type StatsResponse struct {
		Uptime                 string                        `json:"uptime"`
		TotalMessages          uint64                        `json:"total_messages"`
		TotalBytes             uint64                        `json:"total_bytes"`
		TotalWireBytes         uint64                        `json:"total_wire_bytes"`
		MessagesPerSec         float64                       `json:"messages_per_sec"`
		MbitsPerSec            float64                       `json:"mbits_per_sec"`
		WireMbitsPerSec        float64                       `json:"wire_mbits_per_sec"`
		Connected              bool                          `json:"connected"`
		ConnectionDuration     string                        `json:"connection_duration"`
		ReconnectCount         uint64                        `json:"reconnect_count"`
		GoVersion              string                        `json:"go_version"`
		Goroutines             int                           `json:"goroutines"`
		MemoryUsage            string                        `json:"memory_usage"`
		GC                     GCStats                       `json:"gc"`
		Stream                 StreamStats                   `json:"stream"`
		ASNs                   int                           `json:"asns"`
		Prefixes               int                           `json:"prefixes"`
		IPv4Prefixes           int                           `json:"ipv4_prefixes"`
		IPv6Prefixes           int                           `json:"ipv6_prefixes"`
		Peerings               int                           `json:"peerings"`
		Peers                  int                           `json:"peers"`
		DatabaseSizeBytes      int64                         `json:"database_size_bytes"`
		LiveRoutes             int                           `json:"live_routes"`
		IPv4Routes             int                           `json:"ipv4_routes"`
		IPv6Routes             int                           `json:"ipv6_routes"`
		OldestRoute            *time.Time                    `json:"oldest_route,omitempty"`
		NewestRoute            *time.Time                    `json:"newest_route,omitempty"`
		IPv4UpdatesPerSec      float64                       `json:"ipv4_updates_per_sec"`
		IPv6UpdatesPerSec      float64                       `json:"ipv6_updates_per_sec"`
		HandlerStats           []HandlerStatsInfo            `json:"handler_stats"`
		IPv4PrefixDistribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
		IPv6PrefixDistribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
		WHOISStats             *WHOISStatsInfo               `json:"whois_stats,omitempty"`
	}

	return func(w http.ResponseWriter, r *http.Request) {
		// Create a 4 second timeout context for this request
		ctx, cancel := context.WithTimeout(r.Context(), statsContextTimeout)
		defer cancel()

		// Check if context is already cancelled
		select {
		case <-ctx.Done():
			http.Error(w, "Request timeout", http.StatusRequestTimeout)

			return
		default:
		}

		metrics := s.streamer.GetMetrics()

		// Get database stats with timeout
		statsChan := make(chan database.Stats)
		errChan := make(chan error)

		go func() {
			dbStats, err := s.db.GetStatsContext(ctx)
			if err != nil {
				s.logger.Debug("Database stats query failed", "error", err)
				errChan <- err

				return
			}
			statsChan <- dbStats
		}()

		var dbStats database.Stats
		select {
		case <-ctx.Done():
			s.logger.Error("Database stats timeout")
			// Don't write response here - timeout middleware already handles it
			return
		case err := <-errChan:
			s.logger.Error("Failed to get database stats", "error", err)
			writeJSONError(w, http.StatusInternalServerError, err.Error())

			return
		case dbStats = <-statsChan:
			// Success
		}

		uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
		if metrics.ConnectedSince.IsZero() {
			uptime = "0s"
		}

		const bitsPerMegabit = 1000000.0

		// Get route counts from database
		ipv4Routes, ipv6Routes, err := s.db.GetLiveRouteCountsContext(ctx)
		if err != nil {
			s.logger.Warn("Failed to get live route counts", "error", err)
			// Continue with zero counts
		}

		// Get route update metrics
		routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()

		// Get handler stats
		handlerStats := s.streamer.GetHandlerStats()
		handlerStatsInfo := make([]HandlerStatsInfo, 0, len(handlerStats))
		const microsecondsPerMillisecond = 1000.0
		for _, hs := range handlerStats {
			handlerStatsInfo = append(handlerStatsInfo, HandlerStatsInfo{
				Name:               hs.Name,
				QueueLength:        hs.QueueLength,
				QueueCapacity:      hs.QueueCapacity,
				QueueHighWaterMark: hs.QueueHighWaterMark,
				ProcessedCount:     hs.ProcessedCount,
				DroppedCount:       hs.DroppedCount,
				AvgProcessTimeMs:   float64(hs.AvgProcessTime.Microseconds()) / microsecondsPerMillisecond,
				MinProcessTimeMs:   float64(hs.MinProcessTime.Microseconds()) / microsecondsPerMillisecond,
				MaxProcessTimeMs:   float64(hs.MaxProcessTime.Microseconds()) / microsecondsPerMillisecond,
			})
		}

		// Get memory stats
		var memStats runtime.MemStats
		runtime.ReadMemStats(&memStats)

		// Get WHOIS stats if fetcher is available
		var whoisStats *WHOISStatsInfo
		if s.asnFetcher != nil {
			whoisStats = s.getWHOISStats(ctx)
		}

		// Calculate connection duration
		connectionDuration := "disconnected"
		if metrics.Connected && !metrics.ConnectedSince.IsZero() {
			connectionDuration = time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
		}

		// Get announcement/withdrawal stats from metrics tracker
		metricsTracker := s.streamer.GetMetricsTracker()
		announcements := metricsTracker.GetAnnouncementCount()
		withdrawals := metricsTracker.GetWithdrawalCount()
		churnRate := metricsTracker.GetChurnRate()
		bgpPeerCount := metricsTracker.GetBGPPeerCount()

		// Calculate last GC pause
		const (
			nanosecondsPerMillisecond = 1e6
			gcPauseHistorySize        = 256 // Size of runtime.MemStats.PauseNs circular buffer
		)
		var lastPauseMs float64
		if memStats.NumGC > 0 {
			// PauseNs is a circular buffer, get the most recent pause
			lastPauseIdx := (memStats.NumGC + gcPauseHistorySize - 1) % gcPauseHistorySize
			lastPauseMs = float64(memStats.PauseNs[lastPauseIdx]) / nanosecondsPerMillisecond
		}

		stats := StatsResponse{
			Uptime:             uptime,
			TotalMessages:      metrics.TotalMessages,
			TotalBytes:         metrics.TotalBytes,
			TotalWireBytes:     metrics.TotalWireBytes,
			MessagesPerSec:     metrics.MessagesPerSec,
			MbitsPerSec:        metrics.BitsPerSec / bitsPerMegabit,
			WireMbitsPerSec:    metrics.WireBitsPerSec / bitsPerMegabit,
			Connected:          metrics.Connected,
			ConnectionDuration: connectionDuration,
			ReconnectCount:     metrics.ReconnectCount,
			GoVersion:          runtime.Version(),
			Goroutines:         runtime.NumGoroutine(),
			MemoryUsage:        humanize.Bytes(memStats.Alloc),
			GC: GCStats{
				NumGC:          memStats.NumGC,
				TotalPauseMs:   memStats.PauseTotalNs / uint64(nanosecondsPerMillisecond),
				LastPauseMs:    lastPauseMs,
				HeapAllocBytes: memStats.HeapAlloc,
				HeapSysBytes:   memStats.HeapSys,
			},
			Stream: StreamStats{
				Announcements:    announcements,
				Withdrawals:      withdrawals,
				RouteChurnPerSec: churnRate,
				BGPPeerCount:     bgpPeerCount,
			},
			ASNs:                   dbStats.ASNs,
			Prefixes:               dbStats.Prefixes,
			IPv4Prefixes:           dbStats.IPv4Prefixes,
			IPv6Prefixes:           dbStats.IPv6Prefixes,
			Peerings:               dbStats.Peerings,
			Peers:                  dbStats.Peers,
			DatabaseSizeBytes:      dbStats.FileSizeBytes,
			LiveRoutes:             dbStats.LiveRoutes,
			IPv4Routes:             ipv4Routes,
			IPv6Routes:             ipv6Routes,
			OldestRoute:            dbStats.OldestRoute,
			NewestRoute:            dbStats.NewestRoute,
			IPv4UpdatesPerSec:      routeMetrics.IPv4UpdatesPerSec,
			IPv6UpdatesPerSec:      routeMetrics.IPv6UpdatesPerSec,
			HandlerStats:           handlerStatsInfo,
			IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
			IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
			WHOISStats:             whoisStats,
		}

		if err := writeJSONSuccess(w, stats); err != nil {
			s.logger.Error("Failed to encode stats", "error", err)
		}
	}
}

// handleStatusHTML returns a handler that serves the HTML status page,
// which displays real-time statistics fetched via JavaScript.
func (s *Server) handleStatusHTML() http.HandlerFunc {
	return func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")

		tmpl := templates.StatusTemplate()
		if err := tmpl.Execute(w, nil); err != nil {
			s.logger.Error("Failed to render template", "error", err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		}
	}
}

// handleIPLookup returns a handler that looks up AS information for an IP address
func (s *Server) handleIPLookup() http.HandlerFunc {
	return s.handleIPInfo()
}

// IPLookupResponse is the standard response for IP/hostname lookups.
type IPLookupResponse struct {
	Query   string             `json:"query"`
	Results []*database.IPInfo `json:"results"`
	Errors  []string           `json:"errors,omitempty"`
}

// handleIPInfo returns a handler that provides comprehensive IP information.
// Used for /ip, /ip/{addr}, and /api/v1/ip/{ip} endpoints.
// Accepts IP addresses (single or comma-separated) and hostnames.
// Always returns the same response structure with PTR records for each IP.
func (s *Server) handleIPInfo() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Get IP/hostname from URL param, falling back to client IP
		target := chi.URLParam(r, "ip")
		if target == "" {
			target = chi.URLParam(r, "addr")
		}
		if target == "" {
			// Use client IP (RealIP middleware has already processed this)
			target = extractClientIP(r)
		}

		if target == "" {
			writeJSONError(w, http.StatusBadRequest, "Could not determine IP address")

			return
		}

		ctx := r.Context()
		response := IPLookupResponse{
			Query:   target,
			Results: make([]*database.IPInfo, 0),
		}

		// Collect all IPs to look up
		var ipsToLookup []string

		// Check if target contains commas (multiple IPs)
		targets := strings.Split(target, ",")
		for _, t := range targets {
			t = strings.TrimSpace(t)
			if t == "" {
				continue
			}

			// Check if this target is an IP address
			if parsedIP := net.ParseIP(t); parsedIP != nil {
				ipsToLookup = append(ipsToLookup, t)
			} else {
				// It's a hostname - resolve it
				resolved, err := net.DefaultResolver.LookupHost(ctx, t)
				if err != nil {
					response.Errors = append(response.Errors, t+": "+err.Error())

					continue
				}
				ipsToLookup = append(ipsToLookup, resolved...)
			}
		}

		if len(ipsToLookup) == 0 {
			writeJSONError(w, http.StatusBadRequest, "No valid IPs or hostnames provided")

			return
		}

		// Track ASNs that need WHOIS refresh
		refreshASNs := make(map[int]bool)

		// Look up each IP
		for _, ip := range ipsToLookup {
			ipInfo, err := s.db.GetIPInfoContext(ctx, ip)
			if err != nil {
				response.Errors = append(response.Errors, ip+": "+err.Error())

				continue
			}

			// Do PTR lookup for this IP
			ptrs, err := net.DefaultResolver.LookupAddr(ctx, ip)
			if err == nil && len(ptrs) > 0 {
				// Remove trailing dots from PTR records
				for i, ptr := range ptrs {
					ptrs[i] = strings.TrimSuffix(ptr, ".")
				}
				ipInfo.PTR = ptrs
			}

			response.Results = append(response.Results, ipInfo)

			if ipInfo.NeedsWHOISRefresh {
				refreshASNs[ipInfo.ASN] = true
			}
		}

		// Queue WHOIS refresh for stale ASNs (non-blocking)
		if s.asnFetcher != nil {
			for asn := range refreshASNs {
				s.asnFetcher.QueueImmediate(asn)
			}
		}

		// Return response (even if no results, include errors)
		if len(response.Results) == 0 && len(response.Errors) > 0 {
			writeJSONError(w, http.StatusNotFound, "No routes found: "+response.Errors[0])

			return
		}

		if err := writeJSONSuccess(w, response); err != nil {
			s.logger.Error("Failed to encode IP lookup response", "error", err)
		}
	}
}

// extractClientIP extracts the client IP from the request.
// Works with chi's RealIP middleware which sets RemoteAddr.
func extractClientIP(r *http.Request) string {
	// RemoteAddr is in the form "IP:port" or just "IP" for unix sockets
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		// Might be just an IP without port
		return r.RemoteAddr
	}

	return host
}

// handleASDetailJSON returns AS details as JSON
func (s *Server) handleASDetailJSON() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		asnStr := chi.URLParam(r, "asn")
		asn, err := strconv.Atoi(asnStr)
		if err != nil {
			writeJSONError(w, http.StatusBadRequest, "Invalid ASN")

			return
		}

		asInfo, prefixes, err := s.db.GetASDetailsContext(r.Context(), asn)
		if err != nil {
			if errors.Is(err, database.ErrNoRoute) {
				writeJSONError(w, http.StatusNotFound, err.Error())
			} else {
				writeJSONError(w, http.StatusInternalServerError, err.Error())
			}

			return
		}

		// Group prefixes by IP version
		const ipVersionV4 = 4
		var ipv4Prefixes, ipv6Prefixes []database.LiveRoute
		for _, p := range prefixes {
			if p.IPVersion == ipVersionV4 {
				ipv4Prefixes = append(ipv4Prefixes, p)
			} else {
				ipv6Prefixes = append(ipv6Prefixes, p)
			}
		}

		response := map[string]interface{}{
			"asn":           asInfo,
			"ipv4_prefixes": ipv4Prefixes,
			"ipv6_prefixes": ipv6Prefixes,
			"total_count":   len(prefixes),
		}

		if err := writeJSONSuccess(w, response); err != nil {
			s.logger.Error("Failed to encode AS details", "error", err)
		}
	}
}

// handlePrefixDetailJSON returns prefix details as JSON
func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Get prefix and length from URL params
		prefixParam := chi.URLParam(r, "prefix")
		lenParam := chi.URLParam(r, "len")

		if prefixParam == "" || lenParam == "" {
			writeJSONError(w, http.StatusBadRequest, "Prefix and length parameters are required")

			return
		}

		// Combine prefix and length into CIDR notation
		prefix := prefixParam + "/" + lenParam

		routes, err := s.db.GetPrefixDetailsContext(r.Context(), prefix)
		if err != nil {
			if errors.Is(err, database.ErrNoRoute) {
				writeJSONError(w, http.StatusNotFound, err.Error())
			} else {
				writeJSONError(w, http.StatusInternalServerError, err.Error())
			}

			return
		}

		// Group by origin AS
		originMap := make(map[int][]database.LiveRoute)
		for _, route := range routes {
			originMap[route.OriginASN] = append(originMap[route.OriginASN], route)
		}

		response := map[string]interface{}{
			"prefix":       prefix,
			"routes":       routes,
			"origins":      originMap,
			"peer_count":   len(routes),
			"origin_count": len(originMap),
		}

		if err := writeJSONSuccess(w, response); err != nil {
			s.logger.Error("Failed to encode prefix details", "error", err)
		}
	}
}

// handleASDetail returns a handler that serves the AS detail HTML page
func (s *Server) handleASDetail() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		asnStr := chi.URLParam(r, "asn")
		asn, err := strconv.Atoi(asnStr)
		if err != nil {
			http.Error(w, "Invalid ASN", http.StatusBadRequest)

			return
		}

		asInfo, prefixes, err := s.db.GetASDetailsContext(r.Context(), asn)
		if err != nil {
			if errors.Is(err, database.ErrNoRoute) {
				http.Error(w, "AS not found", http.StatusNotFound)
			} else {
				s.logger.Error("Failed to get AS details", "error", err)
				http.Error(w, "Internal server error", http.StatusInternalServerError)
			}

			return
		}

		// Get peers
		peers, err := s.db.GetASPeersContext(r.Context(), asn)
		if err != nil {
			s.logger.Error("Failed to get AS peers", "error", err)
			// Continue without peers rather than failing the whole request
			peers = []database.ASPeer{}
		}

		// Group prefixes by IP version
		const ipVersionV4 = 4
		var ipv4Prefixes, ipv6Prefixes []database.LiveRoute
		for _, p := range prefixes {
			if p.IPVersion == ipVersionV4 {
				ipv4Prefixes = append(ipv4Prefixes, p)
			} else {
				ipv6Prefixes = append(ipv6Prefixes, p)
			}
		}

		// Sort prefixes by network address
		sort.Slice(ipv4Prefixes, func(i, j int) bool {
			// Parse the prefixes to compare network addresses
			ipI, netI, _ := net.ParseCIDR(ipv4Prefixes[i].Prefix)
			ipJ, netJ, _ := net.ParseCIDR(ipv4Prefixes[j].Prefix)

			// Compare by network address first
			cmp := bytes.Compare(ipI.To4(), ipJ.To4())
			if cmp != 0 {
				return cmp < 0
			}

			// If network addresses are equal, compare by mask length
			onesI, _ := netI.Mask.Size()
			onesJ, _ := netJ.Mask.Size()

			return onesI < onesJ
		})

		sort.Slice(ipv6Prefixes, func(i, j int) bool {
			// Parse the prefixes to compare network addresses
			ipI, netI, _ := net.ParseCIDR(ipv6Prefixes[i].Prefix)
			ipJ, netJ, _ := net.ParseCIDR(ipv6Prefixes[j].Prefix)

			// Compare by network address first
			cmp := bytes.Compare(ipI.To16(), ipJ.To16())
			if cmp != 0 {
				return cmp < 0
			}

			// If network addresses are equal, compare by mask length
			onesI, _ := netI.Mask.Size()
			onesJ, _ := netJ.Mask.Size()

			return onesI < onesJ
		})

		// Prepare template data
		data := struct {
			ASN          *database.ASN
			IPv4Prefixes []database.LiveRoute
			IPv6Prefixes []database.LiveRoute
			TotalCount   int
			IPv4Count    int
			IPv6Count    int
			Peers        []database.ASPeer
			PeerCount    int
		}{
			ASN:          asInfo,
			IPv4Prefixes: ipv4Prefixes,
			IPv6Prefixes: ipv6Prefixes,
			TotalCount:   len(prefixes),
			IPv4Count:    len(ipv4Prefixes),
			IPv6Count:    len(ipv6Prefixes),
			Peers:        peers,
			PeerCount:    len(peers),
		}

		// Check if context is still valid before writing response
		select {
		case <-r.Context().Done():
			// Request was cancelled, don't write response
			return
		default:
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		tmpl := templates.ASDetailTemplate()
		if err := tmpl.Execute(w, data); err != nil {
			s.logger.Error("Failed to render AS detail template", "error", err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		}
	}
}

// handlePrefixDetail returns a handler that serves the prefix detail HTML page
func (s *Server) handlePrefixDetail() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Get prefix and length from URL params
		prefixParam := chi.URLParam(r, "prefix")
		lenParam := chi.URLParam(r, "len")

		if prefixParam == "" || lenParam == "" {
			http.Error(w, "Prefix and length parameters are required", http.StatusBadRequest)

			return
		}

		// Combine prefix and length into CIDR notation
		prefix := prefixParam + "/" + lenParam

		routes, err := s.db.GetPrefixDetailsContext(r.Context(), prefix)
		if err != nil {
			if errors.Is(err, database.ErrNoRoute) {
				http.Error(w, "Prefix not found", http.StatusNotFound)
			} else {
				s.logger.Error("Failed to get prefix details", "error", err)
				http.Error(w, "Internal server error", http.StatusInternalServerError)
			}

			return
		}

		// Group by origin AS and collect unique AS info
		type ASNInfo struct {
			ASN         int
			Handle      string
			Description string
			PeerCount   int
		}
		originMap := make(map[int]*ASNInfo)
		for _, route := range routes {
			if _, exists := originMap[route.OriginASN]; !exists {
				// Get AS info from database
				asInfo, _, _ := s.db.GetASDetailsContext(r.Context(), route.OriginASN)
				handle := ""
				description := ""
				if asInfo != nil {
					handle = asInfo.Handle
					description = asInfo.Description
				}
				originMap[route.OriginASN] = &ASNInfo{
					ASN:         route.OriginASN,
					Handle:      handle,
					Description: description,
					PeerCount:   0,
				}
			}
			originMap[route.OriginASN].PeerCount++
		}

		// Get the first route to extract some common info
		var maskLength, ipVersion int
		if len(routes) > 0 {
			// Parse CIDR to get mask length and IP version
			_, ipNet, err := net.ParseCIDR(prefix)
			if err == nil {
				ones, _ := ipNet.Mask.Size()
				maskLength = ones
				if ipNet.IP.To4() != nil {
					ipVersion = 4
				} else {
					ipVersion = 6
				}
			}
		}

		// Convert origin map to sorted slice
		var origins []*ASNInfo
		for _, origin := range originMap {
			origins = append(origins, origin)
		}

		// Create enhanced routes with AS path handles
		type ASPathEntry struct {
			ASN    int
			Handle string
		}
		type EnhancedRoute struct {
			database.LiveRoute
			ASPathWithHandle []ASPathEntry
		}

		enhancedRoutes := make([]EnhancedRoute, len(routes))
		for i, route := range routes {
			enhancedRoute := EnhancedRoute{
				LiveRoute:        route,
				ASPathWithHandle: make([]ASPathEntry, len(route.ASPath)),
			}

			// Look up handle for each AS in the path
			for j, asn := range route.ASPath {
				handle := asinfo.GetHandle(asn)
				enhancedRoute.ASPathWithHandle[j] = ASPathEntry{
					ASN:    asn,
					Handle: handle,
				}
			}

			enhancedRoutes[i] = enhancedRoute
		}

		// Prepare template data
		data := struct {
			Prefix      string
			MaskLength  int
			IPVersion   int
			Routes      []EnhancedRoute
			Origins     []*ASNInfo
			PeerCount   int
			OriginCount int
		}{
			Prefix:      prefix,
			MaskLength:  maskLength,
			IPVersion:   ipVersion,
			Routes:      enhancedRoutes,
			Origins:     origins,
			PeerCount:   len(routes),
			OriginCount: len(originMap),
		}

		// Check if context is still valid before writing response
		select {
		case <-r.Context().Done():
			// Request was cancelled, don't write response
			return
		default:
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		tmpl := templates.PrefixDetailTemplate()
		if err := tmpl.Execute(w, data); err != nil {
			s.logger.Error("Failed to render prefix detail template", "error", err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		}
	}
}

// handlePrefixLength shows a random sample of IPv4 prefixes with the specified mask length
func (s *Server) handlePrefixLength() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		lengthStr := chi.URLParam(r, "length")
		if lengthStr == "" {
			http.Error(w, "Length parameter is required", http.StatusBadRequest)

			return
		}

		maskLength, err := strconv.Atoi(lengthStr)
		if err != nil {
			http.Error(w, "Invalid mask length", http.StatusBadRequest)

			return
		}

		// Validate IPv4 mask length
		const maxIPv4MaskLength = 32
		if maskLength < 0 || maskLength > maxIPv4MaskLength {
			http.Error(w, "Invalid IPv4 mask length", http.StatusBadRequest)

			return
		}

		const ipVersion = 4

		// Get random sample of prefixes
		const maxPrefixes = 500
		prefixes, err := s.db.GetRandomPrefixesByLengthContext(r.Context(), maskLength, ipVersion, maxPrefixes)
		if err != nil {
			s.logger.Error("Failed to get prefixes by length", "error", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)

			return
		}

		// Sort prefixes for display
		sort.Slice(prefixes, func(i, j int) bool {
			// First compare by IP version
			if prefixes[i].IPVersion != prefixes[j].IPVersion {
				return prefixes[i].IPVersion < prefixes[j].IPVersion
			}
			// Then by prefix
			return prefixes[i].Prefix < prefixes[j].Prefix
		})

		// Create enhanced prefixes with AS descriptions
		type EnhancedPrefix struct {
			database.LiveRoute
			OriginASDescription string
			Age                 string
		}

		enhancedPrefixes := make([]EnhancedPrefix, len(prefixes))
		for i, prefix := range prefixes {
			enhancedPrefixes[i] = EnhancedPrefix{
				LiveRoute: prefix,
				Age:       formatAge(prefix.LastUpdated),
			}

			// Get AS description
			if asInfo, ok := asinfo.Get(prefix.OriginASN); ok {
				enhancedPrefixes[i].OriginASDescription = asInfo.Description
			}
		}

		// Render template
		data := map[string]interface{}{
			"MaskLength": maskLength,
			"IPVersion":  ipVersion,
			"Prefixes":   enhancedPrefixes,
			"Count":      len(prefixes),
		}

		// Check if context is still valid before writing response
		select {
		case <-r.Context().Done():
			// Request was cancelled, don't write response
			return
		default:
		}

		tmpl := templates.PrefixLengthTemplate()
		if err := tmpl.Execute(w, data); err != nil {
			s.logger.Error("Failed to render prefix length template", "error", err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		}
	}
}

// handlePrefixLength6 shows a random sample of IPv6 prefixes with the specified mask length
func (s *Server) handlePrefixLength6() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		lengthStr := chi.URLParam(r, "length")
		if lengthStr == "" {
			http.Error(w, "Length parameter is required", http.StatusBadRequest)

			return
		}

		maskLength, err := strconv.Atoi(lengthStr)
		if err != nil {
			http.Error(w, "Invalid mask length", http.StatusBadRequest)

			return
		}

		// Validate IPv6 mask length
		const maxIPv6MaskLength = 128
		if maskLength < 0 || maskLength > maxIPv6MaskLength {
			http.Error(w, "Invalid IPv6 mask length", http.StatusBadRequest)

			return
		}

		const ipVersion = 6

		// Get random sample of prefixes
		const maxPrefixes = 500
		prefixes, err := s.db.GetRandomPrefixesByLengthContext(r.Context(), maskLength, ipVersion, maxPrefixes)
		if err != nil {
			s.logger.Error("Failed to get prefixes by length", "error", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)

			return
		}

		// Sort prefixes for display
		sort.Slice(prefixes, func(i, j int) bool {
			// First compare by IP version
			if prefixes[i].IPVersion != prefixes[j].IPVersion {
				return prefixes[i].IPVersion < prefixes[j].IPVersion
			}
			// Then by prefix
			return prefixes[i].Prefix < prefixes[j].Prefix
		})

		// Create enhanced prefixes with AS descriptions
		type EnhancedPrefix struct {
			database.LiveRoute
			OriginASDescription string
			Age                 string
		}

		enhancedPrefixes := make([]EnhancedPrefix, len(prefixes))
		for i, prefix := range prefixes {
			enhancedPrefixes[i] = EnhancedPrefix{
				LiveRoute: prefix,
				Age:       formatAge(prefix.LastUpdated),
			}

			// Get AS description
			if asInfo, ok := asinfo.Get(prefix.OriginASN); ok {
				enhancedPrefixes[i].OriginASDescription = asInfo.Description
			}
		}

		// Render template
		data := map[string]interface{}{
			"MaskLength": maskLength,
			"IPVersion":  ipVersion,
			"Prefixes":   enhancedPrefixes,
			"Count":      len(prefixes),
		}

		// Check if context is still valid before writing response
		select {
		case <-r.Context().Done():
			// Request was cancelled, don't write response
			return
		default:
		}

		tmpl := templates.PrefixLengthTemplate()
		if err := tmpl.Execute(w, data); err != nil {
			s.logger.Error("Failed to render prefix length template", "error", err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		}
	}
}

// formatAge returns a human-readable age string
func formatAge(timestamp time.Time) string {
	age := time.Since(timestamp)

	const hoursPerDay = 24

	if age < time.Minute {
		return "< 1m"
	} else if age < time.Hour {
		minutes := int(age.Minutes())

		return strconv.Itoa(minutes) + "m"
	} else if age < hoursPerDay*time.Hour {
		hours := int(age.Hours())

		return strconv.Itoa(hours) + "h"
	}

	days := int(age.Hours() / hoursPerDay)

	return strconv.Itoa(days) + "d"
}
