package main

import (
	"context"
	"crypto/subtle"
	"flag"
	"fmt"
	"net"
	"net/netip"
	"os"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"dragontcp/internal/protocol"
	"dragontcp/internal/wire"
)

var active int64

type dnsEntry struct {
	ips     []netip.Addr
	expires time.Time
}

type dnsCache struct {
	mu      sync.RWMutex
	entries map[string]dnsEntry
	ttl     time.Duration
	max     int
}

func newDNSCache(ttl time.Duration, max int) *dnsCache {
	return &dnsCache{
		entries: make(map[string]dnsEntry),
		ttl:     ttl,
		max:     max,
	}
}

func (c *dnsCache) resolve(ctx context.Context, host string) ([]netip.Addr, error) {
	if ip, err := netip.ParseAddr(host); err == nil {
		return []netip.Addr{ip}, nil
	}

	now := time.Now()
	c.mu.RLock()
	entry, ok := c.entries[host]
	c.mu.RUnlock()
	if ok && now.Before(entry.expires) {
		return entry.ips, nil
	}

	ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
	if err != nil {
		return nil, err
	}

	c.mu.Lock()
	if len(c.entries) >= c.max {
		// Simple bounded reset keeps the hot cache cheap and prevents growth.
		c.entries = make(map[string]dnsEntry, c.max)
	}
	c.entries[host] = dnsEntry{ips: ips, expires: now.Add(c.ttl)}
	c.mu.Unlock()

	return ips, nil
}

func tokenEqual(a, b string) bool {
	if len(a) != len(b) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}

var blockedSpecial = []netip.Prefix{
	netip.MustParsePrefix("0.0.0.0/8"),
	netip.MustParsePrefix("100.64.0.0/10"),
	netip.MustParsePrefix("192.0.0.0/24"),
	netip.MustParsePrefix("192.0.2.0/24"),
	netip.MustParsePrefix("198.18.0.0/15"),
	netip.MustParsePrefix("198.51.100.0/24"),
	netip.MustParsePrefix("203.0.113.0/24"),
	netip.MustParsePrefix("240.0.0.0/4"),
	netip.MustParsePrefix("2001:db8::/32"),
}

func addressAllowed(addr netip.Addr, allowPrivate bool) bool {
	if addr.IsUnspecified() || addr.IsMulticast() {
		return false
	}

	if allowPrivate {
		return true
	}

	if !addr.IsGlobalUnicast() ||
		addr.IsPrivate() ||
		addr.IsLoopback() ||
		addr.IsLinkLocalUnicast() {
		return false
	}

	for _, prefix := range blockedSpecial {
		if prefix.Contains(addr) {
			return false
		}
	}

	return true
}

func dialTarget(ctx context.Context, host string, port int, allowPrivate bool, cache *dnsCache, tcpBuffer int) (net.Conn, error) {
	ips, err := cache.resolve(ctx, host)
	if err != nil {
		return nil, err
	}

	var lastErr error
	var blocked []string

	d := net.Dialer{
		Timeout:   10 * time.Second,
		KeepAlive: 30 * time.Second,
	}

	for _, ip := range ips {
		if !addressAllowed(ip, allowPrivate) {
			blocked = append(blocked, ip.String())
			continue
		}

		addr := net.JoinHostPort(ip.String(), strconv.Itoa(port))
		conn, err := d.DialContext(ctx, "tcp", addr)
		if err == nil {
			protocol.TuneTCP(conn)
			protocol.TuneTCPBuffer(conn, tcpBuffer)
			return conn, nil
		}
		lastErr = err
	}

	if lastErr != nil {
		return nil, lastErr
	}
	if len(blocked) > 0 {
		return nil, fmt.Errorf("target resolves only to blocked addresses: %s", strings.Join(blocked, ","))
	}
	return nil, fmt.Errorf("no usable target address")
}

func handle(
	conn net.Conn,
	token string,
	allowPrivate bool,
	cache *dnsCache,
	tcpBuffer int,
	slots chan struct{},
	manager *streamManager,
	xorManager *chunkManager,
	chunkMax int,
	bufferBytes int,
	chunkBuffered int,
	chunkPollWait time.Duration,
	debug *serverDebug,
) {
	defer func() {
		<-slots
		atomic.AddInt64(&active, -1)
		_ = conn.Close()
	}()

	protocol.TuneTCP(conn)
	protocol.TuneTCPBuffer(conn, tcpBuffer)

	// One listener serves both wires. The legacy XOR framing starts every
	// request with the ASCII magic "UP"; the binary framing starts with a mode
	// byte of 0-4, so the two are never ambiguous.
	_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
	conn, isXOR, err := sniffWire(conn)
	if err != nil {
		return
	}
	if isXOR {
		if debug != nil && debug.enabled {
			debug.logf("WIRE peer=%v mode=xor", conn.RemoteAddr())
		}
		handleXOR(conn, token, allowPrivate, cache, tcpBuffer, xorManager,
			chunkMax, chunkBuffered, chunkPollWait, debug)
		return
	}
	if debug != nil && debug.enabled {
		debug.logf("WIRE peer=%v mode=binary", conn.RemoteAddr())
	}

	for {
		_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
		req, err := wire.ReadRequest(conn)
		if err != nil {
			return
		}
		if err := processWireRequest(
			conn,
			req,
			token,
			allowPrivate,
			cache,
			tcpBuffer,
			manager,
			chunkMax,
			bufferBytes,
			chunkPollWait,
			debug,
		); err != nil {
			return
		}
	}
}

func main() {
	var (
		host           = flag.String("host", "0.0.0.0", "listen host")
		port           = flag.Int("port", 53, "listen port")
		token          = flag.String("token", "", "optional shared token")
		maxConnections = flag.Int("max-connections", 20000, "max simultaneous tunnels")
		allowPrivate   = flag.Bool("allow-private", false, "allow private/loopback targets")
		dnsCacheTTL    = flag.Duration("dns-cache-ttl", 30*time.Second, "server DNS cache TTL")
		dnsCacheSize   = flag.Int("dns-cache-size", 4096, "maximum cached DNS hostnames")
		tcpBuffer      = flag.Int("tcp-buffer", 0, "optional TCP read/write buffer bytes; 0 keeps OS autotuning")
		chunkMax       = flag.Int("chunk-max", 1048576, "maximum adaptive chunk payload bytes (32 bytes to 1 MiB)")
		chunkBuffered  = flag.Int("chunk-buffered", 256, "compatibility buffer units; 256 = about 16 MiB per active session")
		chunkPollWait  = flag.Duration("chunk-poll-wait", 200*time.Millisecond, "server long-poll wait for chunk data")
		sessionTimeout = flag.Duration("chunk-session-timeout", 2*time.Minute, "idle chunk session timeout")
		debugEnabled   = flag.Bool("debug", false, "log session/connect/errors and periodic statistics")
		debugChunks    = flag.Bool("debug-chunks", false, "log every chunk protocol record; very verbose")
		debugStats     = flag.Duration("debug-stats-interval", 5*time.Second, "periodic debug statistics interval; 0 disables")
	)
	flag.Parse()

	if *chunkMax < 32 || *chunkMax > protocol.MaxChunkPayload {
		fmt.Fprintf(os.Stderr, "--chunk-max must be between 32 and %d\n", protocol.MaxChunkPayload)
		os.Exit(2)
	}
	if *chunkBuffered < 8 {
		fmt.Fprintln(os.Stderr, "--chunk-buffered must be at least 8")
		os.Exit(2)
	}

	listenAddr := net.JoinHostPort(*host, strconv.Itoa(*port))
	ln, err := net.Listen("tcp", listenAddr)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	defer ln.Close()

	fmt.Printf("DragonTCP Go server listening on %s\n", listenAddr)
	fmt.Printf("max_connections=%d tcp_buffer=%d\n", *maxConnections, *tcpBuffer)

	slots := make(chan struct{}, *maxConnections)
	cache := newDNSCache(*dnsCacheTTL, *dnsCacheSize)
	debug := newServerDebug(*debugEnabled, *debugChunks, *debugStats)
	bufferBytes := *chunkBuffered * 65536
	if bufferBytes < 1024*1024 {
		bufferBytes = 1024 * 1024
	}
	if bufferBytes > 64*1024*1024 {
		bufferBytes = 64 * 1024 * 1024
	}
	manager := newStreamManager(*sessionTimeout, debug)
	xorManager := newChunkManager(*sessionTimeout, debug)
	fmt.Printf("binary_transport=true chunk_max=%d buffer_bytes=%d poll_wait=%s\n", *chunkMax, bufferBytes, chunkPollWait.String())
	if debug.enabled {
		fmt.Printf("debug=true debug_chunks=%t stats_interval=%s\n", debug.chunks, debug.statsEvery)
	}

	for {
		conn, err := ln.Accept()
		if err != nil {
			fmt.Fprintln(os.Stderr, "accept:", err)
			continue
		}

		select {
		case slots <- struct{}{}:
			atomic.AddInt64(&active, 1)
			if debug.enabled {
				debug.logf("ACCEPT peer=%v active_connections=%d", conn.RemoteAddr(), atomic.LoadInt64(&active))
			}
			go handle(
				conn,
				*token,
				*allowPrivate,
				cache,
				*tcpBuffer,
				slots,
				manager,
				xorManager,
				*chunkMax,
				bufferBytes,
				*chunkBuffered,
				*chunkPollWait,
				debug,
			)
		default:
			if debug.enabled {
				debug.errorf("REJECT peer=%v reason=max-connections", conn.RemoteAddr())
			}
			_ = conn.Close()
		}
	}
}
