package probe

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"log"
	"net"
	"net/http"
	"time"

	"github.com/jboard/jboard-agent/internal/config"
)

// Three-carrier TCP ping targets (Chinese ISP backbone nodes)
var latencyTargets = []struct {
	Carrier string
	Host    string
	Port    string
}{
	{"mobile", "js-cm-v4.ip.zstaticcdn.com", "80"},
	{"unicom", "js-cu-v4.ip.zstaticcdn.com", "80"},
	{"telecom", "js-ct-v4.ip.zstaticcdn.com", "80"},
}

type latencyEntry struct {
	Carrier   string `json:"carrier"`
	LatencyMs int    `json:"latencyMs"`
}

type latencyPayload struct {
	Latencies []latencyEntry `json:"latencies"`
}

// LatencyLoop periodically measures TCP ping latency to three carriers and pushes to J-Board.
func LatencyLoop(ctx context.Context, cfg *config.Config) {
	ticker := time.NewTicker(cfg.LatencyInterval)
	defer ticker.Stop()

	// Run immediately
	measureAndPush(cfg)

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			measureAndPush(cfg)
		}
	}
}

func measureAndPush(cfg *config.Config) {
	var entries []latencyEntry

	for _, target := range latencyTargets {
		ms := tcpPing(target.Host, target.Port)
		if ms >= 0 {
			entries = append(entries, latencyEntry{
				Carrier:   target.Carrier,
				LatencyMs: ms,
			})
			log.Printf("[latency] %s: %dms", target.Carrier, ms)
		} else {
			log.Printf("[latency] %s: timeout", target.Carrier)
		}
	}

	if len(entries) == 0 {
		return
	}

	payload := latencyPayload{Latencies: entries}
	body, _ := json.Marshal(payload)

	if err := postToServer(cfg, "/api/agent/latency", body); err != nil {
		log.Printf("[latency] push error: %v", err)
	}
}

// tcpPing measures TCP handshake latency in milliseconds. Returns -1 on failure.
// The DNS lookup is intentionally performed before timing starts, matching
// classic probe panels such as Komari, so DNS jitter is not mixed into latency.
func tcpPing(host, port string) int {
	const (
		timeout              = 3 * time.Second
		highLatencyThreshold = 1000
		highLatencyRetries   = 3
	)

	ip, err := resolveIP(host)
	if err != nil {
		return -1
	}

	latency, err := measureTCPConnect(ip, port, timeout)
	if err != nil {
		return -1
	}
	best := latency

	if latency > highLatencyThreshold {
		for i := 0; i < highLatencyRetries; i++ {
			retryLatency, retryErr := measureTCPConnect(ip, port, timeout)
			if retryErr != nil {
				continue
			}
			if retryLatency < best {
				best = retryLatency
			}
			if retryLatency <= highLatencyThreshold {
				break
			}
		}
	}

	return best
}

func resolveIP(host string) (string, error) {
	if ip := net.ParseIP(host); ip != nil {
		return host, nil
	}

	addrs, err := net.LookupHost(host)
	if err != nil || len(addrs) == 0 {
		return "", errors.New("failed to resolve target")
	}

	return addrs[0], nil
}

func measureTCPConnect(ip string, port string, timeout time.Duration) (int, error) {
	start := time.Now()
	conn, err := net.DialTimeout("tcp", net.JoinHostPort(ip, port), timeout)
	if err != nil {
		return -1, err
	}
	conn.Close()
	return int(time.Since(start).Milliseconds()), nil
}

func postToServer(cfg *config.Config, path string, body []byte) error {
	req, err := http.NewRequest("POST", cfg.ServerURL+path, bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+cfg.AuthToken)

	client := &http.Client{Timeout: 15 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return &httpError{StatusCode: resp.StatusCode}
	}
	return nil
}

type httpError struct {
	StatusCode int
}

func (e *httpError) Error() string {
	return "server returned " + http.StatusText(e.StatusCode)
}
