// Package platformvm GPU backend selection — pure-Go shim that probes the
// lux-gpu-kernels plugin DSOs at PROCESS START in the canonical order
// (cuda → hip → metal → vulkan → webgpu) and exposes the first successful
// open as ActiveGPUBackend().
//
// Decomplected from platformvm_gpu.go / platformvm_gpu_nocgo.go: the
// probe order and package-level handle live here. The actual dlopen +
// dlsym calls live in platformvm_gpu.go (cgo build) or in the nocgo
// stub. Both files expose the same openGPUBackend signature so this
// file compiles identically in either mode.
//
// If a plugin opens cleanly, the chain routes transitions through it.
// If a probe fails (no plugin installed, missing runtime, etc.) the
// chain runs the existing pure-Go path. Every GPU launcher error makes
// the caller fall back to the Go path WITHOUT panic — strict positive
// overlay with graceful degradation.
package platformvm

import (
	"os"
	"runtime"
	"sync"
)

// =============================================================================
// Canonical plugin search paths — every backend has a name + a list of
// DSO basenames the loader tries in order. The first one that opens AND
// dlsym's all four launchers wins.
// =============================================================================

type pluginCandidate struct {
	kind      GPUBackendKind
	basenames []string
}

// candidatePlugins is the dlopen probe order: cuda → hip → metal → vulkan
// → webgpu. Each candidate lists the DSO basenames the loader probes (no
// path component). The probe layers add platform-conventional prefix +
// suffix (lib*.so / lib*.dylib).
func candidatePlugins() []pluginCandidate {
	return []pluginCandidate{
		{GPUCUDA, []string{
			"libluxgpu_backend_cuda.so",
			"libluxgpu_backend_cuda.dylib",
		}},
		{GPUHIP, []string{
			"libluxgpu_backend_hip.so",
			"libluxgpu_backend_hip.dylib",
		}},
		{GPUMetal, []string{
			"libluxgpu_backend_metal.dylib",
		}},
		{GPUVulkan, []string{
			"libluxgpu_backend_vulkan.so",
			"libluxgpu_backend_vulkan.dylib",
		}},
		{GPUWebGPU, []string{
			"libluxgpu_backend_webgpu.so",
			"libluxgpu_backend_webgpu.dylib",
		}},
	}
}

// searchPaths returns the directories the loader probes for each
// candidate basename. Order:
//
//  1. LUX_GPU_PLUGIN_DIR — explicit override.
//  2. /usr/local/lib/lux-gpu/ — prefix install.
//  3. /usr/lib/lux-gpu/.
//  4. Empty string ("" — let dlopen consult the system search path).
//
// The empty-string fallback lets a plugin in DYLD_LIBRARY_PATH /
// LD_LIBRARY_PATH still resolve, which is the standard CI override.
func searchPaths() []string {
	out := make([]string, 0, 4)
	if d := os.Getenv("LUX_GPU_PLUGIN_DIR"); d != "" {
		out = append(out, d)
	}
	out = append(out, "/usr/local/lib/lux-gpu", "/usr/lib/lux-gpu", "")
	return out
}

// =============================================================================
// Package-level active backend — set once by init(), read by every call site.
// =============================================================================

var (
	activeMu      sync.RWMutex
	activeBackend *GPUBackend // nil = no plugin loaded
)

// ActiveGPUBackend returns the currently-loaded backend, or nil if none
// satisfied the probe at init(). Callers MUST check b.IsAvailable() before
// invoking any kernel; calling on a nil receiver is safe and returns
// ErrGPUNotAvailable from every method.
func ActiveGPUBackend() *GPUBackend {
	activeMu.RLock()
	defer activeMu.RUnlock()
	return activeBackend
}

// SetActiveGPUBackend replaces the active backend. Used by tests to inject
// a mock plugin. Returns the previous backend (caller is responsible for
// closing it). Passing nil clears the slot.
func SetActiveGPUBackend(b *GPUBackend) *GPUBackend {
	activeMu.Lock()
	defer activeMu.Unlock()
	prev := activeBackend
	activeBackend = b
	return prev
}

// =============================================================================
// AutoBackend — pure-Go probe driver. Walks candidatePlugins() in order and
// returns the first one that opens cleanly. Returns nil if every probe fails
// (no plugin installed, missing runtime, etc.).
// =============================================================================

// AutoBackend probes the canonical plugin search paths in the cuda →
// hip → metal → vulkan → webgpu order and returns the first backend that
// opens AND resolves all four host launchers. Returns nil + the last
// error if no plugin satisfies the probe.
func AutoBackend() (*GPUBackend, error) {
	var lastErr error = ErrGPUNotAvailable
	for _, cand := range candidatePlugins() {
		// Skip kind-incompatible probes early. Metal only on darwin,
		// cuda/hip primarily on linux. The wrong-platform DSOs simply
		// won't be on disk — but skipping known impossibles makes the
		// fail-fast logs less noisy.
		if !kindLikelyOnHost(cand.kind) {
			continue
		}
		for _, basename := range cand.basenames {
			for _, dir := range searchPaths() {
				path := basename
				if dir != "" {
					path = dir + "/" + basename
				}
				b, err := openGPUBackend(cand.kind, path)
				if err != nil {
					lastErr = err
					continue
				}
				return b, nil
			}
		}
	}
	return nil, lastErr
}

// kindLikelyOnHost is a coarse platform filter — true if the OS could
// plausibly host this backend's runtime. The dlopen call would catch
// impossibilities anyway, but skipping known-bad combos cuts noise.
func kindLikelyOnHost(k GPUBackendKind) bool {
	switch k {
	case GPUMetal:
		return runtime.GOOS == "darwin"
	case GPUCUDA, GPUHIP:
		// CUDA/HIP are linux-first; the linux runtime may also be
		// loadable under WSL2. macOS cannot run either today.
		return runtime.GOOS != "darwin"
	case GPUVulkan, GPUWebGPU:
		// Vulkan via MoltenVK works on darwin too; WebGPU via Dawn/wgpu
		// is portable.
		return true
	default:
		return false
	}
}

// =============================================================================
// init() — runs the canonical probe order at process start. The result is
// stored in the package-level activeBackend slot. Any error is silently
// swallowed (the chain runs the Go path) so a missing plugin never
// blocks node startup.
//
// Test code can call SetActiveGPUBackend() after init() to override.
// =============================================================================

func init() {
	b, err := AutoBackend()
	if err != nil || b == nil {
		// Silent — the absence of a plugin is the common case.
		return
	}
	activeMu.Lock()
	activeBackend = b
	activeMu.Unlock()
}
