package main

// The first breadcrumbs, written before anything else can fail.
//
// OpsLog keeps its log in the data folder, which lives beside the executable —
// so every fault that happens BEFORE that folder exists is invisible. Reported
// from a Windows 10 machine: "the process appears, no data folder is created,
// nothing starts", with no file anywhere to say why. There was nothing to read
// because the only place we write to had not been created yet.
//
// This writes to %LOCALAPPDATA%\OpsLog\startup.log instead: a folder Windows
// guarantees is writable for the user, whatever OpsLog itself was installed
// into. It records the handful of milestones between the process starting and
// the window appearing, and nothing else — it is not a second log, it is the
// answer to "it does not start".

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync/atomic"
	"time"
)

// bootLogPath is the file, or "" when even LOCALAPPDATA is unavailable.
func bootLogPath() string {
	dir := os.Getenv("LOCALAPPDATA")
	if strings.TrimSpace(dir) == "" {
		dir = os.TempDir()
	}
	if dir == "" {
		return ""
	}
	dir = filepath.Join(dir, "OpsLog")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return ""
	}
	return filepath.Join(dir, "startup.log")
}

// bootLog appends one line. Never fails loudly: it exists to explain a failure,
// so it must not become one.
func bootLog(format string, args ...any) {
	p := bootLogPath()
	if p == "" {
		return
	}
	f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
	if err != nil {
		return
	}
	defer f.Close()
	// Trimmed when it gets long. A startup log that grows for two years is a
	// file nobody opens, and the interesting launch is always the last one.
	if fi, err := f.Stat(); err == nil && fi.Size() > 256*1024 {
		f.Close()
		_ = os.Remove(p)
		f, err = os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
		if err != nil {
			return
		}
		defer f.Close()
	}
	fmt.Fprintf(f, "%s %s\n", time.Now().Format("2006-01-02 15:04:05.000"), fmt.Sprintf(format, args...))
}

// bootLogLaunch records what was launched and from where.
func bootLogLaunch() {
	exe, _ := os.Executable()
	bootLog("launch: %s %v", exe, os.Args[1:])
}

// checkDataDirWritable makes sure the folder OpsLog keeps everything in can
// actually be created and written to, and says exactly what failed if not.
//
// The data folder sits beside the executable, which is fine on a stick or in a
// home directory and refused outright under Program Files — where Windows
// silently denies the write to anything not elevated. That refusal used to end
// the launch with no window and no message.
func checkDataDirWritable() error {
	dir, err := userDataDir()
	if err != nil {
		return fmt.Errorf("cannot work out where to keep the data: %w", err)
	}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("cannot create the data folder %s: %w", dir, err)
	}
	probe := filepath.Join(dir, ".writetest")
	if err := os.WriteFile(probe, []byte("ok"), 0o644); err != nil {
		return fmt.Errorf("the data folder %s cannot be written to: %w", dir, err)
	}
	_ = os.Remove(probe)
	bootLog("data dir ok: %s", dir)
	return nil
}

// startupReached flips as soon as OnStartup runs — the first moment OpsLog's
// own code is executing inside the window's lifecycle.
var startupReached atomic.Bool

// startupStuckMessage is what an operator sees when the window never starts.
//
// It names the two causes and what to do about each, because "OpsLog is not
// responding" sends people to reinstall OpsLog, which is the one thing that
// cannot help: the part that has not started is not ours.
const startupStuckMessage = "OpsLog started but its window never opened.\n\n" +
	"This is the WebView2 runtime failing to start, and it is almost always one of two things:\n\n" +
	"• The Microsoft Edge WebView2 Runtime is missing — install it from Microsoft, then start OpsLog again.\n" +
	"• An antivirus is blocking msedgewebview2.exe — add OpsLog's folder to its exceptions.\n\n" +
	"The details are in the OpsLog folder under LOCALAPPDATA (startup.log)."

// webviewDataPath is where WebView2 keeps its profile.
//
// Named rather than left to the default, which lands in the ROAMING profile: on
// a managed account that folder can be redirected to a network share, and a
// WebView2 profile on a share that is slow or unreachable does not fail — it
// hangs, which is indistinguishable from a runtime that will not start.
//
// It also gives the folder a name someone can be told to delete: a corrupt
// profile is a real cause of this, and "delete this folder and try again" is
// only advice if the folder can be pointed at.
func webviewDataPath() string {
	dir := os.Getenv("LOCALAPPDATA")
	if strings.TrimSpace(dir) == "" {
		return "" // let Wails decide; there is nothing better to offer
	}
	p := filepath.Join(dir, "OpsLog", "WebView2")
	if err := os.MkdirAll(p, 0o755); err != nil {
		bootLog("WebView2 profile folder %s could not be created: %v — leaving it to Wails", p, err)
		return ""
	}
	return p
}

// stuckMarkerPath is written before the window is attempted and removed once it
// opens, so the NEXT launch can tell that the last one never got there.
func stuckMarkerPath() string {
	dir := os.Getenv("LOCALAPPDATA")
	if strings.TrimSpace(dir) == "" {
		dir = os.TempDir()
	}
	return filepath.Join(dir, "OpsLog", ".launching")
}

// lastLaunchHung is set at startup from the marker left by the previous run.
var lastLaunchHung bool

// noteLaunchAttempt records that a window is about to be attempted, and reports
// whether the previous attempt ever finished.
func noteLaunchAttempt() {
	p := stuckMarkerPath()
	if _, err := os.Stat(p); err == nil {
		lastLaunchHung = true
		bootLog("the previous launch never opened its window — trying again with GPU acceleration off")
	}
	_ = os.MkdirAll(filepath.Dir(p), 0o755)
	_ = os.WriteFile(p, []byte(time.Now().Format(time.RFC3339)), 0o644)
}

// clearLaunchMarker is called once the window is up: the attempt finished.
func clearLaunchMarker() { _ = os.Remove(stuckMarkerPath()) }

// fixedWebView2Path finds a FIXED-VERSION WebView2 runtime shipped beside
// OpsLog, or "" when there is none.
//
// Microsoft publishes the runtime in two forms. Evergreen installs itself into
// Windows and updates itself — the normal case, and the one this looks for
// first. FIXED VERSION is a folder of files an application carries with it and
// points at, needing no installation and no administrator: it exists precisely
// for machines where Evergreen cannot be installed, which is not a rare
// situation on a managed, offline or otherwise locked-down Windows.
//
// So: drop the extracted folder next to OpsLog.exe and it is used. Nothing to
// configure — a setting for this would be a question asked of the one operator
// least able to answer it, on a machine where nothing starts.
func fixedWebView2Path() string {
	exe, err := os.Executable()
	if err != nil {
		return ""
	}
	base := filepath.Dir(exe)
	for _, name := range []string{"WebView2", "WebView2Runtime", "webview2"} {
		dir := filepath.Join(base, name)
		if p := findWebView2Binary(dir); p != "" {
			bootLog("using the fixed-version WebView2 runtime in %s", p)
			return p
		}
	}
	return ""
}

// findWebView2Binary returns dir (or its single versioned subfolder) when it
// holds msedgewebview2.exe — the fixed-version download unpacks with the
// version as a folder level, and asking someone to flatten it by hand is one
// more step to get wrong.
func findWebView2Binary(dir string) string {
	if fileExists(filepath.Join(dir, "msedgewebview2.exe")) {
		return dir
	}
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		sub := filepath.Join(dir, e.Name())
		if fileExists(filepath.Join(sub, "msedgewebview2.exe")) {
			return sub
		}
	}
	return ""
}

// edgeBlockedMessage is shown when an Edge blocker is found in the way.
//
// It names the tool rather than the symptom: OpsLog draws its whole interface
// with the WebView2 engine, which is Edge's, so a machine where Edge is blocked
// cannot run it — and no amount of reinstalling OpsLog will change that.
const edgeBlockedMessage = "OpsLog cannot start because Edge is blocked on this PC.\n\n" +
	"A blocker tool (or a policy) is stopping msedgewebview2.exe from running. " +
	"OpsLog draws its whole interface with that engine, so it cannot open its window.\n\n" +
	"Unblock Edge — in the tool that blocked it — and start OpsLog again. " +
	"The details are in the OpsLog folder under LOCALAPPDATA (startup.log)."
