package main

import (
	"context"
	"fmt"
	"io"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/xvzc/spoofdpi/internal/cache"
	"github.com/xvzc/spoofdpi/internal/config"
	"github.com/xvzc/spoofdpi/internal/desync"
	"github.com/xvzc/spoofdpi/internal/dns"
	"github.com/xvzc/spoofdpi/internal/logging"
	"github.com/xvzc/spoofdpi/internal/netutil"
	"github.com/xvzc/spoofdpi/internal/packet"
	"github.com/xvzc/spoofdpi/internal/rule"
	"github.com/xvzc/spoofdpi/internal/server"
	"github.com/xvzc/spoofdpi/internal/server/http"
	"github.com/xvzc/spoofdpi/internal/server/socks5"
	"github.com/xvzc/spoofdpi/internal/server/tun"
	"github.com/xvzc/spoofdpi/internal/session"
)

// Version and commit are set at build time.
var (
	version = "dev"
	commit  = "unknown"
	build   = "unknown"
)

type SwitchableWriter struct {
	// target is a pointer to an interface, or just the interface itself.
	// We use a pointer to the interface for direct updates.
	target io.Writer
}

func (sw *SwitchableWriter) SetWriter(w io.Writer) {
	// Update the underlying value that the pointer references
	sw.target = w
}

func (sw *SwitchableWriter) Write(p []byte) (n int, err error) {
	// Access the current writer through the pointer
	return sw.target.Write(p)
}

type DelayedWriter struct {
	writer io.Writer
	delay  time.Duration
}

// DelayedWriter is stateless, so value receiver is technically fine,
// but pointer receiver is preferred for consistency in Go.
func (dw *DelayedWriter) Write(p []byte) (n int, err error) {
	if dw.delay > 0 {
		time.Sleep(dw.delay)
	}
	return dw.writer.Write(p)
}

func main() {
	cmd := config.CreateCommand(runApp, version, commit, build)
	if err := cmd.Run(context.Background(), os.Args); err != nil {
		fmt.Println("application failed to start")
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func runApp(mainctx context.Context, configDir string, cfg *config.Config) (err error) {
	appctx, cancel := signal.NotifyContext(
		session.WithNewTraceID(mainctx),
		syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP,
	)
	defer cancel()

	var writer io.Writer
	// Channel to capture critical TUI execution failures
	if !cfg.Startup.App.NoTUI {
		if err := startTUI(cancel); err != nil {
			return fmt.Errorf("failed to start tui: %w", err)
		}
		writer = TUIWriter{}
	} else {
		writer = os.Stdout
	}

	dw := &DelayedWriter{
		writer: writer,
		delay:  29 * time.Millisecond,
	}
	sw := &SwitchableWriter{target: dw}

	logging.SetGlobalLogger(appctx, cfg.Startup.App.LogLevel, sw)
	logger := log.Logger.With().Ctx(appctx).Logger()

	// In TUI mode the alt-screen has already been claimed by startTUI.
	// Letting a setup error bubble up via `return err` would land in
	// main's fmt.Println + os.Exit(1) path, which tears down the alt
	// screen before the user can read what went wrong.
	//
	// Catch the error here instead: log it through the configured logger
	// so it shows up in the TUI, park on appctx until the user dismisses
	// with Ctrl+C, and clear `err` so main exits cleanly.
	//
	// Headless mode keeps the original behavior — the error propagates
	// to main, which prints "application failed to start" to stderr and
	// exits with status 1.
	defer func() {
		if err == nil || cfg.Startup.App.NoTUI {
			return
		}
		logger.Error().Err(err).Msg("application failed to start")
		<-appctx.Done()
		err = nil
	}()

	logger.Info().Str("version", version).Msg("spoofdpi")
	if configDir != "" {
		logger.Info().
			Str("dir", configDir).
			Msgf("loaded config file")
	} else {
		logger.Warn().
			Msg("config file not found")
		logger.Warn().
			Msg(" please try 'sudo -E spoofdpi' if you expect a configuration to be loaded")
	}

	for _, m := range config.WarnMsgs {
		logger.Warn().Msg(m)
	}

	logger.Info().Str("mode", cfg.Startup.App.Mode.String()).Msgf("app")

	srv, err := createServer(appctx, logger, cfg)
	if err != nil {
		return fmt.Errorf("create server: %w", err)
	}

	logger.Info().Msg("https info")
	logger.Info().
		Str("split-mode", cfg.Runtime.HTTPS.SplitMode.String()).
		Uint8("chunk-size", uint8(cfg.Runtime.HTTPS.ChunkSize)).
		Bool("disorder", cfg.Runtime.HTTPS.Disorder).
		Msg(" split")

	logger.Info().
		Uint8("count", uint8(cfg.Runtime.HTTPS.FakeCount)).
		Msg(" fake")

	if cfg.Runtime.Conn.DNSTimeout > 0 {
		logger.Info().
			Str("value", fmt.Sprintf("%dms", cfg.Runtime.Conn.DNSTimeout.Milliseconds())).
			Msgf("dns connection timeout")
	}
	if cfg.Runtime.Conn.TCPTimeout > 0 {
		logger.Info().
			Str("value", fmt.Sprintf("%dms", cfg.Runtime.Conn.TCPTimeout.Milliseconds())).
			Msgf("tcp connection timeout")
	}
	if cfg.Runtime.Conn.UDPIdleTimeout > 0 {
		logger.Info().
			Str("value", fmt.Sprintf("%dms", cfg.Runtime.Conn.UDPIdleTimeout.Milliseconds())).
			Msgf("udp idle timeout")
	}

	time.Sleep(300 * time.Millisecond)
	if err := srv.ListenAndServe(appctx); err != nil {
		return fmt.Errorf("listen and serve: %w", err)
	}
	logger.Info().Msgf("server started on %s", srv.Addr())
	if cfg.Startup.App.AutoConfigureNetwork {
		if stateFile, acErr := srv.SetupNetworkJobs(appctx); acErr != nil {
			// Non-fatal: server is running, just couldn't auto-set
			// system proxy. Log and continue rather than tearing down.
			logger.Error().Err(acErr).Msg("failed to set system network config")
		} else if acErr := netutil.ApplyJobs(logger, stateFile); acErr != nil {
			logger.Error().Err(acErr).Msg("failed to apply network config")
		} else {
			defer netutil.ResetJobs(logger, stateFile)
		}
	}

	sw.SetWriter(writer)

	<-appctx.Done()

	return nil
}

func setupPcapIO(
	logger zerolog.Logger,
	route *packet.Route,
	cfg *config.Config,
) (tcpSniffer, udpSniffer packet.Sniffer, tcpWriter, udpWriter packet.Writer, err error) {
	if !cfg.NeedsPcap() {
		return
	}

	pktLogger := logging.WithScope(logger, "pkt")
	hopCache := cache.NewLRUCache[netutil.IPKey, uint8](16, 8192, nil)

	logger.Info().Msg("network info")
	logger.Info().
		Str("name", route.Iface.Name).
		Str("mac", route.Iface.HardwareAddr.String()).
		Msg(" interface")
	logger.Info().
		Str("mac", route.GatewayMAC.String()).
		Msg(" gateway")

	if cfg.NeedsPcapTCP() {
		tcpHandle, hErr := packet.NewHandle(&route.Iface)
		if hErr != nil {
			err = fmt.Errorf("tcp pcap handle on %s: %w", route.Iface.Name, hErr)
			return
		}
		tcpSniffer = packet.NewTCPSniffer(pktLogger, tcpHandle, hopCache, &cfg.Runtime)
		tcpWriter = packet.NewTCPWriter(pktLogger, tcpHandle, route)
		tcpSniffer.StartCapturing()
	}

	if cfg.NeedsPcapUDP() {
		udpHandle, hErr := packet.NewHandle(&route.Iface)
		if hErr != nil {
			err = fmt.Errorf("udp pcap handle on %s: %w", route.Iface.Name, hErr)
			return
		}
		udpSniffer = packet.NewUDPSniffer(pktLogger, udpHandle, hopCache, &cfg.Runtime)
		udpWriter = packet.NewUDPWriter(pktLogger, udpHandle, route)
		udpSniffer.StartCapturing()
	}

	return
}

func createServer(
	appctx context.Context,
	logger zerolog.Logger,
	cfg *config.Config,
) (server.Server, error) {
	// --- Rule set ---
	ruleSet := rule.NewRuleSet()
	for _, r := range cfg.Startup.Rules {
		if err := ruleSet.Add(&r); err != nil {
			return nil, err
		}
	}

	// --- DNS resolver ---
	resolver := dns.NewClient(
		logging.WithScope(logger, "dns"),
		&cfg.Runtime,
	)

	// Clean up stale network state before route discovery so a crashed TUN
	// session does not leave the routing table in a state that obscures the
	// real default route.
	netutil.ResetJobs(logger, tun.StateFile)
	netutil.ResetJobs(logger, http.StateFile)
	netutil.ResetJobs(logger, socks5.StateFile)

	discoverCtx, discoverCancel := context.WithTimeout(appctx, 10*time.Second)
	defaultRoute, err := packet.DiscoverRoute(discoverCtx, cfg)
	discoverCancel()
	if err != nil {
		return nil, fmt.Errorf("failed to find default route: %w", err)
	}

	tcpSniffer, udpSniffer, tcpWriter, udpWriter, err := setupPcapIO(
		logger,
		defaultRoute,
		cfg,
	)
	if err != nil {
		return nil, err
	}

	tlsDesyncer := desync.NewTLSDesyncer(
		logging.WithScope(logger, "dsn"),
		tcpWriter,
		tcpSniffer,
	)
	udpDesyncer := desync.NewUDPDesyncer(
		logging.WithScope(logger, "dsn"),
		udpWriter,
		udpSniffer,
	)

	switch cfg.Startup.App.Mode {
	case config.AppModeHTTP:
		return http.NewHTTPProxy(
			logging.WithScope(logger, "srv"),
			http.NewHTTPHandler(logging.WithScope(logger, "hnd")),
			http.NewHTTPSHandler(logging.WithScope(logger, "hnd"), tlsDesyncer, &cfg.Runtime),
			http.NewHTTPSystemNetwork(logging.WithScope(logger, "sys"), defaultRoute),
			resolver,
			ruleSet,
			cfg.Startup.App.ListenAddr,
			&cfg.Runtime,
		), nil

	case config.AppModeSOCKS5:
		udpPool := netutil.NewConnRegistry[netutil.NATKey](4096, 60*time.Second)
		udpPool.RunCleanupLoop(appctx)
		return socks5.NewSOCKS5Proxy(
			logging.WithScope(logger, "srv"),
			socks5.NewConnectHandler(
				logging.WithScope(logger, "hnd"),
				tlsDesyncer,
				cfg.Startup.App.ListenAddr,
				&cfg.Runtime,
			),
			socks5.NewBindHandler(logging.WithScope(logger, "hnd")),
			socks5.NewUdpAssociateHandler(
				logging.WithScope(logger, "hnd"),
				udpPool,
				udpDesyncer,
				&cfg.Runtime,
			),
			socks5.NewSOCKS5SystemNetwork(logging.WithScope(logger, "sys"), defaultRoute),
			resolver,
			ruleSet,
			cfg.Startup.App.ListenAddr,
			&cfg.Runtime,
		), nil

	case config.AppModeTUN:
		logger.Info().
			Str("interface", defaultRoute.Iface.Name).
			Str("gateway", defaultRoute.Gateway.String()).
			Msg("determined default interface and gateway")
		sysNet, err := tun.NewTUNSystemNetwork(
			logging.WithScope(logger, "sys"),
			defaultRoute,
			cfg.Startup.App.FreebsdFIB,
		)
		if err != nil {
			return nil, fmt.Errorf("failed to create sysnet: %w", err)
		}
		return tun.NewTUNServer(
			logging.WithScope(logger, "srv"),
			tun.NewTCPHandler(
				logging.WithScope(logger, "hnd"),
				tlsDesyncer,
				ruleSet,
				&cfg.Runtime,
			),
			tun.NewUDPHandler(
				logging.WithScope(logger, "hnd"),
				udpDesyncer,
				ruleSet,
				&cfg.Runtime,
			),
			sysNet,
		), nil

	default:
		return nil, fmt.Errorf("unknown server mode: %s", cfg.Startup.App.Mode)
	}
}
