package hscontrol

import (
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"net/netip"
	"strings"

	"github.com/arl/statsviz"
	"github.com/juanfont/headscale/hscontrol/types"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"tailscale.com/tsweb"
)

// protectedDebugHandler wraps an http.Handler with an access check that
// allows requests from loopback, Tailscale CGNAT IPs, and private
// (RFC 1918 / RFC 4193) addresses. This extends tsweb.Protected which
// only allows loopback and Tailscale IPs.
func protectedDebugHandler(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if tsweb.AllowDebugAccess(r) {
			h.ServeHTTP(w, r)

			return
		}

		// tsweb.AllowDebugAccess rejects X-Forwarded-For and non-TS IPs.
		// Additionally allow private/LAN addresses so operators can reach
		// debug endpoints from their local network without tailscaled.
		ipStr, _, err := net.SplitHostPort(r.RemoteAddr)
		if err == nil {
			ip, parseErr := netip.ParseAddr(ipStr)
			if parseErr == nil && ip.IsPrivate() {
				h.ServeHTTP(w, r)

				return
			}
		}

		http.Error(w, "debug access denied", http.StatusForbidden)
	})
}

func (h *Headscale) debugHTTPServer() *http.Server {
	debugMux := http.NewServeMux()
	debug := tsweb.Debugger(debugMux)

	// State overview endpoint
	debug.Handle("overview", "State overview", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check Accept header to determine response format
		acceptHeader := r.Header.Get("Accept")
		wantsJSON := strings.Contains(acceptHeader, "application/json")

		if wantsJSON {
			overview := h.state.DebugOverviewJSON()

			overviewJSON, err := json.MarshalIndent(overview, "", "  ")
			if err != nil {
				httpError(w, err)
				return
			}

			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write(overviewJSON)
		} else {
			// Default to text/plain for backward compatibility
			overview := h.state.DebugOverview()

			w.Header().Set("Content-Type", "text/plain")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte(overview))
		}
	}))

	// Configuration endpoint
	debug.Handle("config", "Current configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		config := h.state.DebugConfig()

		configJSON, err := json.MarshalIndent(config, "", "  ")
		if err != nil {
			httpError(w, err)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(configJSON)
	}))

	// Policy endpoint
	debug.Handle("policy", "Current policy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		policy, err := h.state.DebugPolicy()
		if err != nil {
			httpError(w, err)
			return
		}
		// Policy data is HuJSON, which is a superset of JSON
		// Set content type based on Accept header preference
		acceptHeader := r.Header.Get("Accept")
		if strings.Contains(acceptHeader, "application/json") {
			w.Header().Set("Content-Type", "application/json")
		} else {
			w.Header().Set("Content-Type", "text/plain")
		}

		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(policy))
	}))

	// Filter rules endpoint
	debug.Handle("filter", "Current filter rules", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		filter, err := h.state.DebugFilter()
		if err != nil {
			httpError(w, err)
			return
		}

		filterJSON, err := json.MarshalIndent(filter, "", "  ")
		if err != nil {
			httpError(w, err)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(filterJSON)
	}))

	// SSH policies endpoint
	debug.Handle("ssh", "SSH policies per node", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		sshPolicies := h.state.DebugSSHPolicies()

		sshJSON, err := json.MarshalIndent(sshPolicies, "", "  ")
		if err != nil {
			httpError(w, err)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(sshJSON)
	}))

	// DERP map endpoint
	debug.Handle("derp", "DERP map configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check Accept header to determine response format
		acceptHeader := r.Header.Get("Accept")
		wantsJSON := strings.Contains(acceptHeader, "application/json")

		if wantsJSON {
			derpInfo := h.state.DebugDERPJSON()

			derpJSON, err := json.MarshalIndent(derpInfo, "", "  ")
			if err != nil {
				httpError(w, err)
				return
			}

			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write(derpJSON)
		} else {
			// Default to text/plain for backward compatibility
			derpInfo := h.state.DebugDERPMap()

			w.Header().Set("Content-Type", "text/plain")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte(derpInfo))
		}
	}))

	// NodeStore endpoint
	debug.Handle("nodestore", "NodeStore information", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check Accept header to determine response format
		acceptHeader := r.Header.Get("Accept")
		wantsJSON := strings.Contains(acceptHeader, "application/json")

		if wantsJSON {
			nodeStoreNodes := h.state.DebugNodeStoreJSON()

			nodeStoreJSON, err := json.MarshalIndent(nodeStoreNodes, "", "  ")
			if err != nil {
				httpError(w, err)
				return
			}

			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write(nodeStoreJSON)
		} else {
			// Default to text/plain for backward compatibility
			nodeStoreInfo := h.state.DebugNodeStore()

			w.Header().Set("Content-Type", "text/plain")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte(nodeStoreInfo))
		}
	}))

	// Registration cache endpoint
	debug.Handle("registration-cache", "Registration cache information", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		cacheInfo := h.state.DebugRegistrationCache()

		cacheJSON, err := json.MarshalIndent(cacheInfo, "", "  ")
		if err != nil {
			httpError(w, err)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(cacheJSON)
	}))

	// Routes endpoint
	debug.Handle("routes", "Primary routes", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check Accept header to determine response format
		acceptHeader := r.Header.Get("Accept")
		wantsJSON := strings.Contains(acceptHeader, "application/json")

		if wantsJSON {
			routes := h.state.DebugRoutes()

			routesJSON, err := json.MarshalIndent(routes, "", "  ")
			if err != nil {
				httpError(w, err)
				return
			}

			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write(routesJSON)
		} else {
			// Default to text/plain for backward compatibility
			routes := h.state.DebugRoutesString()

			w.Header().Set("Content-Type", "text/plain")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte(routes))
		}
	}))

	// Policy manager endpoint
	debug.Handle("policy-manager", "Policy manager state", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check Accept header to determine response format
		acceptHeader := r.Header.Get("Accept")
		wantsJSON := strings.Contains(acceptHeader, "application/json")

		if wantsJSON {
			policyManagerInfo := h.state.DebugPolicyManagerJSON()

			policyManagerJSON, err := json.MarshalIndent(policyManagerInfo, "", "  ")
			if err != nil {
				httpError(w, err)
				return
			}

			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write(policyManagerJSON)
		} else {
			// Default to text/plain for backward compatibility
			policyManagerInfo := h.state.DebugPolicyManager()

			w.Header().Set("Content-Type", "text/plain")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte(policyManagerInfo))
		}
	}))

	debug.Handle("mapresponses", "Map responses for all nodes", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		res, err := h.mapBatcher.DebugMapResponses()
		if err != nil {
			httpError(w, err)
			return
		}

		if res == nil {
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte("HEADSCALE_DEBUG_DUMP_MAPRESPONSE_PATH not set"))

			return
		}

		resJSON, err := json.MarshalIndent(res, "", "  ")
		if err != nil {
			httpError(w, err)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(resJSON)
	}))

	// Batcher endpoint
	debug.Handle("batcher", "Batcher connected nodes", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check Accept header to determine response format
		acceptHeader := r.Header.Get("Accept")
		wantsJSON := strings.Contains(acceptHeader, "application/json")

		if wantsJSON {
			batcherInfo := h.debugBatcherJSON()

			batcherJSON, err := json.MarshalIndent(batcherInfo, "", "  ")
			if err != nil {
				httpError(w, err)
				return
			}

			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write(batcherJSON)
		} else {
			// Default to text/plain for backward compatibility
			batcherInfo := h.debugBatcher()

			w.Header().Set("Content-Type", "text/plain")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte(batcherInfo))
		}
	}))

	// statsviz.Register would mount handlers directly on the raw mux,
	// bypassing the access gate. Build the server by hand and wrap
	// each handler with protectedDebugHandler.
	statsvizSrv, err := statsviz.NewServer()
	if err == nil {
		debugMux.Handle("/debug/statsviz/", protectedDebugHandler(statsvizSrv.Index()))
		debugMux.Handle("/debug/statsviz/ws", protectedDebugHandler(statsvizSrv.Ws()))
		debug.URL("/debug/statsviz", "Statsviz (visualise go metrics)")
	}

	debug.URL("/metrics", "Prometheus metrics")
	debugMux.Handle("/metrics", promhttp.Handler())

	debugHTTPServer := &http.Server{
		Addr:         h.cfg.MetricsAddr,
		Handler:      debugMux,
		ReadTimeout:  types.HTTPTimeout,
		WriteTimeout: 0,
	}

	return debugHTTPServer
}

// debugBatcher returns debug information about the batcher's connected nodes.
func (h *Headscale) debugBatcher() string {
	var sb strings.Builder
	sb.WriteString("=== Batcher Connected Nodes ===\n\n")

	totalNodes := 0
	connectedCount := 0

	// Collect nodes and sort them by ID
	type nodeStatus struct {
		id                types.NodeID
		connected         bool
		activeConnections int
	}

	var nodes []nodeStatus

	debugInfo := h.mapBatcher.Debug()
	for nodeID, info := range debugInfo {
		nodes = append(nodes, nodeStatus{
			id:                nodeID,
			connected:         info.Connected,
			activeConnections: info.ActiveConnections,
		})
		totalNodes++

		if info.Connected {
			connectedCount++
		}
	}

	// Sort by node ID
	for i := 0; i < len(nodes); i++ {
		for j := i + 1; j < len(nodes); j++ {
			if nodes[i].id > nodes[j].id {
				nodes[i], nodes[j] = nodes[j], nodes[i]
			}
		}
	}

	// Output sorted nodes
	for _, node := range nodes {
		status := "disconnected"
		if node.connected {
			status = "connected"
		}

		if node.activeConnections > 0 {
			sb.WriteString(fmt.Sprintf("Node %d:\t%s (%d connections)\n", node.id, status, node.activeConnections))
		} else {
			sb.WriteString(fmt.Sprintf("Node %d:\t%s\n", node.id, status))
		}
	}

	sb.WriteString(fmt.Sprintf("\nSummary: %d connected, %d total\n", connectedCount, totalNodes))

	return sb.String()
}

// DebugBatcherInfo represents batcher connection information in a structured format.
type DebugBatcherInfo struct {
	ConnectedNodes map[string]DebugBatcherNodeInfo `json:"connected_nodes"` // NodeID -> node connection info
	TotalNodes     int                             `json:"total_nodes"`
}

// DebugBatcherNodeInfo represents connection information for a single node.
type DebugBatcherNodeInfo struct {
	Connected         bool `json:"connected"`
	ActiveConnections int  `json:"active_connections"`
}

// debugBatcherJSON returns structured debug information about the batcher's connected nodes.
func (h *Headscale) debugBatcherJSON() DebugBatcherInfo {
	info := DebugBatcherInfo{
		ConnectedNodes: make(map[string]DebugBatcherNodeInfo),
		TotalNodes:     0,
	}

	debugInfo := h.mapBatcher.Debug()
	for nodeID, debugData := range debugInfo {
		info.ConnectedNodes[fmt.Sprintf("%d", nodeID)] = DebugBatcherNodeInfo{
			Connected:         debugData.Connected,
			ActiveConnections: debugData.ActiveConnections,
		}
		info.TotalNodes++
	}

	return info
}
