package server

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net"
	"net/http"
	"net/url"
	"runtime"
	"sort"
	"strconv"
	"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"
)

// handleRoot returns a handler that redirects to /status
func (s *Server) handleRoot() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/status", http.StatusSeeOther)
	}
}

// writeJSONError writes a standardized JSON error response
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
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,
	})
}

// handleStatusJSON returns a handler that serves JSON statistics
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"`
		MessagesPerSec         float64                       `json:"messages_per_sec"`
		MbitsPerSec            float64                       `json:"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"`
		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"`
	}

	return func(w http.ResponseWriter, r *http.Request) {
		// Create a 1 second timeout context for this request
		ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
		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)

		stats := Stats{
			Uptime:                 uptime,
			TotalMessages:          metrics.TotalMessages,
			TotalBytes:             metrics.TotalBytes,
			MessagesPerSec:         metrics.MessagesPerSec,
			MbitsPerSec:            metrics.BitsPerSec / 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,
			IPv4UpdatesPerSec:      routeMetrics.IPv4UpdatesPerSec,
			IPv6UpdatesPerSec:      routeMetrics.IPv6UpdatesPerSec,
			IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
			IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
		}

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

// handleStats returns a handler that serves API v1 statistics
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"`
		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"`
	}

	// StatsResponse represents the API statistics response
	type StatsResponse struct {
		Uptime                 string                        `json:"uptime"`
		TotalMessages          uint64                        `json:"total_messages"`
		TotalBytes             uint64                        `json:"total_bytes"`
		MessagesPerSec         float64                       `json:"messages_per_sec"`
		MbitsPerSec            float64                       `json:"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"`
		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"`
	}

	return func(w http.ResponseWriter, r *http.Request) {
		// Create a 1 second timeout context for this request
		ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
		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,
				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)

		stats := StatsResponse{
			Uptime:                 uptime,
			TotalMessages:          metrics.TotalMessages,
			TotalBytes:             metrics.TotalBytes,
			MessagesPerSec:         metrics.MessagesPerSec,
			MbitsPerSec:            metrics.BitsPerSec / 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,
			IPv4UpdatesPerSec:      routeMetrics.IPv4UpdatesPerSec,
			IPv6UpdatesPerSec:      routeMetrics.IPv6UpdatesPerSec,
			HandlerStats:           handlerStatsInfo,
			IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
			IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
		}

		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
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 func(w http.ResponseWriter, r *http.Request) {
		ip := chi.URLParam(r, "ip")
		if ip == "" {
			writeJSONError(w, http.StatusBadRequest, "IP parameter is required")

			return
		}

		// Look up AS information for the IP
		asInfo, err := s.db.GetASInfoForIPContext(r.Context(), ip)
		if err != nil {
			// Check if it's an invalid IP error
			if errors.Is(err, database.ErrInvalidIP) {
				writeJSONError(w, http.StatusBadRequest, err.Error())
			} else {
				// All other errors (including ErrNoRoute) are 404
				writeJSONError(w, http.StatusNotFound, err.Error())
			}

			return
		}

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

// 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) {
		prefixParam := chi.URLParam(r, "prefix")
		if prefixParam == "" {
			writeJSONError(w, http.StatusBadRequest, "Prefix parameter is required")

			return
		}

		// URL decode the prefix parameter
		prefix, err := url.QueryUnescape(prefixParam)
		if err != nil {
			writeJSONError(w, http.StatusBadRequest, "Invalid prefix parameter")

			return
		}

		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
		}

		// 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
		}{
			ASN:          asInfo,
			IPv4Prefixes: ipv4Prefixes,
			IPv6Prefixes: ipv6Prefixes,
			TotalCount:   len(prefixes),
			IPv4Count:    len(ipv4Prefixes),
			IPv6Count:    len(ipv6Prefixes),
		}

		// 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) {
		prefixParam := chi.URLParam(r, "prefix")
		if prefixParam == "" {
			http.Error(w, "Prefix parameter is required", http.StatusBadRequest)

			return
		}

		// URL decode the prefix parameter
		prefix, err := url.QueryUnescape(prefixParam)
		if err != nil {
			http.Error(w, "Invalid prefix parameter", http.StatusBadRequest)

			return
		}

		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 {
			Number      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{
					Number:      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 {
			Number 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{
					Number: 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)
		}
	}
}

// handleIPRedirect looks up the prefix containing the IP and redirects to its detail page
func (s *Server) handleIPRedirect() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ip := chi.URLParam(r, "ip")
		if ip == "" {
			http.Error(w, "IP parameter is required", http.StatusBadRequest)

			return
		}

		// Look up AS information for the IP (which includes the prefix)
		asInfo, err := s.db.GetASInfoForIP(ip)
		if err != nil {
			if errors.Is(err, database.ErrInvalidIP) {
				http.Error(w, "Invalid IP address", http.StatusBadRequest)
			} else if errors.Is(err, database.ErrNoRoute) {
				http.Error(w, "No route found for this IP", http.StatusNotFound)
			} else {
				s.logger.Error("Failed to look up IP", "error", err)
				http.Error(w, "Internal server error", http.StatusInternalServerError)
			}

			return
		}

		// Redirect to the prefix detail page (URL encode the prefix)
		http.Redirect(w, r, "/prefix/"+url.QueryEscape(asInfo.Prefix), http.StatusSeeOther)
	}
}

// handlePrefixLength shows a random sample of 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
		}

		// Determine IP version based on mask length
		const (
			maxIPv4MaskLength = 32
			maxIPv6MaskLength = 128
		)
		var ipVersion int
		if maskLength <= maxIPv4MaskLength {
			ipVersion = 4
		} else if maskLength <= maxIPv6MaskLength {
			ipVersion = 6
		} else {
			http.Error(w, "Invalid mask length", http.StatusBadRequest)

			return
		}

		// 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"
}
