package netcheck

import (
	"context"
	"fmt"
	"net"
	"strings"
	"time"

	"vpnclient/internal/config"
	"vpnclient/internal/linknorm"
	"vpnclient/internal/protocols/awg"
	"vpnclient/internal/protocols/hysteria2"
)

// Result is a reachability measurement to a proxy host.
type Result struct {
	Name  string `json:"name"`
	Host  string `json:"host"`
	Port  string `json:"port"`
	Ms    int64  `json:"ms"`
	OK    bool   `json:"ok"`
	Error string `json:"error,omitempty"`
}

// PingProxyURI measures connect time to the host in a share/proxy URI.
func PingProxyURI(ctx context.Context, name, proxyURI string) Result {
	return PingProfile(ctx, name, "", proxyURI)
}

// PingProfile pings using protocol hints when available.
// Naive uses TCP; Hysteria 2 uses UDP (QUIC) — TCP would always look "refused".
func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyURI string) Result {
	res := Result{Name: name}
	if strings.TrimSpace(proxyURI) == "" {
		res.Error = "нет ссылки сервера"
		return res
	}

	hy2 := proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI)
	isAWG := proto == config.ProtocolAWG || awg.Detect(proxyURI)
	var host, port string
	if isAWG {
		h, p, err := awg.HostPort(proxyURI)
		if err != nil {
			res.Error = err.Error()
			return res
		}
		host, port = h, p
	} else if hy2 {
		h, p, err := hysteria2.HostPort(proxyURI)
		if err != nil {
			res.Error = err.Error()
			return res
		}
		host, port = h, p
	} else {
		normalized, _, _, err := linknorm.Normalize(proto, proxyURI)
		if err != nil {
			res.Error = err.Error()
			return res
		}
		rest := normalized
		if i := strings.Index(rest, "://"); i >= 0 {
			rest = rest[i+3:]
		}
		if at := strings.LastIndex(rest, "@"); at >= 0 {
			rest = rest[at+1:]
		}
		if i := strings.IndexAny(rest, "/?#"); i >= 0 {
			rest = rest[:i]
		}
		host = rest
		port = "443"
		if i := strings.LastIndex(rest, ":"); i >= 0 {
			host = rest[:i]
			port = rest[i+1:]
		}
	}

	res.Host = host
	res.Port = port
	if host == "" {
		res.Error = "нет хоста"
		return res
	}

	start := time.Now()
	var err error
	if hy2 || isAWG {
		err = probeUDP(ctx, host, port)
	} else {
		err = probeTCP(ctx, host, port)
	}
	if err != nil {
		res.Error = friendlyDialError(err, hy2 || isAWG)
		return res
	}
	res.Ms = time.Since(start).Milliseconds()
	res.OK = true
	return res
}

func probeTCP(ctx context.Context, host, port string) error {
	d := net.Dialer{Timeout: 4 * time.Second}
	conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
	if err != nil {
		return err
	}
	_ = conn.Close()
	return nil
}

func probeUDP(ctx context.Context, host, port string) error {
	d := net.Dialer{Timeout: 4 * time.Second}
	conn, err := d.DialContext(ctx, "udp", net.JoinHostPort(host, port))
	if err != nil {
		return err
	}
	defer conn.Close()

	deadline := time.Now().Add(1500 * time.Millisecond)
	if dl, ok := ctx.Deadline(); ok && dl.Before(deadline) {
		deadline = dl
	}
	_ = conn.SetDeadline(deadline)

	// Any datagram is enough to exercise the UDP path; QUIC rarely answers a bare probe.
	if _, err := conn.Write([]byte{0}); err != nil {
		return err
	}
	buf := make([]byte, 64)
	_, err = conn.Read(buf)
	if err == nil {
		return nil
	}
	if ne, ok := err.(net.Error); ok && ne.Timeout() {
		// No ICMP unreachable → port is typically open or filtered; treat as reachable for UI ping.
		return nil
	}
	return err
}

func friendlyDialError(err error, hy2 bool) string {
	msg := err.Error()
	lower := strings.ToLower(msg)
	switch {
	case strings.Contains(lower, "refused"):
		if hy2 {
			return "UDP порт недоступен (connection refused)"
		}
		return "порт закрыт (connection refused)"
	case strings.Contains(lower, "timeout") || strings.Contains(lower, "timed out"):
		return "таймаут"
	case strings.Contains(lower, "no such host"):
		return "DNS: хост не найден"
	default:
		return fmt.Sprintf("недоступен: %v", err)
	}
}
