package awg

import (
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"net"
	"net/netip"
	"strconv"
	"strings"
)

// Conf is a parsed AmneziaWG / WireGuard client configuration (AWG 2.0 fields included).
type Conf struct {
	PrivateKey  string
	Address     []string
	DNS         []string
	MTU         int
	Jc          int
	Jmin        int
	Jmax        int
	S1, S2, S3, S4 int
	H1, H2, H3, H4 string
	I1, I2, I3, I4, I5 string
	PublicKey   string
	PresharedKey string
	Endpoint    string
	AllowedIPs  []string
	Keepalive   int
	Name        string
}

// Detect reports whether raw looks like an AWG/WireGuard config or share link.
func Detect(raw string) bool {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return false
	}
	lower := strings.ToLower(raw)
	if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
		strings.HasPrefix(lower, "wireguard://") {
		return true
	}
	if strings.Contains(lower, "[interface]") && strings.Contains(lower, "privatekey") {
		return true
	}
	return false
}

// Parse accepts a WireGuard/AWG .conf body or awg:// URI and returns Conf.
func Parse(raw string) (Conf, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return Conf{}, fmt.Errorf("пустой AWG конфиг")
	}
	lower := strings.ToLower(raw)
	switch {
	case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
		strings.HasPrefix(lower, "wireguard://"):
		return parseURI(raw)
	default:
		return parseINI(raw)
	}
}

func parseINI(raw string) (Conf, error) {
	var c Conf
	section := ""
	for _, line := range strings.Split(raw, "\n") {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
			continue
		}
		if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
			section = strings.ToLower(strings.TrimSpace(line[1 : len(line)-1]))
			continue
		}
		key, val, ok := splitKV(line)
		if !ok {
			continue
		}
		k := strings.ToLower(key)
		switch section {
		case "interface":
			switch k {
			case "privatekey":
				c.PrivateKey = val
			case "address":
				c.Address = appendCSV(c.Address, val)
			case "dns":
				c.DNS = appendCSV(c.DNS, val)
			case "mtu":
				c.MTU, _ = strconv.Atoi(val)
			case "jc":
				c.Jc, _ = strconv.Atoi(val)
			case "jmin":
				c.Jmin, _ = strconv.Atoi(val)
			case "jmax":
				c.Jmax, _ = strconv.Atoi(val)
			case "s1":
				c.S1, _ = strconv.Atoi(val)
			case "s2":
				c.S2, _ = strconv.Atoi(val)
			case "s3":
				c.S3, _ = strconv.Atoi(val)
			case "s4":
				c.S4, _ = strconv.Atoi(val)
			case "h1":
				c.H1 = val
			case "h2":
				c.H2 = val
			case "h3":
				c.H3 = val
			case "h4":
				c.H4 = val
			case "i1":
				c.I1 = val
			case "i2":
				c.I2 = val
			case "i3":
				c.I3 = val
			case "i4":
				c.I4 = val
			case "i5":
				c.I5 = val
			}
		case "peer":
			switch k {
			case "publickey":
				c.PublicKey = val
			case "presharedkey":
				c.PresharedKey = val
			case "endpoint":
				c.Endpoint = val
			case "allowedips":
				c.AllowedIPs = appendCSV(c.AllowedIPs, val)
			case "persistentkeepalive":
				c.Keepalive, _ = strconv.Atoi(val)
			}
		}
	}
	if err := c.validate(); err != nil {
		return Conf{}, err
	}
	return c, nil
}

func parseURI(raw string) (Conf, error) {
	// awg://host:port/?public_key=...&private_key=...&address=10.8.0.2/32&jc=4&...
	u := raw
	schemeIdx := strings.Index(u, "://")
	if schemeIdx < 0 {
		return Conf{}, fmt.Errorf("bad awg uri")
	}
	rest := u[schemeIdx+3:]
	hostPort := rest
	query := ""
	if i := strings.Index(rest, "?"); i >= 0 {
		hostPort = rest[:i]
		query = rest[i+1:]
	}
	hostPort = strings.TrimSuffix(hostPort, "/")
	c := Conf{Endpoint: hostPort}
	for _, part := range strings.Split(query, "&") {
		if part == "" {
			continue
		}
		k, v, ok := strings.Cut(part, "=")
		if !ok {
			continue
		}
		k = strings.ToLower(strings.TrimSpace(k))
		v = strings.TrimSpace(v)
		v, _ = urlDecode(v)
		switch k {
		case "private_key", "privatekey":
			c.PrivateKey = v
		case "public_key", "publickey":
			c.PublicKey = v
		case "preshared_key", "presharedkey", "psk":
			c.PresharedKey = v
		case "address", "addr":
			c.Address = appendCSV(c.Address, v)
		case "dns":
			c.DNS = appendCSV(c.DNS, v)
		case "allowed_ips", "allowedips":
			c.AllowedIPs = appendCSV(c.AllowedIPs, v)
		case "mtu":
			c.MTU, _ = strconv.Atoi(v)
		case "keepalive", "persistentkeepalive":
			c.Keepalive, _ = strconv.Atoi(v)
		case "jc":
			c.Jc, _ = strconv.Atoi(v)
		case "jmin":
			c.Jmin, _ = strconv.Atoi(v)
		case "jmax":
			c.Jmax, _ = strconv.Atoi(v)
		case "s1":
			c.S1, _ = strconv.Atoi(v)
		case "s2":
			c.S2, _ = strconv.Atoi(v)
		case "s3":
			c.S3, _ = strconv.Atoi(v)
		case "s4":
			c.S4, _ = strconv.Atoi(v)
		case "h1":
			c.H1 = v
		case "h2":
			c.H2 = v
		case "h3":
			c.H3 = v
		case "h4":
			c.H4 = v
		case "i1":
			c.I1 = v
		case "i2":
			c.I2 = v
		case "i3":
			c.I3 = v
		case "i4":
			c.I4 = v
		case "i5":
			c.I5 = v
		case "name", "remark":
			c.Name = v
		case "endpoint":
			c.Endpoint = v
		}
	}
	if err := c.validate(); err != nil {
		return Conf{}, err
	}
	return c, nil
}

func (c *Conf) validate() error {
	if c.PrivateKey == "" {
		return fmt.Errorf("awg: нет PrivateKey")
	}
	if c.PublicKey == "" {
		return fmt.Errorf("awg: нет PublicKey")
	}
	if c.Endpoint == "" {
		return fmt.Errorf("awg: нет Endpoint")
	}
	if len(c.Address) == 0 {
		return fmt.Errorf("awg: нет Address")
	}
	if len(c.AllowedIPs) == 0 {
		c.AllowedIPs = []string{"0.0.0.0/0", "::/0"}
	}
	return nil
}

// HostPort returns UDP endpoint host and port for ping.
func HostPort(raw string) (host, port string, err error) {
	c, err := Parse(raw)
	if err != nil {
		return "", "", err
	}
	h, p, err := net.SplitHostPort(c.Endpoint)
	if err != nil {
		// bare host without port
		return c.Endpoint, "51820", nil
	}
	return h, p, nil
}

// ToIPC builds the amneziawg-go UAPI configuration string.
func (c Conf) ToIPC() (string, error) {
	priv, err := decodeKey(c.PrivateKey)
	if err != nil {
		return "", fmt.Errorf("private key: %w", err)
	}
	pub, err := decodeKey(c.PublicKey)
	if err != nil {
		return "", fmt.Errorf("public key: %w", err)
	}
	var b strings.Builder
	b.WriteString("private_key=")
	b.WriteString(hex.EncodeToString(priv))
	writeInt(&b, "jc", c.Jc)
	writeInt(&b, "jmin", c.Jmin)
	writeInt(&b, "jmax", c.Jmax)
	writeInt(&b, "s1", c.S1)
	writeInt(&b, "s2", c.S2)
	writeInt(&b, "s3", c.S3)
	writeInt(&b, "s4", c.S4)
	writeStr(&b, "h1", c.H1)
	writeStr(&b, "h2", c.H2)
	writeStr(&b, "h3", c.H3)
	writeStr(&b, "h4", c.H4)
	writeStr(&b, "i1", c.I1)
	writeStr(&b, "i2", c.I2)
	writeStr(&b, "i3", c.I3)
	writeStr(&b, "i4", c.I4)
	writeStr(&b, "i5", c.I5)

	b.WriteString("\npublic_key=")
	b.WriteString(hex.EncodeToString(pub))
	b.WriteString("\nendpoint=")
	b.WriteString(c.Endpoint)
	for _, ip := range c.AllowedIPs {
		b.WriteString("\nallowed_ip=")
		b.WriteString(strings.TrimSpace(ip))
	}
	if c.PresharedKey != "" {
		psk, err := decodeKey(c.PresharedKey)
		if err != nil {
			return "", fmt.Errorf("preshared key: %w", err)
		}
		b.WriteString("\npreshared_key=")
		b.WriteString(hex.EncodeToString(psk))
	}
	if c.Keepalive > 0 {
		writeInt(&b, "persistent_keepalive_interval", c.Keepalive)
	}
	return b.String(), nil
}

// LocalAddrs parses Interface Address values.
func (c Conf) LocalAddrs() ([]netip.Addr, error) {
	var out []netip.Addr
	for _, a := range c.Address {
		a = strings.TrimSpace(a)
		if a == "" {
			continue
		}
		if p, err := netip.ParsePrefix(a); err == nil {
			out = append(out, p.Addr())
			continue
		}
		addr, err := netip.ParseAddr(a)
		if err != nil {
			return nil, fmt.Errorf("address %q: %w", a, err)
		}
		out = append(out, addr)
	}
	if len(out) == 0 {
		return nil, fmt.Errorf("no local addresses")
	}
	return out, nil
}

// DNSAddrs parses DNS servers (defaults to 1.1.1.1).
func (c Conf) DNSAddrs() []netip.Addr {
	var out []netip.Addr
	for _, d := range c.DNS {
		d = strings.TrimSpace(d)
		if d == "" {
			continue
		}
		if addr, err := netip.ParseAddr(d); err == nil {
			out = append(out, addr)
		}
	}
	if len(out) == 0 {
		out = append(out, netip.MustParseAddr("1.1.1.1"))
	}
	return out
}

func (c Conf) EffectiveMTU() int {
	if c.MTU > 0 {
		return c.MTU
	}
	return 1420
}

func decodeKey(s string) ([]byte, error) {
	s = strings.TrimSpace(s)
	if b, err := base64.StdEncoding.DecodeString(s); err == nil && len(b) == 32 {
		return b, nil
	}
	if b, err := hex.DecodeString(s); err == nil && len(b) == 32 {
		return b, nil
	}
	return nil, fmt.Errorf("expected 32-byte base64/hex key")
}

func writeInt(b *strings.Builder, key string, v int) {
	if v == 0 {
		return
	}
	b.WriteByte('\n')
	b.WriteString(key)
	b.WriteByte('=')
	b.WriteString(strconv.Itoa(v))
}

func writeStr(b *strings.Builder, key, v string) {
	v = strings.TrimSpace(v)
	if v == "" {
		return
	}
	b.WriteByte('\n')
	b.WriteString(key)
	b.WriteByte('=')
	b.WriteString(v)
}

func splitKV(line string) (key, val string, ok bool) {
	i := strings.IndexAny(line, "=:")
	if i < 0 {
		return "", "", false
	}
	return strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:]), true
}

func appendCSV(dst []string, raw string) []string {
	for _, p := range strings.Split(raw, ",") {
		p = strings.TrimSpace(p)
		if p != "" {
			dst = append(dst, p)
		}
	}
	return dst
}

func urlDecode(s string) (string, error) {
	var b strings.Builder
	for i := 0; i < len(s); {
		switch s[i] {
		case '+':
			b.WriteByte(' ')
			i++
		case '%':
			if i+2 >= len(s) {
				return s, nil
			}
			var v byte
			for _, c := range []byte{s[i+1], s[i+2]} {
				v <<= 4
				switch {
				case c >= '0' && c <= '9':
					v |= c - '0'
				case c >= 'a' && c <= 'f':
					v |= c - 'a' + 10
				case c >= 'A' && c <= 'F':
					v |= c - 'A' + 10
				default:
					return s, nil
				}
			}
			b.WriteByte(v)
			i += 3
		default:
			b.WriteByte(s[i])
			i++
		}
	}
	return b.String(), nil
}

// NormalizeShareLink stores a canonical conf body (or passes through awg URI).
func NormalizeShareLink(raw string) (normalized string, remark string, err error) {
	c, err := Parse(raw)
	if err != nil {
		return "", "", err
	}
	if c.Name != "" {
		remark = c.Name
	} else if h, _, e := net.SplitHostPort(c.Endpoint); e == nil {
		remark = h
	} else {
		remark = c.Endpoint
	}
	// Prefer keeping original INI when pasted; re-serialize only for URI imports.
	lower := strings.ToLower(strings.TrimSpace(raw))
	if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
		strings.HasPrefix(lower, "wireguard://") {
		return serializeINI(c), remark, nil
	}
	return strings.TrimSpace(raw), remark, nil
}

func serializeINI(c Conf) string {
	var b strings.Builder
	b.WriteString("[Interface]\n")
	b.WriteString("PrivateKey = " + c.PrivateKey + "\n")
	if len(c.Address) > 0 {
		b.WriteString("Address = " + strings.Join(c.Address, ", ") + "\n")
	}
	if len(c.DNS) > 0 {
		b.WriteString("DNS = " + strings.Join(c.DNS, ", ") + "\n")
	}
	if c.MTU > 0 {
		b.WriteString("MTU = " + strconv.Itoa(c.MTU) + "\n")
	}
	if c.Jc != 0 {
		b.WriteString("Jc = " + strconv.Itoa(c.Jc) + "\n")
	}
	if c.Jmin != 0 {
		b.WriteString("Jmin = " + strconv.Itoa(c.Jmin) + "\n")
	}
	if c.Jmax != 0 {
		b.WriteString("Jmax = " + strconv.Itoa(c.Jmax) + "\n")
	}
	if c.S1 != 0 {
		b.WriteString("S1 = " + strconv.Itoa(c.S1) + "\n")
	}
	if c.S2 != 0 {
		b.WriteString("S2 = " + strconv.Itoa(c.S2) + "\n")
	}
	if c.S3 != 0 {
		b.WriteString("S3 = " + strconv.Itoa(c.S3) + "\n")
	}
	if c.S4 != 0 {
		b.WriteString("S4 = " + strconv.Itoa(c.S4) + "\n")
	}
	if c.H1 != "" {
		b.WriteString("H1 = " + c.H1 + "\n")
	}
	if c.H2 != "" {
		b.WriteString("H2 = " + c.H2 + "\n")
	}
	if c.H3 != "" {
		b.WriteString("H3 = " + c.H3 + "\n")
	}
	if c.H4 != "" {
		b.WriteString("H4 = " + c.H4 + "\n")
	}
	if c.I1 != "" {
		b.WriteString("I1 = " + c.I1 + "\n")
	}
	if c.I2 != "" {
		b.WriteString("I2 = " + c.I2 + "\n")
	}
	if c.I3 != "" {
		b.WriteString("I3 = " + c.I3 + "\n")
	}
	if c.I4 != "" {
		b.WriteString("I4 = " + c.I4 + "\n")
	}
	if c.I5 != "" {
		b.WriteString("I5 = " + c.I5 + "\n")
	}
	b.WriteString("\n[Peer]\n")
	b.WriteString("PublicKey = " + c.PublicKey + "\n")
	if c.PresharedKey != "" {
		b.WriteString("PresharedKey = " + c.PresharedKey + "\n")
	}
	b.WriteString("Endpoint = " + c.Endpoint + "\n")
	if len(c.AllowedIPs) > 0 {
		b.WriteString("AllowedIPs = " + strings.Join(c.AllowedIPs, ", ") + "\n")
	}
	if c.Keepalive > 0 {
		b.WriteString("PersistentKeepalive = " + strconv.Itoa(c.Keepalive) + "\n")
	}
	return b.String()
}
