// Package debug provides HTTP debug endpoints and CLI client for the proxy server.
package debug

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// StatusFilters contains filter options for status queries.
type StatusFilters struct {
	IPs            []string
	Names          []string
	Status         string
	ConnectionType string
}

// Client provides CLI access to debug endpoints.
type Client struct {
	baseURL    string
	jsonOutput bool
	httpClient *http.Client
	out        io.Writer
}

// NewClient creates a new debug client.
func NewClient(baseURL string, jsonOutput bool, out io.Writer) *Client {
	if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
		baseURL = "http://" + baseURL
	}
	baseURL = strings.TrimSuffix(baseURL, "/")

	return &Client{
		baseURL:    baseURL,
		jsonOutput: jsonOutput,
		out:        out,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// Health fetches the health status.
func (c *Client) Health(ctx context.Context) error {
	return c.fetchAndPrint(ctx, "/debug/health", c.printHealth)
}

func (c *Client) printHealth(data map[string]any) {
	_, _ = fmt.Fprintf(c.out, "Status: %v\n", data["status"])
	_, _ = fmt.Fprintf(c.out, "Uptime: %v\n", data["uptime"])
	_, _ = fmt.Fprintf(c.out, "Management Connected: %s\n", boolIcon(data["management_connected"]))
	_, _ = fmt.Fprintf(c.out, "All Clients Healthy:  %s\n", boolIcon(data["all_clients_healthy"]))

	total, _ := data["certs_total"].(float64)
	ready, _ := data["certs_ready"].(float64)
	pending, _ := data["certs_pending"].(float64)
	failed, _ := data["certs_failed"].(float64)
	if total > 0 {
		_, _ = fmt.Fprintf(c.out, "Certificates:         %d ready, %d pending, %d failed (%d total)\n",
			int(ready), int(pending), int(failed), int(total))
	}
	if domains, ok := data["certs_ready_domains"].([]any); ok && len(domains) > 0 {
		_, _ = fmt.Fprintf(c.out, "  Ready:\n")
		for _, d := range domains {
			_, _ = fmt.Fprintf(c.out, "    %v\n", d)
		}
	}
	if domains, ok := data["certs_pending_domains"].([]any); ok && len(domains) > 0 {
		_, _ = fmt.Fprintf(c.out, "  Pending:\n")
		for _, d := range domains {
			_, _ = fmt.Fprintf(c.out, "    %v\n", d)
		}
	}
	if domains, ok := data["certs_failed_domains"].(map[string]any); ok && len(domains) > 0 {
		_, _ = fmt.Fprintf(c.out, "  Failed:\n")
		for d, errMsg := range domains {
			_, _ = fmt.Fprintf(c.out, "    %s: %v\n", d, errMsg)
		}
	}

	c.printHealthClients(data)
}

func (c *Client) printHealthClients(data map[string]any) {
	clients, ok := data["clients"].(map[string]any)
	if !ok || len(clients) == 0 {
		return
	}

	_, _ = fmt.Fprintf(c.out, "\n%-38s %-9s %-7s %-8s %-8s %-16s %s\n",
		"ACCOUNT ID", "HEALTHY", "MGMT", "SIGNAL", "RELAYS", "PEERS (P2P/RLY)", "DEGRADED")
	_, _ = fmt.Fprintln(c.out, strings.Repeat("-", 110))

	for accountID, v := range clients {
		ch, ok := v.(map[string]any)
		if !ok {
			continue
		}

		healthy := boolIcon(ch["healthy"])
		mgmt := boolIcon(ch["management_connected"])
		signal := boolIcon(ch["signal_connected"])

		relaysConn, _ := ch["relays_connected"].(float64)
		relaysTotal, _ := ch["relays_total"].(float64)
		relays := fmt.Sprintf("%d/%d", int(relaysConn), int(relaysTotal))

		peersConnected, _ := ch["peers_connected"].(float64)
		peersTotal, _ := ch["peers_total"].(float64)
		peersP2P, _ := ch["peers_p2p"].(float64)
		peersRelayed, _ := ch["peers_relayed"].(float64)
		peersDegraded, _ := ch["peers_degraded"].(float64)
		peers := fmt.Sprintf("%d/%d (%d/%d)", int(peersConnected), int(peersTotal), int(peersP2P), int(peersRelayed))
		degraded := fmt.Sprintf("%d", int(peersDegraded))

		_, _ = fmt.Fprintf(c.out, "%-38s %-9s %-7s %-8s %-8s %-16s %s", accountID, healthy, mgmt, signal, relays, peers, degraded)
		if errMsg, ok := ch["error"].(string); ok && errMsg != "" {
			_, _ = fmt.Fprintf(c.out, "  (%s)", errMsg)
		}
		_, _ = fmt.Fprintln(c.out)
	}
}

func boolIcon(v any) string {
	b, ok := v.(bool)
	if !ok {
		return "?"
	}
	if b {
		return "yes"
	}
	return "no"
}

// ListClients fetches the list of all clients.
func (c *Client) ListClients(ctx context.Context) error {
	return c.fetchAndPrint(ctx, "/debug/clients", c.printClients)
}

func (c *Client) printClients(data map[string]any) {
	_, _ = fmt.Fprintf(c.out, "Uptime: %v\n", data["uptime"])
	_, _ = fmt.Fprintf(c.out, "Clients: %v\n\n", data["client_count"])

	clients, ok := data["clients"].([]any)
	if !ok || len(clients) == 0 {
		_, _ = fmt.Fprintln(c.out, "No clients connected.")
		return
	}

	_, _ = fmt.Fprintf(c.out, "%-38s %-12s %-40s %s\n", "ACCOUNT ID", "AGE", "SERVICES", "HAS CLIENT")
	_, _ = fmt.Fprintln(c.out, strings.Repeat("-", 110))

	for _, item := range clients {
		c.printClientRow(item)
	}

	c.printInboundListeners(clients)
}

func (c *Client) printInboundListeners(clients []any) {
	type row struct {
		accountID string
		tunnelIP  string
		httpsPort int
		httpPort  int
	}
	var rows []row
	for _, item := range clients {
		client, ok := item.(map[string]any)
		if !ok {
			continue
		}
		inbound, ok := client["inbound_listener"].(map[string]any)
		if !ok {
			continue
		}
		tunnelIP, _ := inbound["tunnel_ip"].(string)
		httpsPort, _ := inbound["https_port"].(float64)
		httpPort, _ := inbound["http_port"].(float64)
		accountID, _ := client["account_id"].(string)
		rows = append(rows, row{
			accountID: accountID,
			tunnelIP:  tunnelIP,
			httpsPort: int(httpsPort),
			httpPort:  int(httpPort),
		})
	}
	if len(rows) == 0 {
		return
	}

	_, _ = fmt.Fprintln(c.out)
	_, _ = fmt.Fprintln(c.out, "Inbound listeners (per-account):")
	_, _ = fmt.Fprintf(c.out, "  %-38s %-20s %-7s %s\n", "ACCOUNT ID", "TUNNEL IP", "HTTPS", "HTTP")
	_, _ = fmt.Fprintln(c.out, "  "+strings.Repeat("-", 78))
	for _, r := range rows {
		_, _ = fmt.Fprintf(c.out, "  %-38s %-20s %-7d %d\n", r.accountID, r.tunnelIP, r.httpsPort, r.httpPort)
	}
}

func (c *Client) printClientRow(item any) {
	client, ok := item.(map[string]any)
	if !ok {
		return
	}

	services := c.extractServiceKeys(client)
	hasClient := "no"
	if hc, ok := client["has_client"].(bool); ok && hc {
		hasClient = "yes"
	}

	_, _ = fmt.Fprintf(c.out, "%-38s %-12v %s %s\n",
		client["account_id"],
		client["age"],
		services,
		hasClient,
	)
}

func (c *Client) extractServiceKeys(client map[string]any) string {
	d, ok := client["service_keys"].([]any)
	if !ok || len(d) == 0 {
		return "-"
	}

	parts := make([]string, len(d))
	for i, key := range d {
		parts[i] = fmt.Sprint(key)
	}
	return strings.Join(parts, ", ")
}

// ClientStatus fetches the status of a specific client.
func (c *Client) ClientStatus(ctx context.Context, accountID string, filters StatusFilters) error {
	params := url.Values{}
	if len(filters.IPs) > 0 {
		params.Set("filter-by-ips", strings.Join(filters.IPs, ","))
	}
	if len(filters.Names) > 0 {
		params.Set("filter-by-names", strings.Join(filters.Names, ","))
	}
	if filters.Status != "" {
		params.Set("filter-by-status", filters.Status)
	}
	if filters.ConnectionType != "" {
		params.Set("filter-by-connection-type", filters.ConnectionType)
	}

	path := "/debug/clients/" + url.PathEscape(accountID)
	if len(params) > 0 {
		path += "?" + params.Encode()
	}
	return c.fetchAndPrint(ctx, path, c.printClientStatus)
}

func (c *Client) printClientStatus(data map[string]any) {
	_, _ = fmt.Fprintf(c.out, "Account: %v\n", data["account_id"])
	if inbound, ok := data["inbound_listener"].(map[string]any); ok {
		tunnelIP, _ := inbound["tunnel_ip"].(string)
		httpsPort, _ := inbound["https_port"].(float64)
		httpPort, _ := inbound["http_port"].(float64)
		_, _ = fmt.Fprintf(c.out, "Inbound listener: %s (https=%d, http=%d)\n", tunnelIP, int(httpsPort), int(httpPort))
	}
	_, _ = fmt.Fprintln(c.out)
	if status, ok := data["status"].(string); ok {
		_, _ = fmt.Fprint(c.out, status)
	}
}

// ClientSyncResponse fetches the sync response of a specific client.
func (c *Client) ClientSyncResponse(ctx context.Context, accountID string) error {
	path := "/debug/clients/" + url.PathEscape(accountID) + "/syncresponse"
	return c.fetchAndPrintJSON(ctx, path)
}

// PingTCP performs a TCP ping through a client.
// ipVersion may be "4", "6", or "" for automatic.
func (c *Client) PingTCP(ctx context.Context, accountID, host string, port int, timeout time.Duration, ipVersion string) error {
	params := url.Values{}
	params.Set("host", host)
	params.Set("port", fmt.Sprintf("%d", port))
	if timeout > 0 {
		params.Set("timeout", timeout.String())
	}
	if ipVersion != "" {
		params.Set("ip_version", ipVersion)
	}

	path := fmt.Sprintf("/debug/clients/%s/pingtcp?%s", url.PathEscape(accountID), params.Encode())
	return c.fetchAndPrint(ctx, path, c.printPingResult)
}

func (c *Client) printPingResult(data map[string]any) {
	success, _ := data["success"].(bool)
	host := net.JoinHostPort(fmt.Sprint(data["host"]), fmt.Sprint(data["port"]))
	if success {
		remote, _ := data["remote"].(string)
		if remote != "" && remote != host {
			_, _ = fmt.Fprintf(c.out, "Success: %s (via %s)\n", host, remote)
		} else {
			_, _ = fmt.Fprintf(c.out, "Success: %s\n", host)
		}
		_, _ = fmt.Fprintf(c.out, "Latency: %v\n", data["latency"])
	} else {
		_, _ = fmt.Fprintf(c.out, "Failed: %s\n", host)
		c.printError(data)
	}
}

// SetLogLevel sets the log level of a specific client.
func (c *Client) SetLogLevel(ctx context.Context, accountID, level string) error {
	params := url.Values{}
	params.Set("level", level)

	path := fmt.Sprintf("/debug/clients/%s/loglevel?%s", url.PathEscape(accountID), params.Encode())
	return c.fetchAndPrint(ctx, path, c.printLogLevelResult)
}

func (c *Client) printLogLevelResult(data map[string]any) {
	success, _ := data["success"].(bool)
	if success {
		_, _ = fmt.Fprintf(c.out, "Log level set to: %v\n", data["level"])
	} else {
		_, _ = fmt.Fprintln(c.out, "Failed to set log level")
		c.printError(data)
	}
}

// PerfSet live-retunes the tunnel buffer pool cap on all running embedded
// clients. Batch size is not live-tunable; configure it at proxy startup.
func (c *Client) PerfSet(ctx context.Context, value uint32) error {
	path := fmt.Sprintf("/debug/perf?value=%d", value)
	return c.fetchAndPrint(ctx, path, c.printPerfSet)
}

func (c *Client) printPerfSet(data map[string]any) {
	if errMsg, ok := data["error"].(string); ok && errMsg != "" {
		c.printError(data)
		return
	}
	val, _ := data["value"].(float64)
	applied, _ := data["applied"].(float64)
	_, _ = fmt.Fprintf(c.out, "Pool cap set to: %d\n", uint32(val))
	_, _ = fmt.Fprintf(c.out, "Applied to %d live clients\n", int(applied))
	if failed, ok := data["failed"].(map[string]any); ok && len(failed) > 0 {
		_, _ = fmt.Fprintln(c.out, "Failed:")
		for k, v := range failed {
			_, _ = fmt.Fprintf(c.out, "  %s: %v\n", k, v)
		}
	}
}

// Runtime fetches runtime stats (heap, goroutines, RSS).
func (c *Client) Runtime(ctx context.Context) error {
	return c.fetchAndPrint(ctx, "/debug/runtime", c.printRuntime)
}

func (c *Client) printRuntime(data map[string]any) {
	i := func(k string) uint64 {
		v, _ := data[k].(float64)
		return uint64(v)
	}
	mb := func(n uint64) string { return fmt.Sprintf("%.1f MB", float64(n)/(1<<20)) }

	_, _ = fmt.Fprintf(c.out, "Uptime:       %v\n", data["uptime"])
	_, _ = fmt.Fprintf(c.out, "Go:           %v on %d CPU (GOMAXPROCS=%d)\n", data["go_version"], uint32(i("num_cpu")), uint32(i("gomaxprocs")))
	_, _ = fmt.Fprintf(c.out, "Goroutines:   %d\n", i("goroutines"))
	_, _ = fmt.Fprintf(c.out, "Live objects: %d\n", i("live_objects"))
	_, _ = fmt.Fprintf(c.out, "GC:           %d cycles, %v pause total\n", i("num_gc"), time.Duration(i("pause_total_ns")))
	_, _ = fmt.Fprintln(c.out, "Heap:")
	_, _ = fmt.Fprintf(c.out, "  alloc:      %s\n", mb(i("heap_alloc")))
	_, _ = fmt.Fprintf(c.out, "  in-use:     %s\n", mb(i("heap_inuse")))
	_, _ = fmt.Fprintf(c.out, "  idle:       %s\n", mb(i("heap_idle")))
	_, _ = fmt.Fprintf(c.out, "  released:   %s\n", mb(i("heap_released")))
	_, _ = fmt.Fprintf(c.out, "  sys:        %s\n", mb(i("heap_sys")))
	_, _ = fmt.Fprintf(c.out, "Total sys:    %s\n", mb(i("sys")))
	if _, ok := data["vm_rss"]; ok {
		_, _ = fmt.Fprintln(c.out, "Process:")
		_, _ = fmt.Fprintf(c.out, "  VmRSS:      %s\n", mb(i("vm_rss")))
		_, _ = fmt.Fprintf(c.out, "  VmSize:     %s\n", mb(i("vm_size")))
		_, _ = fmt.Fprintf(c.out, "  VmData:     %s\n", mb(i("vm_data")))
	}
	_, _ = fmt.Fprintf(c.out, "Clients:      %d (%d started)\n", i("clients"), i("started"))
}

// StartClient starts a specific client.
func (c *Client) StartClient(ctx context.Context, accountID string) error {
	path := "/debug/clients/" + url.PathEscape(accountID) + "/start"
	return c.fetchAndPrint(ctx, path, c.printStartResult)
}

func (c *Client) printStartResult(data map[string]any) {
	success, _ := data["success"].(bool)
	if success {
		_, _ = fmt.Fprintln(c.out, "Client started")
	} else {
		_, _ = fmt.Fprintln(c.out, "Failed to start client")
		c.printError(data)
	}
}

// StopClient stops a specific client.
func (c *Client) StopClient(ctx context.Context, accountID string) error {
	path := "/debug/clients/" + url.PathEscape(accountID) + "/stop"
	return c.fetchAndPrint(ctx, path, c.printStopResult)
}

func (c *Client) printStopResult(data map[string]any) {
	success, _ := data["success"].(bool)
	if success {
		_, _ = fmt.Fprintln(c.out, "Client stopped")
	} else {
		_, _ = fmt.Fprintln(c.out, "Failed to stop client")
		c.printError(data)
	}
}

func (c *Client) printError(data map[string]any) {
	if errMsg, ok := data["error"].(string); ok {
		_, _ = fmt.Fprintf(c.out, "Error: %s\n", errMsg)
	}
}

// CaptureOptions configures a capture request.
type CaptureOptions struct {
	AccountID  string
	Duration   string
	FilterExpr string
	Text       bool
	Verbose    bool
	ASCII      bool
	Output     io.Writer
}

// Capture streams a packet capture from the debug endpoint. The response body
// (pcap or text) is written directly to opts.Output until the server closes the
// connection or the context is cancelled.
func (c *Client) Capture(ctx context.Context, opts CaptureOptions) error {
	if opts.AccountID == "" {
		return fmt.Errorf("account ID is required")
	}
	if opts.Output == nil {
		return fmt.Errorf("output writer is required")
	}

	params := url.Values{}
	if opts.Duration != "" {
		params.Set("duration", opts.Duration)
	}
	if opts.FilterExpr != "" {
		params.Set("filter", opts.FilterExpr)
	}
	if opts.Text {
		params.Set("format", "text")
	}
	if opts.Verbose {
		params.Set("verbose", "true")
	}
	if opts.ASCII {
		params.Set("ascii", "true")
	}

	path := fmt.Sprintf("/debug/clients/%s/capture", url.PathEscape(opts.AccountID))
	if len(params) > 0 {
		path += "?" + params.Encode()
	}

	fullURL := c.baseURL + path
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}

	// Use a separate client without timeout since captures stream for their full duration.
	httpClient := &http.Client{}
	resp, err := httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode >= 400 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("server error (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
	}

	_, err = io.Copy(opts.Output, resp.Body)
	if err != nil && ctx.Err() != nil {
		return nil
	}
	return err
}

func (c *Client) fetchAndPrint(ctx context.Context, path string, printer func(map[string]any)) error {
	data, raw, err := c.fetch(ctx, path)
	if err != nil {
		return err
	}

	if c.jsonOutput {
		return c.writeJSON(data)
	}

	if data != nil {
		printer(data)
		return nil
	}

	_, _ = fmt.Fprintln(c.out, string(raw))
	return nil
}

func (c *Client) fetchAndPrintJSON(ctx context.Context, path string) error {
	data, raw, err := c.fetch(ctx, path)
	if err != nil {
		return err
	}

	if data != nil {
		return c.writeJSON(data)
	}

	_, _ = fmt.Fprintln(c.out, string(raw))
	return nil
}

func (c *Client) writeJSON(data map[string]any) error {
	enc := json.NewEncoder(c.out)
	enc.SetIndent("", "  ")
	return enc.Encode(data)
}

func (c *Client) fetch(ctx context.Context, path string) (map[string]any, []byte, error) {
	fullURL := c.baseURL + path
	if !strings.Contains(path, "format=json") {
		if strings.Contains(path, "?") {
			fullURL += "&format=json"
		} else {
			fullURL += "?format=json"
		}
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
	if err != nil {
		return nil, nil, fmt.Errorf("create request: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, nil, fmt.Errorf("request failed: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, nil, fmt.Errorf("read response: %w", err)
	}

	if resp.StatusCode >= 400 {
		return nil, nil, fmt.Errorf("server error (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
	}

	var data map[string]any
	if err := json.Unmarshal(body, &data); err != nil {
		return nil, body, nil
	}

	return data, body, nil
}
