// Package main provides C-exported functions for embedding Tailscale's
// tsnet into a Flutter iOS app via c-archive.
//
// Build: go build -buildmode=c-archive -o libtailscale.a
//
// All exported functions use C-compatible types. Error convention:
//   - Returns NULL on success, C string with error message on failure
//   - Caller must free returned strings with TailscaleFreeString()
//
// Uses userspace netstack — NO VPN entitlement needed on iOS.
package main

/*
#include <stdlib.h>
*/
import "C"
import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"os"
	"runtime"
	"sync"
	"unsafe"

	"tailscale.com/tsnet"
)

var (
	mu       sync.Mutex
	server   *tsnet.Server

	proxyMu       sync.Mutex
	proxyListener net.Listener
	proxyCancel   context.CancelFunc
)

// TailscaleStart joins the Tailnet with the given auth key.
// stateDir: app's documents directory for persisting identity.
// authKey: Tailscale auth key (reusable, tag-scoped).
// hostname: device name in the Tailnet.
// Returns NULL on success, error string on failure (caller must free).
//
//export TailscaleStart
func TailscaleStart(stateDir, authKey, hostname *C.char) *C.char {
	mu.Lock()
	defer mu.Unlock()

	if server != nil {
		return nil // already started — idempotent
	}

	// On Android, Go's net.Interfaces() uses netlink which requires
	// CAP_NET_ADMIN. Set GODEBUG to use Go's pure DNS resolver and
	// suppress network interface enumeration errors.
	if runtime.GOOS == "android" {
		os.Setenv("GODEBUG", "netdns=go")
	}

	s := &tsnet.Server{
		Dir:       C.GoString(stateDir),
		Hostname:  C.GoString(hostname),
		AuthKey:   C.GoString(authKey),
		Ephemeral: false,
	}

	if err := s.Start(); err != nil {
		return C.CString(fmt.Sprintf("tsnet start: %v", err))
	}

	server = s
	return nil
}

// TailscaleStartProxy creates a localhost TCP proxy to a Tailscale IP.
// Writes the local port to *localPort. Connect gRPC to 127.0.0.1:<localPort>.
// Returns NULL on success, error string on failure (caller must free).
//
//export TailscaleStartProxy
func TailscaleStartProxy(remoteIP *C.char, remotePort C.int, localPort *C.int) *C.char {
	mu.Lock()
	s := server
	mu.Unlock()

	if s == nil {
		return C.CString("not started, call TailscaleStart() first")
	}

	proxyMu.Lock()
	defer proxyMu.Unlock()

	stopProxyLocked()

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return C.CString(fmt.Sprintf("listen: %v", err))
	}

	ctx, cancel := context.WithCancel(context.Background())
	proxyListener = ln
	proxyCancel = cancel

	remoteAddr := fmt.Sprintf("%s:%d", C.GoString(remoteIP), int(remotePort))
	port := ln.Addr().(*net.TCPAddr).Port
	*localPort = C.int(port)

	go acceptLoop(ctx, s, ln, remoteAddr)

	return nil
}

func acceptLoop(ctx context.Context, s *tsnet.Server, ln net.Listener, remoteAddr string) {
	for {
		conn, err := ln.Accept()
		if err != nil {
			select {
			case <-ctx.Done():
				return
			default:
				continue
			}
		}
		go handleConn(ctx, s, conn, remoteAddr)
	}
}

func handleConn(ctx context.Context, s *tsnet.Server, local net.Conn, remoteAddr string) {
	defer local.Close()

	remote, err := s.Dial(ctx, "tcp", remoteAddr)
	if err != nil {
		return
	}
	defer remote.Close()

	done := make(chan struct{}, 2)
	go func() {
		io.Copy(remote, local)
		done <- struct{}{}
	}()
	go func() {
		io.Copy(local, remote)
		done <- struct{}{}
	}()

	select {
	case <-done:
	case <-ctx.Done():
	}
}

// TailscaleStopProxy stops the local TCP proxy.
// Returns NULL on success, error string on failure (caller must free).
//
//export TailscaleStopProxy
func TailscaleStopProxy() *C.char {
	proxyMu.Lock()
	defer proxyMu.Unlock()
	stopProxyLocked()
	return nil
}

func stopProxyLocked() {
	if proxyCancel != nil {
		proxyCancel()
		proxyCancel = nil
	}
	if proxyListener != nil {
		proxyListener.Close()
		proxyListener = nil
	}
}

// TailscaleStop disconnects from the Tailnet and cleans up.
// Returns NULL on success, error string on failure (caller must free).
//
//export TailscaleStop
func TailscaleStop() *C.char {
	TailscaleStopProxy()

	mu.Lock()
	defer mu.Unlock()

	if server == nil {
		return nil
	}

	err := server.Close()
	server = nil
	if err != nil {
		return C.CString(fmt.Sprintf("close: %v", err))
	}
	return nil
}

// TailscaleStatus returns the current Tailscale status as a JSON string.
// On success, returns the JSON string (caller must free). Sets *errOut to NULL.
// On failure, returns NULL and sets *errOut to error string (caller must free).
//
//export TailscaleStatus
func TailscaleStatus(errOut **C.char) *C.char {
	mu.Lock()
	s := server
	mu.Unlock()

	if s == nil {
		*errOut = nil
		return C.CString("{}")
	}

	lc, err := s.LocalClient()
	if err != nil {
		*errOut = C.CString(fmt.Sprintf("local client: %v", err))
		return nil
	}

	st, err := lc.Status(context.Background())
	if err != nil {
		*errOut = C.CString(fmt.Sprintf("status: %v", err))
		return nil
	}

	b, err := json.Marshal(st)
	if err != nil {
		*errOut = C.CString(fmt.Sprintf("marshal: %v", err))
		return nil
	}

	*errOut = nil
	return C.CString(string(b))
}

// TailscaleIP returns this node's Tailscale IPv4 address.
// On success, returns the IP string (caller must free). Sets *errOut to NULL.
// On failure, returns NULL and sets *errOut to error string (caller must free).
//
//export TailscaleIP
func TailscaleIP(errOut **C.char) *C.char {
	mu.Lock()
	s := server
	mu.Unlock()

	if s == nil {
		*errOut = C.CString("not started")
		return nil
	}

	lc, err := s.LocalClient()
	if err != nil {
		*errOut = C.CString(fmt.Sprintf("local client: %v", err))
		return nil
	}

	st, err := lc.Status(context.Background())
	if err != nil {
		*errOut = C.CString(fmt.Sprintf("status: %v", err))
		return nil
	}

	if st.Self == nil || len(st.Self.TailscaleIPs) == 0 {
		*errOut = C.CString("no tailscale IP assigned yet")
		return nil
	}

	for _, ip := range st.Self.TailscaleIPs {
		if ip.Is4() {
			*errOut = nil
			return C.CString(ip.String())
		}
	}

	*errOut = nil
	return C.CString(st.Self.TailscaleIPs[0].String())
}

// TailscaleFreeString frees a string returned by other Tailscale functions.
//
//export TailscaleFreeString
func TailscaleFreeString(s *C.char) {
	if s != nil {
		C.free(unsafe.Pointer(s))
	}
}

func main() {} // required for c-archive, unused
