package main

import (
	"fmt"
	"net"
	"strings"
	"sync"
	"time"

	"dragontcp/internal/xorchunk"
)

// DragonTCP speaks two wires that are not interchangeable:
//
//	b — compact binary records (29/5-byte headers, SHA-256 keystream mask)
//	x — legacy UP/OK framing with XOR 0xAD over ASCII chunk commands
//
// Networks differ in which they pass, so the client can be pinned to either or
// left on auto, which decides by actually fetching a URL through each wire and
// keeping the first that answers.
const (
	WireBinary = "b"
	WireXOR    = "x"
	WireAuto   = "auto"
)

// probeTarget is fetched through a candidate wire to decide whether it works.
// A plain HTTP host is used deliberately: it exercises OPEN, upload and
// download in one go, and a valid status line proves bytes survived intact.
const (
	probeHost    = "ip.dr2.site"
	probePort    = 80
	probeTimeout = 8 * time.Second
)

type wireSelector struct {
	mu         sync.Mutex
	configured string // b, x or auto
	resolved   string // b or x once decided
	serverAddr string
	token      string
	binOpts    chunkClientOptions
	xorOpts    xorchunk.Options
}

func newWireSelector(configured, serverAddr, token string, binOpts chunkClientOptions, xorOpts xorchunk.Options) *wireSelector {
	s := &wireSelector{
		configured: configured,
		serverAddr: serverAddr,
		token:      token,
		binOpts:    binOpts,
		xorOpts:    xorOpts,
	}
	if configured != WireAuto {
		s.resolved = configured
	}
	return s
}

// dial opens a tunnel over the active wire, resolving the wire first if needed.
func (s *wireSelector) dial(host string, port int) (net.Conn, error) {
	mode := s.mode()
	if mode == WireXOR {
		return xorchunk.Open(s.serverAddr, s.token, host, port, s.xorOpts)
	}
	return openChunkTunnel(s.serverAddr, s.token, host, port, s.binOpts)
}

// mode returns the wire to use, running detection once if configured as auto.
// Detection failure is not cached, so a client that starts before the network
// is usable retries on the next connection instead of latching a bad guess.
func (s *wireSelector) mode() string {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.resolved != "" {
		return s.resolved
	}
	if picked, ok := s.detectLocked(); ok {
		s.resolved = picked
		return picked
	}
	// Undecided: use the binary wire for this attempt without caching it.
	return WireBinary
}

func (s *wireSelector) detectLocked() (string, bool) {
	for _, candidate := range []string{WireBinary, WireXOR} {
		if s.probe(candidate) {
			fmt.Printf("wire probe: %s selected via %s\n", candidate, probeHost)
			return candidate, true
		}
		fmt.Printf("wire probe: %s failed\n", candidate)
	}
	fmt.Printf("wire probe: neither wire reached %s; retrying later\n", probeHost)
	return "", false
}

// probe fetches probeHost through one wire and reports whether a well-formed
// HTTP status line came back.
func (s *wireSelector) probe(mode string) bool {
	type result struct{ ok bool }
	done := make(chan result, 1)

	go func() {
		var (
			conn net.Conn
			err  error
		)
		if mode == WireXOR {
			conn, err = xorchunk.Open(s.serverAddr, s.token, probeHost, probePort, s.xorOpts)
		} else {
			conn, err = openChunkTunnel(s.serverAddr, s.token, probeHost, probePort, s.binOpts)
		}
		if err != nil {
			done <- result{false}
			return
		}
		defer conn.Close()

		request := "GET / HTTP/1.1\r\nHost: " + probeHost + "\r\nUser-Agent: dragontcp\r\nConnection: close\r\n\r\n"
		if _, err := conn.Write([]byte(request)); err != nil {
			done <- result{false}
			return
		}
		buf := make([]byte, 64)
		n, err := conn.Read(buf)
		if n <= 0 || (err != nil && n == 0) {
			done <- result{false}
			return
		}
		done <- result{strings.HasPrefix(string(buf[:n]), "HTTP/")}
	}()

	select {
	case r := <-done:
		return r.ok
	case <-time.After(probeTimeout):
		// The tunnel goroutine is left to unwind on its own; the wire simply
		// did not answer in time, which is all the caller needs to know.
		return false
	}
}
