// Command drover is the entry point for the Discord proxy CLI.
package main

import (
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"time"

	"github.com/spf13/cobra"

	"git.okcu.io/root/drover-go/internal/gui"
	"git.okcu.io/root/drover-go/internal/updater"
)

// Build-time variables, populated via -ldflags "-X main.Version=... -X main.Commit=... -X main.BuildDate=...".
var (
	Version   = "dev"
	Commit    = "dev"
	BuildDate = "dev"
)

// configPath is the path to the TOML config file, set via the --config global flag.
// Reserved for use in later phases.
var configPath string

func main() {
	// On Windows the binary is linked with -H=windowsgui so a double-click
	// doesn't flash a console window. When the user runs us from cmd or
	// PowerShell we still want stdout/stderr to land in their terminal —
	// AttachConsole(ATTACH_PARENT_PROCESS) wires that up. No-op elsewhere.
	attachToParentConsole()

	// Open a debug log file at %LOCALAPPDATA%\Drover\debug.log so we have
	// post-mortem visibility into engine startup failures even when the
	// process was launched via UAC re-elevation (which detaches stderr
	// from the parent terminal).
	setupDebugLog()

	// Detect if we need admin for the command in os.Args[1:]. If we do and
	// we're not admin, re-launch via ShellExecute("runas", ...) and exit.
	// CLI subcommands like "check", "version", "update" don't need admin
	// and will run without UAC prompt.
	needsAdm := CmdNeedsAdmin(os.Args[1:])
	isAdm := IsAdmin()
	log.Printf("main: post-console admin=%v needsAdmin=%v args=%v", isAdm, needsAdm, os.Args[1:])
	if needsAdm && !isAdm {
		log.Printf("main: invoking ReElevate")
		if err := ReElevate(os.Args[1:]); err != nil {
			log.Printf("main: ReElevate returned err: %v", err)
			fmt.Fprintf(os.Stderr, "failed to re-elevate: %v\n", err)
		} else {
			log.Printf("main: ReElevate returned ok, exiting parent")
		}
		os.Exit(0)
	}
	log.Printf("main: continuing in current process (no re-elevation needed)")

	// Inject our build version so the updater package can stamp it on the
	// User-Agent header it sends to git.okcu.io.
	updater.SetVersion(Version)

	if err := newRootCmd().Execute(); err != nil {
		// Cobra already prints the error; just exit non-zero.
		os.Exit(1)
	}
}

// setupDebugLog wires the standard `log` package to write to both stderr
// and %LOCALAPPDATA%\Drover\debug.log. Survives UAC re-launch (each
// process opens its own append-mode handle).
func setupDebugLog() {
	dir := os.Getenv("LOCALAPPDATA")
	if dir == "" {
		dir = os.Getenv("TEMP")
	}
	if dir == "" {
		return
	}
	dir = filepath.Join(dir, "Drover")
	_ = os.MkdirAll(dir, 0755)
	// Truncate on each startup — keeps the log focused on the current
	// run instead of accumulating past sessions. If you need history,
	// rotate before launch.
	f, err := os.OpenFile(filepath.Join(dir, "debug.log"), os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return
	}
	// On a UAC-elevated launch (Start-Process -Verb RunAs) we have no
	// parent console — os.Stderr points at an invalid handle. Writing
	// to it via MultiWriter fails the *entire* write, so logs silently
	// drop. Just write to the file; CLI subcommands launched from a
	// real console can grep the file.
	log.SetOutput(f)
	_ = io.Discard // keep io import used

	log.SetFlags(log.LstdFlags | log.Lmicroseconds)
	log.Printf("=== drover %s start pid=%d args=%v admin=%v at %s ===",
		Version, os.Getpid(), os.Args[1:], IsAdmin(), time.Now().Format(time.RFC3339))
}

func newRootCmd() *cobra.Command {
	root := &cobra.Command{
		Use:           "drover",
		Short:         "Discord proxy via SOCKS5 + WinDivert",
		Version:       fmt.Sprintf("%s (commit %s, built %s)", Version, Commit, BuildDate),
		SilenceUsage:  true,
		SilenceErrors: false,
		// No subcommand and no flags = end-user double-clicked the exe.
		// First do a quick silent update check (no-op if offline or
		// already current); if an update is available we apply it and
		// re-launch ourselves. Then we open the Wails-backed GUI.
		RunE: func(cmd *cobra.Command, args []string) error {
			autoUpdateOnStartup()
			return gui.Run(Version)
		},
	}

	// Custom version template: "drover-go vX.Y.Z (commit abc1234, built 2026-05-01)".
	root.SetVersionTemplate(fmt.Sprintf("drover-go v%s (commit %s, built %s)\n", Version, Commit, BuildDate))

	root.PersistentFlags().StringVar(&configPath, "config", "", "path to TOML config file (reserved)")

	root.AddCommand(newCheckCmd())
	root.AddCommand(newUpdateCmd())
	root.AddCommand(newServiceCmd())
	root.AddCommand(newGUICmd())

	return root
}

func newGUICmd() *cobra.Command {
	return &cobra.Command{
		Use:   "gui",
		Short: "Open the Drover-Go window (same as launching the exe with no args)",
		RunE: func(cmd *cobra.Command, args []string) error {
			return gui.Run(Version)
		},
	}
}

func newCheckCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "check",
		Short: "Run the 7-step proxy diagnostic",
		RunE: func(cmd *cobra.Command, args []string) error {
			fmt.Fprintln(cmd.OutOrStdout(), "TODO: 7-step diagnostic")
			return nil
		},
	}
}

func newUpdateCmd() *cobra.Command {
	var checkOnly bool
	cmd := &cobra.Command{
		Use:   "update",
		Short: "Self-update via the Forgejo Releases API",
		RunE: func(cmd *cobra.Command, args []string) error {
			ctx := cmd.Context()
			out := cmd.OutOrStdout()

			src := updater.NewForgejoSource("git.okcu.io", "root", "drover-go", "windows-amd64.exe")
			rel, hasUpdate, err := updater.CheckForUpdate(ctx, src, Version)
			if err != nil {
				return fmt.Errorf("check for update: %w", err)
			}
			if !hasUpdate {
				fmt.Fprintln(out, "No updates available")
				return nil
			}
			fmt.Fprintf(out, "Update available: %s (current v%s)\n", rel.TagName, Version)
			if checkOnly {
				return nil
			}

			fmt.Fprintln(out, "Downloading...")
			if err := updater.ApplyUpdate(ctx, rel, func(d, t int64) {
				if t > 0 {
					fmt.Fprintf(out, "\r%d/%d bytes", d, t)
				}
			}); err != nil {
				return fmt.Errorf("apply update: %w", err)
			}
			fmt.Fprintln(out, "\nUpdate applied. Restart drover.")
			return nil
		},
	}
	cmd.Flags().BoolVar(&checkOnly, "check-only", false, "only check for an update, do not apply")
	return cmd
}

func newServiceCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "service",
		Short: "Manage the drover Windows service",
	}
	for _, name := range []string{"install", "uninstall", "start", "stop"} {
		name := name
		cmd.AddCommand(&cobra.Command{
			Use:   name,
			Short: fmt.Sprintf("%s the drover Windows service", name),
			RunE: func(cmd *cobra.Command, args []string) error {
				fmt.Fprintf(cmd.OutOrStdout(), "TODO: service %s\n", name)
				return nil
			},
		})
	}
	return cmd
}
