package main

import (
	"embed"
	"os"
	"strings"
	"time"

	"github.com/wailsapp/wails/v2"
	"github.com/wailsapp/wails/v2/pkg/options"
	"github.com/wailsapp/wails/v2/pkg/options/assetserver"
	wailswindows "github.com/wailsapp/wails/v2/pkg/options/windows"
)

//go:embed all:frontend/dist
var assets embed.FS

// profileArg extracts a profile name from the command line. Accepts
// "--profile NAME", "--profile=NAME", "-profile NAME", "-p NAME" so a desktop
// shortcut can launch OpsLog straight into a given profile (e.g. F4BPO / TM2Q).
func profileArg(args []string) string {
	for i := 0; i < len(args); i++ {
		a := args[i]
		switch {
		case a == "--profile" || a == "-profile" || a == "-p":
			if i+1 < len(args) {
				return strings.TrimSpace(args[i+1])
			}
		case strings.HasPrefix(a, "--profile="):
			return strings.TrimSpace(strings.TrimPrefix(a, "--profile="))
		case strings.HasPrefix(a, "-profile="):
			return strings.TrimSpace(strings.TrimPrefix(a, "-profile="))
		case strings.HasPrefix(a, "-p="):
			return strings.TrimSpace(strings.TrimPrefix(a, "-p="))
		}
	}
	return ""
}

// hasFlag reports whether flag is present in args.
func hasFlag(args []string, flag string) bool {
	for _, a := range args {
		if a == flag {
			return true
		}
	}
	return false
}

// acquireInstance grabs the single-instance mutex. On a normal launch it's a plain
// try (fail → another OpsLog is running, so exit). On a --post-update or
// --relaunch start the previous instance may still be shutting down and holding
// the mutex, so retry for a few seconds until it frees.
func acquireInstance(wait bool) bool {
	if acquireSingleInstance() {
		return true
	}
	if !wait {
		return false
	}
	deadline := time.Now().Add(20 * time.Second)
	for time.Now().Before(deadline) {
		time.Sleep(300 * time.Millisecond)
		if acquireSingleInstance() {
			return true
		}
	}
	return false
}

// processStart is stamped on the very first instruction of main, before the
// single-instance guard and before anything else runs.
//
// It exists to split a slow launch in two, because the log alone could not: the
// first line applog writes already sits inside startup(), so everything spent
// loading the binary and creating the WebView2 environment happened before the
// log begins and was invisible. An operator reporting "nothing happens for three
// seconds" was impossible to answer from a file whose first timestamp is the
// moment the app was already running.
var processStart = time.Now()

func main() {
	// Single-instance guard: if OpsLog is already running, focus that window and
	// exit instead of spawning a duplicate. A second process would open its own
	// CAT (FlexRadio) connection and Ultrabeam follow loop, and the two would
	// fight over the rig/antenna frequency — the cause of "the antenna re-tunes on
	// its own" when a windowless zombie instance was left running.
	// A --post-update relaunch (from the auto-updater) may start while the previous
	// instance is still exiting and holding the single-instance mutex — wait for it
	// to free instead of bailing out. Then clear the old exe it left behind.
	bootLogLaunch()
	postUpdate := hasFlag(os.Args[1:], "--post-update")
	// A self-relaunch (database switch) races its own parent: the new process
	// regularly wins the start against the old one's teardown, and the operator
	// got "OpsLog is already running" for following instructions. Same patience
	// as post-update.
	if !acquireInstance(postUpdate || hasFlag(os.Args[1:], "--relaunch")) {
		// SAID, not merely done. This exit is correct — a second instance would
		// fight the first over the rig — but it happened in total silence: no
		// window, no data folder, no log, which is indistinguishable from a
		// program that died on its first instruction.
		bootLog("another instance already holds the single-instance mutex - exiting")
		fatalBox("OpsLog", "OpsLog is already running.\n\nLook for its window, or for a leftover OpsLog.exe in the Task Manager, and close it before starting another.")
		return
	}
	bootLog("single-instance mutex acquired")

	// The data folder is created BESIDE the executable, so a copy dropped into
	// Program Files is refused the write by Windows — and that refusal ended the
	// launch with nothing on screen and nothing in any file, since the only log
	// OpsLog had was inside the folder it could not create.
	if err := checkDataDirWritable(); err != nil {
		bootLog("FATAL %v", err)
		fatalBox("OpsLog", "OpsLog cannot write next to its own program file.\n\n"+err.Error()+
			"\n\nMove OpsLog.exe somewhere your account can write — a folder in Documents, or the desktop — and start it again. Program Files is refused to anything not running as administrator.")
		return
	}
	if postUpdate {
		cleanupOldUpdateBinary()
	}

	// The WebView2 runtime, named before the window is attempted. Its absence is
	// the classic silent failure — the process starts, the browser environment
	// cannot be created, and on some machines that ends the launch without an
	// error anyone can see.
	if v := webView2Version(); v != "" {
		bootLog("WebView2 runtime: %s", v)
	} else {
		bootLog("WebView2 runtime: NOT FOUND — install Microsoft Edge WebView2 Runtime")
	}

	// Create an instance of the app structure
	app := NewApp()
	app.startupProfile = profileArg(os.Args[1:])

	// Restore the window's SIZE and maximised state at CREATION, from the geometry
	// saved on last close. Doing it here (not after startup) is what makes the
	// window open already at the right size instead of maximising then snapping
	// smaller. Position can't be set through options, so it is applied while the
	// window is still hidden (domReady) — invisible, so no jump. First run, or a
	// window closed maximised, keeps the historical maximised default.
	width, height := 1400, 900
	startState := options.Maximised
	if dataDir, err := userDataDir(); err == nil {
		if ws, ok := readWindowState(dataDir); ok && !ws.Maximised &&
			ws.Width >= 1100 && ws.Height >= 700 && ws.Width <= 8000 && ws.Height <= 6000 {
			width, height = ws.Width, ws.Height
			startState = options.Normal
		}
	}

	// Create application with options
	// A HANG has no error to report, and that is the shape being chased here:
	// the process stays up, the data folder is created and stays empty, and no
	// window appears — WebView2 never finishes creating its environment, and
	// nothing returns to be logged.
	//
	// So the absence of progress is what gets reported. If OnStartup has not
	// been reached a few seconds in, this says so and names the two things that
	// cause it, rather than leaving an operator watching a process that is doing
	// nothing at all.
	go func() {
		time.Sleep(12 * time.Second)
		if startupReached.Load() {
			return
		}
		bootLog("STUCK: 12 s after wails.Run and the window has not started — WebView2 never handed control back")
		fatalBox("OpsLog", startupStuckMessage)
	}()
	noteLaunchAttempt()
	bootLog("Windows %s", windowsVersion())
	bootLog("Edge policy: %s", edgePolicyNotes())
	// The blocker utilities: they leave an IFEO entry that makes Windows refuse
	// to run the WebView2 process at all. Named loudly, because a machine in
	// this state looks identical to one with a broken graphics driver.
	if blocked := edgeExecutionBlocked(); blocked != "" {
		bootLog("EDGE EXECUTION IS BLOCKED: %s — an \"Edge blocker\" tool is stopping WebView2 from starting", blocked)
		fatalBox("OpsLog", edgeBlockedMessage)
	}
	bootLog("WebView2 profile: %q", webviewDataPath())
	bootLog("entering wails.Run (window %dx%d, state %v)", width, height, startState)
	err := wails.Run(&options.App{
		Title:            "OpsLog",
		Width:            width,
		Height:           height,
		MinWidth:         1100,
		MinHeight:        700,
		WindowStartState: startState,
		// No OS title bar: it was a dead 32-pixel band above a window that already
		// has its own title strip. The app header takes over — it carries the drag
		// region, the double-click-to-maximise, and the minimise/maximise/close
		// buttons. Windows still draws the resize borders and honours Aero Snap,
		// which is why the frame is dropped rather than the whole chrome.
		Frameless: true,
		// Start hidden and reveal only once the saved position has been applied and
		// the DOM has painted (OnDomReady → domReady) — so the window appears
		// already at its final size and position, with no post-launch jump.
		StartHidden: true,
		AssetServer: &assetserver.Options{
			Assets: assets,
		},
		// The colour the window is painted before the WebView draws. Matches the
		// graphite theme's background (#16181d), which is the default on a fresh
		// install — otherwise every launch flashes white first.
		BackgroundColour: &options.RGBA{R: 0x16, G: 0x18, B: 0x1d, A: 1},
		// WEBVIEW2, pinned down.
		//
		// Its profile folder is normally chosen by Wails under the roaming
		// profile — a place that on a managed or redirected account can be slow,
		// locked or simply gone, and the failure mode is a creation that never
		// returns rather than an error. Naming it puts it on the local disk,
		// beside the startup log, where it can also be DELETED when it is the
		// thing that is broken.
		//
		// GPU acceleration is switched off only when the previous launch hung
		// (see stuckMarker): a WebView2 that cannot get on with the graphics
		// driver hangs exactly like one that cannot start at all, and this is the
		// one lever that separates the two — without costing anything on the
		// machines where it was never the problem.
		Windows: &wailswindows.Options{
			WebviewUserDataPath: webviewDataPath(),
			// A fixed-version runtime carried beside OpsLog, when one is there.
			// Empty means the installed Evergreen runtime, as before.
			WebviewBrowserPath:   fixedWebView2Path(),
			WebviewGpuIsDisabled: lastLaunchHung,
		},
		OnStartup:     app.startup,
		OnDomReady:    app.domReady,
		OnBeforeClose: app.beforeClose,
		OnShutdown:    app.shutdown,
		Bind: []interface{}{
			app,
		},
	})

	bootLog("wails.Run returned")
	if err != nil {
		// The last thing that can fail before a window exists, and the most
		// opaque of them: a missing WebView2 runtime lands here. println goes
		// nowhere in a GUI-subsystem program, so this went unseen and unlogged.
		bootLog("FATAL wails.Run: %v", err)
		fatalBox("OpsLog", "OpsLog could not open its window.\n\n"+err.Error()+
			"\n\nThis is usually a missing WebView2 runtime — install \"Microsoft Edge WebView2 Runtime\" and try again.")
	}
}
