package runtimeconfig

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"time"
)

const managedOpenCodePathEnv = "AGENTTEAM_MANAGED_OPENCODE_PATH"

// ManagedOpenCodeVersion is optionally set by release builds to the OpenCode
// runtime version bundled with the AgentTeam Desktop artifact. Empty means the
// release did not declare a version and the probe skips version matching.
var ManagedOpenCodeVersion string

var managedOpenCodeExecutable = os.Executable

// PrepareManagedOpenCodeEnv creates one agent's persistent, AgentTeam-owned XDG
// directories and returns the mandatory environment for builtin OpenCode.
// Data, state, and cache remain isolated from user-installed OpenCode runtimes;
// only network-dependent startup work is otherwise disabled.
func PrepareManagedOpenCodeEnv(modelsPath, runtimeStateDir string) (map[string]string, error) {
	modelsPath = strings.TrimSpace(modelsPath)
	env := map[string]string{
		"OPENCODE_DISABLE_AUTOUPDATE":         "1",
		"OPENCODE_DISABLE_CLAUDE_CODE_PROMPT": "1",
		"OPENCODE_DISABLE_LSP_DOWNLOAD":       "1",
		"OPENCODE_DISABLE_MODELS_FETCH":       "1",
	}
	if modelsPath != "" {
		env["OPENCODE_MODELS_PATH"] = modelsPath
	}

	runtimeStateDir = strings.TrimSpace(runtimeStateDir)
	if runtimeStateDir == "" {
		return nil, fmt.Errorf("managed opencode storage requires a runtime state directory")
	}

	root := filepath.Join(runtimeStateDir, "opencode")
	storageEnv := map[string]string{
		"XDG_DATA_HOME":  filepath.Join(root, "data"),
		"XDG_STATE_HOME": filepath.Join(root, "state"),
		"XDG_CACHE_HOME": filepath.Join(root, "cache"),
	}
	for _, key := range []string{"XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"} {
		dir := storageEnv[key]
		if err := os.MkdirAll(dir, 0o700); err != nil {
			return nil, fmt.Errorf("create managed opencode %s directory %s: %w", key, dir, err)
		}
		info, err := os.Stat(dir)
		if err != nil {
			return nil, fmt.Errorf("inspect managed opencode %s directory %s: %w", key, dir, err)
		}
		if !info.IsDir() {
			return nil, fmt.Errorf("managed opencode %s path %s is not a directory", key, dir)
		}
		if err := os.Chmod(dir, 0o700); err != nil {
			return nil, fmt.Errorf("secure managed opencode %s directory %s: %w", key, dir, err)
		}
		env[key] = dir
	}
	return env, nil
}

// ManagedOpenCodeConfigDir returns the workdir-local OpenCode config directory
// owned by AgentTeam for the builtin runtime. External OpenCode keeps using its
// legacy workdir `.opencode` directory and must not call this helper.
func ManagedOpenCodeConfigDir(workDir string) string {
	workDir = strings.TrimSpace(workDir)
	if workDir == "" {
		return ""
	}
	return filepath.Join(managedOpenCodeWorkRoot(workDir), "config")
}

func managedOpenCodeWorkRoot(workDir string) string {
	return filepath.Join(workDir, ".opencode", "agentteam-managed")
}

// ManagedOpenCodeBinaryPath returns the AgentTeam-managed OpenCode executable path.
func ManagedOpenCodeBinaryPath() (string, error) {
	probe := ProbeManagedOpenCode()
	if probe.Capable {
		return probe.Path, nil
	}
	return probe.Path, probe.Err
}

// ManagedOpenCodeCandidatePaths returns the release-layout paths where AgentTeam
// Desktop expects the bundled OpenCode executable.
func ManagedOpenCodeCandidatePaths() []string {
	exe, err := managedOpenCodeExecutable()
	if err == nil {
		if resourcesDir, ok := electronResourcesDir(exe, runtime.GOOS); ok {
			return []string{
				filepath.Join(resourcesDir, "runtimes", "opencode", managedOpenCodeBinaryName(runtime.GOOS)),
			}
		}
	}
	if override := strings.TrimSpace(os.Getenv(managedOpenCodePathEnv)); override != "" {
		return []string{override}
	}
	if err != nil {
		return nil
	}
	exeDir := filepath.Dir(exe)
	binary := managedOpenCodeBinaryName(runtime.GOOS)

	candidates := []string{
		filepath.Join(exeDir, "runtimes", "opencode", binary),
		filepath.Join(exeDir, "runtimes", binary),
		filepath.Join(exeDir, binary),
	}
	if filepath.Base(exeDir) == "MacOS" && filepath.Base(filepath.Dir(exeDir)) == "Contents" {
		resourcesDir := filepath.Join(filepath.Dir(exeDir), "Resources")
		candidates = append([]string{
			filepath.Join(resourcesDir, "runtimes", "opencode", binary),
			filepath.Join(resourcesDir, "runtimes", binary),
			filepath.Join(resourcesDir, binary),
		}, candidates...)
	}
	return dedupeRuntimePaths(candidates)
}

func electronResourcesDir(executable, goos string) (string, bool) {
	executable = filepath.Clean(strings.TrimSpace(executable))
	if executable == "." || executable == "" {
		return "", false
	}
	exeDir := filepath.Dir(executable)
	if filepath.Base(exeDir) != "bin" {
		return "", false
	}
	if filepath.Base(executable) != managedAgentTeamBinaryName(goos) {
		return "", false
	}
	resourcesDir := filepath.Dir(exeDir)
	if !strings.EqualFold(filepath.Base(resourcesDir), "resources") {
		return "", false
	}
	return resourcesDir, true
}

// ProbeManagedOpenCode verifies the OpenCode executable bundled with AgentTeam
// Desktop. Missing or unusable managed artifacts are reported as incomplete
// AgentTeam Desktop installs instead of falling back to external package managers.
func ProbeManagedOpenCode() RuntimeProbeResult {
	candidates := ManagedOpenCodeCandidatePaths()
	if len(candidates) == 0 {
		err := fmt.Errorf("AgentTeam Desktop installation incomplete: bundled OpenCode runtime location is unavailable")
		return RuntimeProbeResult{
			Installed:  false,
			Capable:    false,
			ReasonCode: RuntimeProbeReasonAgentTeamInstallIncomplete,
			Message:    err.Error(),
			Err:        err,
		}
	}

	resolver := DefaultRuntimeResolver()
	for _, candidate := range candidates {
		candidate = strings.TrimSpace(candidate)
		if candidate == "" {
			continue
		}
		info, err := resolver.Stat(candidate)
		if err != nil {
			continue
		}
		if info.IsDir() {
			err := fmt.Errorf("AgentTeam Desktop installation incomplete: bundled OpenCode runtime path %q is a directory", candidate)
			return RuntimeProbeResult{
				Path:       candidate,
				Installed:  true,
				Capable:    false,
				ReasonCode: RuntimeProbeReasonAgentTeamInstallIncomplete,
				Message:    err.Error(),
				Err:        err,
			}
		}
		if !runtimeBinaryExecutable(resolver, candidate) {
			err := fmt.Errorf("AgentTeam Desktop installation incomplete: bundled OpenCode runtime %q is not executable", candidate)
			return RuntimeProbeResult{
				Path:       candidate,
				Installed:  true,
				Capable:    false,
				ReasonCode: RuntimeProbeReasonAgentTeamInstallIncomplete,
				Message:    err.Error(),
				Err:        err,
			}
		}
		modelsPath := filepath.Join(filepath.Dir(candidate), "models.json")
		modelsInfo, err := resolver.Stat(modelsPath)
		if err != nil {
			err := fmt.Errorf("AgentTeam Desktop installation incomplete: bundled OpenCode models catalog %q is missing: %w", modelsPath, err)
			return RuntimeProbeResult{
				Path:       candidate,
				Installed:  true,
				Capable:    false,
				ReasonCode: RuntimeProbeReasonAgentTeamInstallIncomplete,
				Message:    err.Error(),
				Err:        err,
			}
		}
		if !modelsInfo.Mode().IsRegular() {
			err := fmt.Errorf("AgentTeam Desktop installation incomplete: bundled OpenCode models catalog path %q is not a regular file", modelsPath)
			return RuntimeProbeResult{
				Path:       candidate,
				Installed:  true,
				Capable:    false,
				ReasonCode: RuntimeProbeReasonAgentTeamInstallIncomplete,
				Message:    err.Error(),
				Err:        err,
			}
		}
		if expectedVersionRequired(candidate) && strings.TrimSpace(ManagedOpenCodeVersion) == "" {
			err := fmt.Errorf("AgentTeam Desktop installation incomplete: bundled OpenCode runtime version was not embedded in the AgentTeam Desktop sidecar")
			return RuntimeProbeResult{
				Path:       candidate,
				Installed:  true,
				Capable:    false,
				ReasonCode: RuntimeProbeReasonAgentTeamInstallIncomplete,
				Message:    err.Error(),
				Err:        err,
			}
		}
		return probeManagedOpenCodeVersion(candidate)
	}

	err := fmt.Errorf("AgentTeam Desktop installation incomplete: bundled OpenCode runtime is missing")
	return RuntimeProbeResult{
		Installed:  false,
		Capable:    false,
		ReasonCode: RuntimeProbeReasonAgentTeamInstallIncomplete,
		Message:    err.Error(),
		Err:        err,
	}
}

func expectedVersionRequired(candidate string) bool {
	exe, err := managedOpenCodeExecutable()
	if err != nil {
		return false
	}
	resourcesDir, ok := electronResourcesDir(exe, runtime.GOOS)
	if !ok {
		return false
	}
	expectedDir := filepath.Join(resourcesDir, "runtimes", "opencode")
	return sameManagedOpenCodePath(filepath.Dir(candidate), expectedDir)
}

func sameManagedOpenCodePath(left, right string) bool {
	left = filepath.Clean(left)
	right = filepath.Clean(right)
	if runtime.GOOS == "windows" {
		return strings.EqualFold(left, right)
	}
	return left == right
}

func probeManagedOpenCodeVersion(path string) RuntimeProbeResult {
	version, err := managedOpenCodeVersion(path)
	if err != nil {
		err = fmt.Errorf("AgentTeam Desktop installation incomplete: bundled OpenCode runtime %q cannot report its version: %w", path, err)
		return RuntimeProbeResult{
			Path:       path,
			Installed:  true,
			Capable:    false,
			ReasonCode: RuntimeProbeReasonAgentTeamInstallIncomplete,
			Message:    err.Error(),
			Err:        err,
		}
	}

	expected := strings.TrimSpace(ManagedOpenCodeVersion)
	if expected == "" {
		return RuntimeProbeResult{Path: path, Installed: true, Capable: true}
	}

	if runtimeVersionForCompare(version) != runtimeVersionForCompare(expected) {
		err := fmt.Errorf("AgentTeam Desktop installation incomplete: bundled OpenCode runtime version %q does not match expected %q", version, expected)
		return RuntimeProbeResult{
			Path:       path,
			Installed:  true,
			Capable:    false,
			ReasonCode: RuntimeProbeReasonVersionMismatch,
			Message:    err.Error(),
			Err:        err,
		}
	}
	return RuntimeProbeResult{Path: path, Installed: true, Capable: true}
}

func managedOpenCodeVersion(path string) (string, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	cmd := exec.CommandContext(ctx, path, "--version")
	ConfigureProbeCommand(cmd)
	cmd.Env = RuntimeProcessEnv()
	var stdout bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = io.Discard
	if err := cmd.Run(); err != nil {
		return "", err
	}
	output := strings.TrimSpace(stdout.String())
	if idx := strings.IndexByte(output, '\n'); idx >= 0 {
		output = strings.TrimSpace(output[:idx])
	}
	return output, nil
}

func managedOpenCodeBinaryName(goos string) string {
	if goos == "windows" {
		return "opencode.exe"
	}
	return "opencode"
}

func managedAgentTeamBinaryName(goos string) string {
	if goos == "windows" {
		return "agentteam.exe"
	}
	return "agentteam"
}

func runtimeVersionForCompare(version string) string {
	version = strings.TrimSpace(version)
	if version == "" {
		return ""
	}
	fields := strings.Fields(version)
	if len(fields) == 0 {
		return version
	}
	return strings.TrimPrefix(fields[len(fields)-1], "v")
}
