// Package applog routes the app's diagnostic output to a rotating log
// file inside the user's data dir. Wails builds with the Windows GUI
// subsystem by default — fmt.Println output is dropped, so launching
// from cmd never showed anything. The file gives us a reliable place to
// inspect what the UDP listener / cluster / CAT layer is doing.
package applog

import (
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"runtime/debug"
	"sync"
	"time"
)

var (
	mu        sync.Mutex
	file      *os.File
	path      string
	crashFile *os.File // kept open so the runtime can write a crash traceback to it
)

// Init opens (creates) the log file in dataDir. On rotation we truncate
// at startup if the file is too big; for now it's a single file, no
// rolling — the volume is low (a few KB per session).
func Init(dataDir string) (string, error) {
	mu.Lock()
	defer mu.Unlock()

	if file != nil {
		return path, nil
	}
	if dataDir == "" {
		return "", fmt.Errorf("empty data dir")
	}
	if err := os.MkdirAll(dataDir, 0o755); err != nil {
		return "", err
	}
	logPath := filepath.Join(dataDir, "opslog.log")
	// One-shot rename for users coming from the HamLog era.
	if _, err := os.Stat(logPath); os.IsNotExist(err) {
		oldLog := filepath.Join(dataDir, "hamlog.log")
		if _, err := os.Stat(oldLog); err == nil {
			_ = os.Rename(oldLog, logPath)
		}
	}
	// Truncate if the file grew past ~5MB so we don't accumulate logs
	// forever. We keep one file — simple and adequate for diagnostics.
	if fi, err := os.Stat(logPath); err == nil && fi.Size() > 5*1024*1024 {
		_ = os.Remove(logPath)
	}
	f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
	if err != nil {
		return "", err
	}
	file = f
	path = logPath

	// Capture a full traceback on a FATAL crash (a Go panic that escapes our
	// recover()s, or a runtime-fatal error like a concurrent map write, or a
	// Windows access violation routed through the Go signal handler) into a
	// dedicated file the runtime writes directly — so otherwise-silent process
	// deaths leave a stack we can read.
	if cf, cerr := os.OpenFile(filepath.Join(dataDir, "opslog-crash.log"),
		os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644); cerr == nil {
		crashFile = cf
		_ = debug.SetCrashOutput(cf, debug.CrashOptions{})
	}

	// Redirect log.Print* and the standard logger to the file too, so
	// any third-party output stays consistent.
	log.SetOutput(io.MultiWriter(file, os.Stderr))
	log.SetFlags(log.LstdFlags | log.Lmicroseconds)

	fmt.Fprintf(file, "\n────── OpsLog start %s ──────\n", time.Now().Format(time.RFC3339))
	return logPath, nil
}

// Printf writes a formatted line with a timestamp. Caller's format may
// or may not end with a newline — we strip a trailing one before adding
// our own, so log entries always look like "HH:MM:SS.mmm msg\n".
func Printf(format string, args ...any) {
	mu.Lock()
	defer mu.Unlock()
	stamp := time.Now().Format("15:04:05.000")
	msg := fmt.Sprintf(format, args...)
	for len(msg) > 0 && (msg[len(msg)-1] == '\n' || msg[len(msg)-1] == '\r') {
		msg = msg[:len(msg)-1]
	}
	if file != nil {
		fmt.Fprintf(file, "%s %s\n", stamp, msg)
	}
	// Also dump to stderr in case the binary was launched with a console
	// attached (wails dev, custom build).
	fmt.Fprintf(os.Stderr, "%s %s\n", stamp, msg)
}

// Path returns where the file is so the UI can surface it.
func Path() string {
	mu.Lock()
	defer mu.Unlock()
	return path
}

// Close flushes and releases the handle. Called from shutdown.
func Close() {
	mu.Lock()
	defer mu.Unlock()
	if file != nil {
		_ = file.Close()
		file = nil
	}
}
