// softether-go is a standalone SoftEther VPN client with built-in DHCP and reconnection.
//
// Usage:
//
//	softether-go -host <server> -user <username> -pass <password> [-hub HUB] [-port 443] [-tap tap0] [-insecure]
package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"os"
	"os/exec"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"git.sagar.ch/sagar/softether-go/pkg/client"
	"git.sagar.ch/sagar/softether-go/pkg/dhcp"
	"git.sagar.ch/sagar/softether-go/pkg/tap"
)

func main() {
	host := flag.String("host", "", "SoftEther server hostname or IP")
	port := flag.Int("port", 443, "SoftEther server port")
	hub := flag.String("hub", "DEFAULT", "Virtual hub name")
	username := flag.String("user", "", "Authentication username")
	password := flag.String("pass", "", "Authentication password")
	plainPass := flag.Bool("plain-password", false, "Send password as plaintext (AuthType 2)")
	tapName := flag.String("tap", "", "TAP interface name (empty = kernel-assigned)")
	insecure := flag.Bool("insecure", false, "Skip TLS certificate verification")
	reconnectDelay := flag.Duration("reconnect-delay", 5*time.Second, "Delay between reconnection attempts")
	acceptDefaultGW := flag.Bool("accept-default-gateway", false, "Install DHCP-provided gateway as default route")
	acceptStaticRoutes := flag.Bool("accept-static-routes", false, "Install DHCP classless static routes (option 121/249)")
	acceptDNS := flag.Bool("accept-dns", false, "Set /etc/resolv.conf from DHCP-provided DNS servers")

	flag.Parse()

	if *host == "" || *username == "" {
		fmt.Fprintf(os.Stderr, "Usage: %s -host <server> -user <username> -pass <password> [-hub HUB] [-port 443] [-tap tap0] [-insecure]\n", os.Args[0])
		os.Exit(1)
	}

	cfg := client.Config{
		Host:               *host,
		Port:               *port,
		Hub:                *hub,
		Username:           *username,
		Password:           *password,
		PlainPassword:      *plainPass,
		InsecureSkipVerify: *insecure,
	}

	// Resolve server IP for host route
	serverIP := resolveHost(*host)

	// Add host route to server via current default gateway
	cleanupRoute := addServerRoute(serverIP)
	defer cleanupRoute()

	// Open TAP device once — reused across reconnections
	dev, err := tap.Open(*tapName)
	if err != nil {
		log.Fatalf("tap open: %v", err)
	}
	defer dev.Close()
	log.Printf("tap interface: %s", dev.Name)

	if err := dev.SetUp(); err != nil {
		log.Fatalf("tap up: %v", err)
	}

	mac, err := dev.MAC()
	if err != nil {
		log.Fatalf("tap mac: %v", err)
	}
	log.Printf("tap mac: %s", mac)

	// Signal handling
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

	for {
		err := runSession(cfg, dev, mac, *acceptDefaultGW, *acceptStaticRoutes, *acceptDNS, sig)
		if err == errShutdown {
			log.Println("shutting down")
			return
		}
		log.Printf("session ended: %v", err)
		log.Printf("reconnecting in %v...", *reconnectDelay)

		select {
		case <-sig:
			log.Println("shutting down")
			return
		case <-time.After(*reconnectDelay):
		}
	}
}

var errShutdown = fmt.Errorf("shutdown requested")

// runSession connects, runs DHCP, and bridges frames until disconnection or signal.
func runSession(cfg client.Config, dev *tap.Device, mac net.HardwareAddr, acceptDefaultGW, acceptStaticRoutes, acceptDNS bool, sig chan os.Signal) error {
	log.Printf("connecting to %s:%d hub=%s user=%s", cfg.Host, cfg.Port, cfg.Hub, cfg.Username)

	sess, err := client.Connect(cfg)
	if err != nil {
		return fmt.Errorf("connect: %w", err)
	}
	log.Printf("connected: session=%s connection=%s", sess.SessionName, sess.ConnectionName)

	tunnel := client.NewTunnel(sess)
	tunnel.StartKeepalive()
	defer tunnel.Close()

	dhcpClient := dhcp.NewClient(mac)
	errCh := make(chan error, 2)

	// bridgeReady gates TAP writes — closed once DHCP completes
	bridgeReady := make(chan struct{})

	// Server → TAP: start reading immediately so DHCP responses are received
	go func() {
		for {
			frames, err := tunnel.ReadFrames()
			if err != nil {
				errCh <- fmt.Errorf("read from server: %w", err)
				return
			}
			for _, frame := range frames {
				dhcpClient.FeedFrame(frame)
				select {
				case <-bridgeReady:
					if _, err := dev.Write(frame); err != nil {
						errCh <- fmt.Errorf("write to tap: %w", err)
						return
					}
				default:
					// Drop non-DHCP frames until bridge is ready
				}
			}
		}
	}()

	// Run DHCP through the tunnel
	lease, err := runDHCP(dhcpClient, tunnel)
	if err != nil {
		return fmt.Errorf("dhcp: %w", err)
	}

	// Configure TAP interface with lease
	cleanup, err := configureTAP(dev.Name, lease, acceptDefaultGW, acceptStaticRoutes, acceptDNS)
	if err != nil {
		return fmt.Errorf("configure tap: %w", err)
	}
	defer cleanup()

	// Open the bridge — TAP writes now flow
	close(bridgeReady)

	// TAP → Server
	go func() {
		buf := make([]byte, 1600)
		for {
			n, err := dev.Read(buf)
			if err != nil {
				errCh <- fmt.Errorf("read from tap: %w", err)
				return
			}
			frame := make([]byte, n)
			copy(frame, buf[:n])
			if err := tunnel.WriteFrames([][]byte{frame}); err != nil {
				errCh <- fmt.Errorf("write to server: %w", err)
				return
			}
		}
	}()

	// Wait for error or signal
	select {
	case err := <-errCh:
		return err
	case <-sig:
		return errShutdown
	}
}

// runDHCP performs a DHCP exchange through the VPN tunnel.
func runDHCP(dc *dhcp.Client, tunnel *client.Tunnel) (*dhcp.Lease, error) {
	log.Println("starting DHCP exchange...")

	sendFrame := func(frame []byte) error {
		return tunnel.WriteFrames([][]byte{frame})
	}

	// Run DHCP with a generous timeout — server might take a moment
	lease, err := dc.Run(sendFrame, 10*time.Second)
	if err != nil {
		return nil, err
	}

	ones, _ := lease.SubnetMask.Size()
	log.Printf("dhcp lease: ip=%s/%d gw=%s dns=%v ttl=%v",
		lease.ClientIP, ones, lease.Gateway, lease.DNS, lease.LeaseTime)
	for _, r := range lease.Routes {
		log.Printf("dhcp route: %s via %s", r.Dest.String(), r.Gateway)
	}
	return lease, nil
}

// configureTAP sets the IP address, routes, and DNS on the TAP interface.
// Returns a cleanup function that undoes the changes.
func configureTAP(ifname string, lease *dhcp.Lease, acceptDefaultGW, acceptStaticRoutes, acceptDNS bool) (func(), error) {
	noop := func() {}
	ones, _ := lease.SubnetMask.Size()
	addr := fmt.Sprintf("%s/%d", lease.ClientIP, ones)

	if err := run("ip", "addr", "add", addr, "dev", ifname); err != nil {
		return noop, fmt.Errorf("ip addr add: %w", err)
	}

	if acceptStaticRoutes && len(lease.Routes) > 0 {
		for _, r := range lease.Routes {
			dest := r.Dest.String()
			if dest == "0.0.0.0/0" {
				if acceptDefaultGW {
					if err := run("ip", "route", "add", "default", "via", r.Gateway.String(), "dev", ifname, "metric", "50"); err != nil {
						log.Printf("warning: static default route: %v", err)
					}
				}
				continue
			}
			if err := run("ip", "route", "add", dest, "via", r.Gateway.String(), "dev", ifname); err != nil {
				log.Printf("warning: static route %s via %s: %v", dest, r.Gateway, err)
			}
		}
	}

	if acceptDefaultGW && lease.Gateway != nil && !hasDefaultRoute(lease.Routes) {
		if err := run("ip", "route", "add", "default", "via", lease.Gateway.String(), "dev", ifname, "metric", "50"); err != nil {
			log.Printf("warning: default route: %v", err)
		}
	}

	// DNS
	var savedResolv []byte
	if acceptDNS && len(lease.DNS) > 0 {
		savedResolv = backupResolv()
		writeResolv(lease.DNS)
	}

	cleanup := func() {
		run("ip", "addr", "flush", "dev", ifname)
		if savedResolv != nil {
			restoreResolv(savedResolv)
		}
	}
	return cleanup, nil
}

// hasDefaultRoute checks if classless static routes include a default route (0.0.0.0/0).
// Per RFC 3442, when option 121 is present it overrides option 3 (Router).
func hasDefaultRoute(routes []dhcp.Route) bool {
	for _, r := range routes {
		ones, bits := r.Dest.Mask.Size()
		if ones == 0 && bits == 32 {
			return true
		}
	}
	return false
}

const resolvPath = "/etc/resolv.conf"

func backupResolv() []byte {
	data, err := os.ReadFile(resolvPath)
	if err != nil {
		log.Printf("warning: backup resolv.conf: %v", err)
		return nil
	}
	return data
}

func writeResolv(servers []net.IP) {
	var buf strings.Builder
	buf.WriteString("# Generated by softether-go\n")
	for _, ip := range servers {
		fmt.Fprintf(&buf, "nameserver %s\n", ip)
	}
	if err := os.WriteFile(resolvPath, []byte(buf.String()), 0644); err != nil {
		log.Printf("warning: write resolv.conf: %v", err)
		return
	}
	log.Printf("dns: set nameservers %v", servers)
}

func restoreResolv(saved []byte) {
	if err := os.WriteFile(resolvPath, saved, 0644); err != nil {
		log.Printf("warning: restore resolv.conf: %v", err)
		return
	}
	log.Println("dns: restored resolv.conf")
}

// resolveHost resolves a hostname to an IPv4 address. If it's already an IP, returns it.
func resolveHost(host string) net.IP {
	if ip := net.ParseIP(host); ip != nil {
		return ip
	}
	ips, err := net.LookupIP(host)
	if err != nil {
		log.Printf("warning: could not resolve %s: %v", host, err)
		return nil
	}
	for _, ip := range ips {
		if ip.To4() != nil {
			return ip.To4()
		}
	}
	log.Printf("warning: no IPv4 address for %s", host)
	return nil
}

// addServerRoute adds a host route to the VPN server via the current default gateway.
// Returns a cleanup function that removes the route.
func addServerRoute(serverIP net.IP) func() {
	noop := func() {}
	if serverIP == nil {
		return noop
	}

	gw, dev := getDefaultGateway()
	if gw == nil {
		log.Println("warning: no default gateway found, skipping server route")
		return noop
	}

	route := serverIP.String() + "/32"
	args := []string{"route", "add", route, "via", gw.String()}
	if dev != "" {
		args = append(args, "dev", dev)
	}

	if err := run("ip", args...); err != nil {
		log.Printf("warning: add server route: %v", err)
		return noop
	}
	log.Printf("added route: %s via %s", route, gw)

	return func() {
		run("ip", "route", "del", route)
		log.Printf("removed route: %s", route)
	}
}

// getDefaultGateway parses `ip route` output to find the current default gateway.
func getDefaultGateway() (net.IP, string) {
	out, err := exec.Command("ip", "route", "show", "default").Output()
	if err != nil {
		return nil, ""
	}
	// Format: "default via 10.0.0.1 dev eth0 ..."
	fields := strings.Fields(string(out))
	var gw net.IP
	var dev string
	for i, f := range fields {
		if f == "via" && i+1 < len(fields) {
			gw = net.ParseIP(fields[i+1])
		}
		if f == "dev" && i+1 < len(fields) {
			dev = fields[i+1]
		}
	}
	return gw, dev
}

func run(name string, args ...string) error {
	cmd := exec.Command(name, args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}
