package checker

import (
	"errors"
	"fmt"
	"net"
	"strings"
	"syscall"
)

// socks5ReplyHints maps SOCKS5 REP codes to short Russian explanations
// used by hintFor for the "connect" and "udp" steps. Codes outside this
// table fall back to a generic "unknown REP" message.
var socks5ReplyHints = map[byte]string{
	0x01: "общий сбой SOCKS5-сервера",
	0x02: "правила прокси запрещают это соединение",
	0x03: "сеть назначения недоступна",
	0x04: "хост назначения недоступен",
	0x05: "connection refused",
	0x06: "истёк TTL",
	0x07: "команда не поддерживается",
	0x08: "тип адреса не поддерживается",
}

// tcpFriendlyName turns a testID into a Russian-friendly label for the
// generic fallback hint.
func tcpFriendlyName(testID string) string {
	switch testID {
	case "tcp":
		return "TCP"
	case "greet":
		return "приветствие SOCKS5"
	case "auth":
		return "авторизация SOCKS5"
	case "connect":
		return "TCP-туннель к Discord"
	case "udp":
		return "UDP ASSOCIATE"
	case "voice-quality":
		return "качество UDP-канала"
	case "api":
		return "Discord API"
	default:
		return testID
	}
}

// hintFor returns a short Russian-language explanation of why a test
// failed. Returns "" when err is nil.
func hintFor(testID string, err error) string {
	if err == nil {
		return ""
	}
	if isContextErr(err) {
		return "Проверка отменена."
	}

	// Common error shapes we recognise across all testIDs.
	var ne net.Error
	isTimeout := errors.As(err, &ne) && ne.Timeout()

	var rep ErrSocks5Reply
	hasReply := errors.As(err, &rep)

	switch testID {
	case "tcp":
		switch {
		case isTimeout:
			return "Превышен таймаут подключения — прокси может быть выключен или брандмауэр режет порт."
		case errors.Is(err, syscall.ECONNREFUSED):
			return "Прокси отклонил TCP-соединение — порт закрыт или сервис не запущен."
		}
		return fmt.Sprintf("Прокси не отвечает по TCP — проверь host и port (%s).", err.Error())

	case "greet":
		switch {
		case errors.Is(err, ErrSocks5BadVersion):
			return "Сервер вернул не SOCKS5 — возможно, это HTTP-прокси."
		case errors.Is(err, ErrSocks5RejectedAllAuth):
			return "Прокси требует авторизацию, но мы её не предложили (или прокси не принимает наши методы)."
		case errors.Is(err, ErrShortReply):
			return "SOCKS5-сервер прислал укороченный ответ на приветствие."
		case isTimeout:
			return "SOCKS5-сервер не ответил на приветствие вовремя."
		}
		return genericFallback(testID, err)

	case "auth":
		switch {
		case errors.Is(err, ErrAuthRejected):
			return "Логин или пароль неверны."
		case errors.Is(err, ErrCredentialTooLong):
			return "Логин или пароль длиннее 255 байт — SOCKS5 такого не позволяет."
		case errors.Is(err, ErrShortReply):
			return "SOCKS5-сервер прислал укороченный ответ на авторизацию."
		case isTimeout:
			return "SOCKS5-сервер не ответил на авторизацию вовремя."
		}
		return genericFallback(testID, err)

	case "connect":
		if hasReply {
			return socks5ReplyHint("connect", rep.Code)
		}
		switch {
		case errors.Is(err, ErrHostTooLong):
			return "Имя хоста длиннее 255 байт — SOCKS5 такого не позволяет."
		case errors.Is(err, ErrShortReply):
			return "SOCKS5-сервер прислал укороченный ответ на CONNECT."
		case isTimeout:
			return "SOCKS5-сервер не ответил на CONNECT вовремя."
		}
		return genericFallback(testID, err)

	case "udp":
		if hasReply {
			return socks5ReplyHint("udp", rep.Code)
		}
		switch {
		case errors.Is(err, ErrUnsupportedRelayATYP):
			return "Прокси выдал IPv6 relay для UDP — пока не поддерживается, голос работать не будет."
		case errors.Is(err, ErrShortReply):
			return "SOCKS5-сервер прислал укороченный ответ на UDP ASSOCIATE."
		case isTimeout:
			return "SOCKS5-сервер не ответил на UDP ASSOCIATE вовремя."
		}
		return genericFallback(testID, err)

	case "voice-quality":
		switch {
		case errors.Is(err, ErrSTUNNoMappedAddress):
			return "STUN-ответ без XOR-MAPPED-ADDRESS — UDP-релей не пропускает обратный трафик."
		case errors.Is(err, ErrSTUNTooShort),
			errors.Is(err, ErrSTUNBadMagicCookie),
			errors.Is(err, ErrSTUNNotSuccess),
			errors.Is(err, ErrSTUNTxIDMismatch),
			errors.Is(err, ErrSTUNUnsupportedFamily):
			return "STUN-релей возвращает мусор — голос работать не будет."
		case isTimeout:
			return "STUN-релей не отвечает — UDP через прокси сильно теряет пакеты."
		}
		var dnsErr *net.DNSError
		if errors.As(err, &dnsErr) {
			return "Не удалось разрезолвить STUN-сервер — проверь системный DNS."
		}
		return genericFallback(testID, err)

	case "api":
		switch {
		case isTimeout:
			return "Discord API не ответил вовремя через прокси — таймаут."
		}
		return fmt.Sprintf("Discord API недоступен через прокси — TLS handshake упал (%s).", err.Error())
	}

	return genericFallback(testID, err)
}

// socks5ReplyHint formats a SOCKS5 REP-code hint specialised by step.
// "connect" wording references Discord; "udp" wording references voice.
func socks5ReplyHint(step string, code byte) string {
	desc, ok := socks5ReplyHints[code]
	if !ok {
		desc = "неизвестная REP"
	}
	switch step {
	case "udp":
		// 0x07 (cmd not supported) is the headline UDP failure mode.
		if code == 0x07 {
			return "Прокси не поддерживает UDP ASSOCIATE — голос Discord работать не будет."
		}
		return fmt.Sprintf("Прокси отклонил UDP ASSOCIATE (REP=%02X, %s).", code, desc)
	case "connect":
		if code == 0x05 {
			return "Прокси не смог подключиться к Discord (REP=05, connection refused)."
		}
		if code == 0x07 {
			return "Прокси не поддерживает CONNECT (REP=07)."
		}
		return fmt.Sprintf("Прокси отклонил CONNECT к Discord (REP=%02X, %s).", code, desc)
	}
	return fmt.Sprintf("Прокси отклонил запрос (REP=%02X, %s).", code, desc)
}

// genericFallback is the catch-all used when we don't recognise the
// (testID, err) shape. Keeps the user informed without exposing raw Go
// error wrapping.
func genericFallback(testID string, err error) string {
	return fmt.Sprintf("Не удалось выполнить шаг «%s»: %s", tcpFriendlyName(testID), err.Error())
}

// voiceQualityWarnHint composes a warn-tier hint based on which threshold
// was violated. Thresholds match runVoiceQuality's warn band: loss>5,
// jitter>30, p50>250. Always returns non-empty.
func voiceQualityWarnHint(loss, jitter, p50 float64) string {
	parts := make([]string, 0, 3)
	if loss > 5.0 {
		parts = append(parts, fmt.Sprintf("Потери UDP %.0f%% — голос будет с заиканиями", loss))
	}
	if jitter > 30.0 {
		parts = append(parts, fmt.Sprintf("большой джиттер %.1fms — звук будет дёргаться", jitter))
	}
	if p50 > 250.0 {
		parts = append(parts, fmt.Sprintf("высокая задержка %.0fms — заметная рассинхронизация при разговоре", p50))
	}
	if len(parts) == 0 {
		// Shouldn't happen — caller only invokes us in the warn band.
		return "UDP-канал на грани приемлемого — возможны помехи в голосе."
	}
	return strings.Join(parts, "; ") + "."
}

// voiceQualityFailHint composes a fail-tier hint. p95 is informational —
// included only when notably worse than p50.
func voiceQualityFailHint(loss, jitter, p50, p95 float64) string {
	_ = p95
	parts := make([]string, 0, 3)
	if loss > 15.0 {
		parts = append(parts, fmt.Sprintf("Потери UDP %.0f%% — голос работать не будет", loss))
	} else if loss > 5.0 {
		parts = append(parts, fmt.Sprintf("Потери UDP %.0f%%", loss))
	}
	if jitter > 60.0 {
		parts = append(parts, fmt.Sprintf("джиттер %.1fms — звук развалится", jitter))
	} else if jitter > 30.0 {
		parts = append(parts, fmt.Sprintf("джиттер %.1fms", jitter))
	}
	if p50 > 400.0 {
		parts = append(parts, fmt.Sprintf("задержка %.0fms — голос идёт со значительной паузой", p50))
	} else if p50 > 250.0 {
		parts = append(parts, fmt.Sprintf("задержка %.0fms", p50))
	}
	if len(parts) == 0 {
		return "UDP-канал не пригоден для голоса."
	}
	return strings.Join(parts, "; ") + "."
}
