package profile

import (
	"bytes"
	"context"
	"os"
	"os/exec"
	"reflect"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
)

var runtimeDetectionCache struct {
	sync.Mutex
	path     string
	results  []runtimeconfig.DetectResult
	runtimes map[string]*runtimeconfig.Runtime
	valid    bool
}

var runtimeDetectionScan = cachedRuntimeDetection

// RuntimeDetectChange describes a change in the machine runtime capability set.
type RuntimeDetectChange struct {
	Previous []string
	Current  []string
}

// RuntimeDetectChangeHandler observes runtime capability changes after a
// detection refresh. Handlers must be non-blocking; they are invoked after the
// profile manager lock is released.
type RuntimeDetectChangeHandler func(RuntimeDetectChange)

// RuntimeManager provides access to detected runtime configurations.
type RuntimeManager struct {
	mgr *Manager
}

// Get returns the runtime with the given name.
func (rm *RuntimeManager) Get(name string) (runtimeconfig.Runtime, bool) {
	rm.mgr.mu.RLock()
	defer rm.mgr.mu.RUnlock()
	rt, ok := rm.mgr.runtimes[name]
	if !ok {
		return runtimeconfig.Runtime{}, false
	}
	return *rt, true
}

// GetOrRefresh returns the runtime with the given name, forcing one detection
// pass before failing. This keeps agent start paths resilient when the registry
// is stale or initialized before a runtime becomes visible on PATH.
func (rm *RuntimeManager) GetOrRefresh(name string) (runtimeconfig.Runtime, bool) {
	if rt, ok := rm.Get(name); ok {
		return rt, true
	}
	rm.RefreshDetect()
	return rm.Get(name)
}

// All returns all detected runtimes sorted by priority.
func (rm *RuntimeManager) All() []runtimeconfig.Runtime {
	rm.mgr.mu.RLock()
	defer rm.mgr.mu.RUnlock()
	return rm.sortedRuntimes()
}

// Detect returns the cached detection results. The cache is populated at
// startup and refreshed by explicit detection requests.
func (rm *RuntimeManager) Detect() []runtimeconfig.DetectResult {
	rm.mgr.mu.RLock()
	defer rm.mgr.mu.RUnlock()
	out := make([]runtimeconfig.DetectResult, len(rm.mgr.cachedDetectResults))
	copy(out, rm.mgr.cachedDetectResults)
	return out
}

// RefreshDetect forces a runtime scan and stores the latest results.
func (rm *RuntimeManager) RefreshDetect() []runtimeconfig.DetectResult {
	results, runtimes := runtimeDetectionScan(true)
	rm.storeDetectResults(results, runtimes, true)

	out := make([]runtimeconfig.DetectResult, len(results))
	copy(out, results)
	return out
}

// OnDetectChange registers a callback that fires when a detection refresh
// changes the set of capable runtime names. It returns an unsubscribe function.
func (rm *RuntimeManager) OnDetectChange(handler RuntimeDetectChangeHandler) func() {
	if handler == nil {
		return func() {}
	}
	rm.mgr.mu.Lock()
	if rm.mgr.runtimeDetectChangeHandlers == nil {
		rm.mgr.runtimeDetectChangeHandlers = make(map[int]RuntimeDetectChangeHandler)
	}
	rm.mgr.nextRuntimeDetectChangeHandlerID++
	id := rm.mgr.nextRuntimeDetectChangeHandlerID
	rm.mgr.runtimeDetectChangeHandlers[id] = handler
	rm.mgr.mu.Unlock()

	var once sync.Once
	return func() {
		once.Do(func() {
			rm.mgr.mu.Lock()
			delete(rm.mgr.runtimeDetectChangeHandlers, id)
			rm.mgr.mu.Unlock()
		})
	}
}

// runDetect performs the actual detection scan: it looks up each known
// runtime binary on PATH, resolves the full path, and extracts version info.
// The results are stored in the manager's cachedDetectResults and runtimes map.
func (rm *RuntimeManager) runDetect() {
	results, runtimes := runtimeDetectionScan(false)
	rm.storeDetectResults(results, runtimes, false)
}

func (rm *RuntimeManager) storeDetectResults(results []runtimeconfig.DetectResult, runtimes map[string]*runtimeconfig.Runtime, notify bool) {
	previousNames := []string(nil)
	currentNames := runtimeCapabilityNames(runtimes)
	var handlers []RuntimeDetectChangeHandler

	rm.mgr.mu.Lock()
	if notify {
		previousNames = runtimeCapabilityNames(rm.mgr.runtimes)
		if !reflect.DeepEqual(previousNames, currentNames) {
			handlers = make([]RuntimeDetectChangeHandler, 0, len(rm.mgr.runtimeDetectChangeHandlers))
			for _, handler := range rm.mgr.runtimeDetectChangeHandlers {
				handlers = append(handlers, handler)
			}
		}
	}
	rm.mgr.cachedDetectResults = results
	rm.mgr.runtimes = runtimes
	rm.mgr.mu.Unlock()

	if len(handlers) == 0 {
		return
	}
	change := RuntimeDetectChange{
		Previous: append([]string(nil), previousNames...),
		Current:  append([]string(nil), currentNames...),
	}
	for _, handler := range handlers {
		handler(change)
	}
}

func runtimeCapabilityNames(runtimes map[string]*runtimeconfig.Runtime) []string {
	names := make([]string, 0, len(runtimes))
	seen := make(map[string]struct{}, len(runtimes))
	for _, rt := range runtimes {
		if rt == nil {
			continue
		}
		name := string(runtimeconfig.NormalizeType(strings.TrimSpace(string(rt.Type))))
		if name == "" {
			name = strings.TrimSpace(rt.Name)
		}
		if name == "" {
			continue
		}
		if _, ok := seen[name]; ok {
			continue
		}
		seen[name] = struct{}{}
		names = append(names, name)
	}
	sort.Slice(names, func(i, j int) bool {
		left, leftErr := runtimeconfig.ValidateType(names[i])
		right, rightErr := runtimeconfig.ValidateType(names[j])
		if leftErr == nil && rightErr == nil {
			leftPriority := runtimeconfig.RuntimePriority(left)
			rightPriority := runtimeconfig.RuntimePriority(right)
			if leftPriority != rightPriority {
				return leftPriority < rightPriority
			}
		}
		return names[i] < names[j]
	})
	return names
}

func (rm *RuntimeManager) refreshDetect() {
	rm.RefreshDetect()
}

func cachedRuntimeDetection(force bool) ([]runtimeconfig.DetectResult, map[string]*runtimeconfig.Runtime) {
	pathEnv := os.Getenv("PATH")

	runtimeDetectionCache.Lock()
	defer runtimeDetectionCache.Unlock()

	if !force && runtimeDetectionCache.valid && runtimeDetectionCache.path == pathEnv {
		return cloneRuntimeDetection(runtimeDetectionCache.results, runtimeDetectionCache.runtimes)
	}

	results := make([]runtimeconfig.DetectResult, len(runtimeconfig.AllRuntimeTypes))
	runtimes := make(map[string]*runtimeconfig.Runtime)
	resolver := runtimeconfig.DefaultRuntimeResolver()

	for i, t := range runtimeconfig.AllRuntimeTypes {
		binary := runtimeconfig.DefaultBinary(t)
		probe := runtimeconfig.ProbeRuntimeBinary(t, binary)
		var version string
		if probe.Installed {
			version = detectRuntimeVersion(t, probe.Path)
		}
		if probe.Capable {
			rt := runtimeconfig.Runtime{Type: t, Name: string(t), Binary: binary}
			normalized, nErr := runtimeconfig.NormalizeRuntime(rt)
			if nErr == nil {
				runtimes[normalized.Name] = &normalized
			}
			if t != runtimeconfig.TypeBuiltin {
				_ = resolver.AppendBinaryDirToPATH(probe.Path)
			}
		}
		results[i] = runtimeconfig.DetectResult{
			Type:       t,
			Binary:     binary,
			Path:       probe.Path,
			Found:      probe.Capable,
			Installed:  probe.Installed,
			Capable:    probe.Capable,
			ReasonCode: probe.ReasonCode,
			Message:    probe.Message,
			Version:    version,
		}
	}

	runtimeDetectionCache.path = pathEnv
	runtimeDetectionCache.results = results
	runtimeDetectionCache.runtimes = runtimes
	runtimeDetectionCache.valid = true
	return cloneRuntimeDetection(results, runtimes)
}

func cloneRuntimeDetection(results []runtimeconfig.DetectResult, runtimes map[string]*runtimeconfig.Runtime) ([]runtimeconfig.DetectResult, map[string]*runtimeconfig.Runtime) {
	clonedResults := make([]runtimeconfig.DetectResult, len(results))
	copy(clonedResults, results)

	clonedRuntimes := make(map[string]*runtimeconfig.Runtime, len(runtimes))
	for name, runtime := range runtimes {
		if runtime == nil {
			continue
		}
		cloned := *runtime
		clonedRuntimes[name] = &cloned
	}
	return clonedResults, clonedRuntimes
}

// detectRuntimeVersion runs the runtime binary to extract its version string.
func detectRuntimeVersion(t runtimeconfig.RuntimeType, binary string) string {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	var args []string
	switch runtimeconfig.EffectiveRuntimeType(t) {
	case runtimeconfig.TypeOpenCode:
		args = []string{"--version"}
	case runtimeconfig.TypeClaudeCode:
		args = []string{"--version"}
	case runtimeconfig.TypeCodex:
		args = []string{"--version"}
	case runtimeconfig.TypeKimi:
		args = []string{"--version"}
	case runtimeconfig.TypePi:
		args = []string{"--version"}
	default:
		return ""
	}

	command, commandArgs := runtimeconfig.RuntimeCommand(t, binary, args)
	cmd := exec.CommandContext(ctx, command, commandArgs...)
	runtimeconfig.ConfigureProbeCommand(cmd)
	cmd.Env = runtimeconfig.RuntimeProcessEnv()
	var stdout bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &bytes.Buffer{}
	if err := cmd.Run(); err != nil {
		return ""
	}

	output := strings.TrimSpace(stdout.String())
	// Take only the first line in case of multi-line output.
	if idx := strings.IndexByte(output, '\n'); idx >= 0 {
		output = strings.TrimSpace(output[:idx])
	}
	return output
}

func (rm *RuntimeManager) detectRuntimes() {
	rm.runDetect()
}

// StartDetect performs the daemon startup detection pass. Runtime detection is
// intentionally not rebuilt on a timer; explicit dashboard refreshes and
// start-miss refreshes are less likely to turn transient subprocess failures
// into runtime registry eviction on the agent dispatch path.
func (rm *RuntimeManager) StartDetect() {
	rm.mgr.mu.Lock()
	if rm.mgr.runtimeDetectCancel != nil {
		rm.mgr.mu.Unlock()
		return
	}
	_, cancel := rm.mgr.startDetectContext()
	rm.mgr.runtimeDetectCancel = cancel
	rm.mgr.mu.Unlock()

	rm.runDetect()
	rm.refreshRuntimeWatches()
}

func (rm *RuntimeManager) refreshRuntimeWatches() {
	rm.mgr.mu.RLock()
	watcher := rm.mgr.watcher
	runtimes := make(map[string]*runtimeconfig.Runtime, len(rm.mgr.runtimes))
	for name, rt := range rm.mgr.runtimes {
		if rt == nil {
			continue
		}
		copy := *rt
		runtimes[name] = &copy
	}
	rm.mgr.mu.RUnlock()
	if watcher == nil {
		return
	}
	watcher.WatchRuntimeConfigs(runtimes)
}

// StopDetect cancels the periodic runtime detection goroutine.
func (rm *RuntimeManager) StopDetect() {
	rm.mgr.mu.Lock()
	defer rm.mgr.mu.Unlock()
	if rm.mgr.runtimeDetectCancel != nil {
		rm.mgr.runtimeDetectCancel()
		rm.mgr.runtimeDetectCancel = nil
	}
}

func (rm *RuntimeManager) sortedRuntimes() []runtimeconfig.Runtime {
	result := make([]runtimeconfig.Runtime, 0, len(rm.mgr.runtimes))
	for _, rt := range rm.mgr.runtimes {
		result = append(result, *rt)
	}
	sort.Slice(result, func(i, j int) bool {
		return runtimeconfig.RuntimePriority(result[i].Type) < runtimeconfig.RuntimePriority(result[j].Type)
	})
	return result
}
