package cat

import (
	"log"
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"
)

// LogSink, when set by the host app at startup, receives every CAT debug
// line so they land in the unified app log (opslog.log) alongside the rest
// of OpsLog's diagnostics. Until it's wired we fall back to a dedicated
// cat.log file so early-startup lines aren't lost.
var LogSink func(format string, args ...any)

// catLogger forwards Printf either to the host LogSink (preferred) or to a
// local file/stderr fallback. Keeps the call sites (debugLog.Printf(...))
// unchanged.
type catLogger struct {
	once     sync.Once
	fallback *log.Logger
}

func (c *catLogger) Printf(format string, args ...any) {
	if LogSink != nil {
		LogSink("cat: "+format, args...)
		return
	}
	// Only now, on a line that genuinely has nowhere else to go, is the fallback
	// file opened. It used to be created at package init, so every installation
	// grew an %APPDATA%\OpsLog\cat.log that nothing ever wrote to once the app
	// wired LogSink — a decoy for anyone told to "check the CAT log".
	c.once.Do(func() { c.fallback = openFallbackLog() })
	if c.fallback != nil {
		c.fallback.Printf(format, args...)
	}
}

// debugLog writes CAT debug events so users can diagnose mode/freq mismatches
// without rebuilding with a console. Once LogSink is set, lines flow into the
// main opslog.log.
var debugLog = &catLogger{}

func openFallbackLog() *log.Logger {
	base, err := os.UserConfigDir()
	if err != nil {
		return log.Default()
	}
	dir := filepath.Join(base, "OpsLog")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return log.Default()
	}
	f, err := os.OpenFile(filepath.Join(dir, "cat.log"),
		os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
	if err != nil {
		return log.Default()
	}
	return log.New(f, "", log.LstdFlags|log.Lmicroseconds)
}

// DebugLogPath returns where the fallback cat.log lives, or "" when CAT lines are
// going to the app log instead — which is the normal case, and the answer callers
// actually need. It previously returned the path unconditionally, so the one place
// that displayed it sent operators to an empty file in %APPDATA% while their CAT
// diagnostics sat in data\opslog.log.
func DebugLogPath() string {
	if LogSink != nil {
		return "" // lines go to the unified app log; there is no separate cat.log
	}
	base, err := os.UserConfigDir()
	if err != nil {
		return ""
	}
	return filepath.Join(base, "OpsLog", "cat.log")
}

// ── CAT wire trace ─────────────────────────────────────────
//
// Opt-in, off by default. Turning it on logs every frame sent and received —
// CI-V as hex, the ASCII backends (Kenwood, Yaesu) as the text they exchange.
//
// It exists because a fault nobody could reason about — "the keyer sends one
// element then stalls" — was settled in one line the moment the actual bytes
// were visible, and the CAT links have since produced the same class of report:
// a MOX button that sets split instead of transmitting, and a split the Kenwood
// backend does not recognise on a rig whose frequency it reads perfectly.
//
// Both are questions about what the RIG actually said. Guessing at that from a
// protocol document has already cost this project a wrong fix that broke the
// case which already worked.
var catTrace atomic.Bool

// SetCIVTrace turns the CAT wire trace on or off. Named for the CI-V link it
// was written for; it now covers the text backends too.
func SetCIVTrace(on bool) {
	catTrace.Store(on)
	if on {
		debugLog.Printf("wire trace ON — every CAT frame to and from the rig is logged")
	} else {
		debugLog.Printf("wire trace OFF")
	}
}

// CIVTraceEnabled reports whether the trace is running (for the settings UI).
func CIVTraceEnabled() bool { return catTrace.Load() }

// traceCIV logs one CI-V frame. dir is "TX" (to the rig) or "RX" (from it).
func traceCIV(dir string, b []byte) {
	if !catTrace.Load() || len(b) == 0 {
		return
	}
	debugLog.Printf("civ %s % X", dir, b)
}

// traceText logs one ASCII exchange, for the Kenwood and Yaesu backends.
//
// The text is QUOTED. A truncated or empty reply must be visible as such
// rather than vanish into the line: "" and ";" look identical unquoted, and
// telling them apart is the whole question when a rig answers a command it
// does not really support.
func traceText(backend, dir, s string) {
	if !catTrace.Load() || s == "" {
		return
	}
	debugLog.Printf("%s %s %q", backend, dir, s)
}
