package installpath

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/fs"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
)

// effectiveUserHome returns the home directory of the actual interactive user,
// accounting for sudo re-execution. When SUDO_USER is set, it resolves the
// original user's home directory instead of root's.
func effectiveUserHome() string {
	if u := os.Getenv("SUDO_USER"); u != "" {
		if usr, err := user.Lookup(u); err == nil {
			return usr.HomeDir
		}
	}
	dir, _ := os.UserHomeDir()
	return dir
}

const shellPathSnippetHeader = "# Added by AgentTeam CLI"

type binaryVersionInfo struct {
	Version string `json:"version"`
}

var runtimeTreeRename = os.Rename

// ManagedDir returns the single authoritative directory for the managed agentteam binary.
// It respects an explicit AGENTTEAM_CONFIG_DIR override, and otherwise resolves the
// real user's home directory even when running under sudo.
func ManagedDir() string {
	if d := os.Getenv(profile.ConfigDirEnvKey); d != "" {
		return filepath.Join(d, "bin")
	}
	return filepath.Join(effectiveUserHome(), "."+profile.AppName, "bin")
}

// ManagedBinaryPath returns the single authoritative path for the managed agentteam binary.
func ManagedBinaryPath() string {
	name := "agentteam"
	if runtime.GOOS == "windows" {
		name += ".exe"
	}
	return filepath.Join(ManagedDir(), name)
}

// EnsureManagedBinaryFromCurrent copies the current executable into the
// managed path when the source is newer or when the managed binary
// does not exist yet.
func EnsureManagedBinaryFromCurrent() (string, error) {
	exePath, err := os.Executable()
	if err != nil {
		return "", fmt.Errorf("resolve current executable: %w", err)
	}
	return EnsureManagedBinary(exePath)
}

// ManagedBinaryForCurrentProcess ensures the current executable has been
// synced into the managed runtime path and reports whether the caller
// should switch execution to that managed binary.
func ManagedBinaryForCurrentProcess() (string, bool, error) {
	exePath, err := os.Executable()
	if err != nil {
		return "", false, fmt.Errorf("resolve current executable: %w", err)
	}

	managedPath, err := EnsureManagedBinary(exePath)
	if err != nil {
		return "", false, err
	}

	currentPath, err := resolvedPath(exePath)
	if err != nil {
		return "", false, fmt.Errorf("resolve current executable path %q: %w", exePath, err)
	}
	resolvedManagedPath, err := resolvedPath(managedPath)
	if err != nil {
		return "", false, fmt.Errorf("resolve managed executable path %q: %w", managedPath, err)
	}

	return managedPath, currentPath != resolvedManagedPath, nil
}

// ForceEnsureManagedBinary copies sourcePath into the managed path
// unconditionally, bypassing version comparison. This should be used when the
// user explicitly requests a service operation (install, reinstall, restart) and
// expects the currently running binary to become the service binary, regardless
// of version ordering.
//
// Background: EnsureManagedBinary compares versions and refuses to replace a
// newer managed binary with an older source. That is correct for automatic
// upgrades (we don't want to accidentally downgrade), but wrong for explicit
// service commands where the user built a local binary and runs `agentteam service
// install` — they intend the binary they just executed to be the one the
// service runs. Without ForceEnsureManagedBinary, a dev-build with a lower
// or equal version string would be silently ignored, and the service would
// continue running the old managed binary.
func ForceEnsureManagedBinary(sourcePath string) (string, error) {
	sourceRealPath, err := resolvedPath(sourcePath)
	if err != nil {
		return "", fmt.Errorf("resolve source executable path %q: %w", sourcePath, err)
	}
	if sourceRealPath == "" {
		return "", fmt.Errorf("source executable path is empty")
	}

	targetPath := ManagedBinaryPath()
	targetRealPath, err := resolvedPath(targetPath)
	if err != nil {
		targetRealPath = targetPath
	}
	// Already running from the managed location — nothing to do.
	if sourceRealPath == targetRealPath && managedTargetIsRegular(targetPath) {
		return targetPath, nil
	}

	if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
		return "", fmt.Errorf("create managed binary directory: %w", err)
	}

	if err := copyExecutableAndRuntime(sourceRealPath, targetPath); err != nil {
		return "", err
	}
	return targetPath, nil
}

// EnsureManagedBinary copies sourcePath into the managed path when
// needed (version-based gate) and returns the authoritative runtime path.
// For service install/reinstall/restart, prefer ForceEnsureManagedBinary
// which always replaces the managed binary.
func EnsureManagedBinary(sourcePath string) (string, error) {
	sourceRealPath, err := resolvedPath(sourcePath)
	if err != nil {
		return "", fmt.Errorf("resolve source executable path %q: %w", sourcePath, err)
	}
	if sourceRealPath == "" {
		return "", fmt.Errorf("source executable path is empty")
	}

	targetPath := ManagedBinaryPath()
	targetRealPath, err := resolvedPath(targetPath)
	if err != nil {
		targetRealPath = targetPath
	}
	if sourceRealPath == targetRealPath && managedTargetIsRegular(targetPath) {
		return targetPath, nil
	}

	if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
		return "", fmt.Errorf("create managed binary directory: %w", err)
	}

	shouldCopy, err := shouldReplaceTarget(sourceRealPath, targetPath)
	if err != nil {
		return "", err
	}
	if !shouldCopy {
		sameContent, err := sameFileContent(sourceRealPath, targetPath)
		if err != nil {
			return "", fmt.Errorf("compare binary contents for runtime sync %q and %q: %w", sourceRealPath, targetPath, err)
		}
		if sameContent {
			if err := syncRuntimeTree(sourceRealPath, targetPath); err != nil {
				return "", err
			}
		}
		return targetPath, nil
	}

	if err := copyExecutableAndRuntime(sourceRealPath, targetPath); err != nil {
		return "", err
	}
	return targetPath, nil
}

func managedTargetIsRegular(path string) bool {
	info, err := os.Lstat(path)
	return err == nil && info.Mode().IsRegular()
}

// EnsureShellPath ensures future interactive shells prepend the managed agentteam
// binary directory so `agentteam` resolves to the managed runtime path.
func EnsureShellPath() error {
	homeDir := effectiveUserHome()
	profilePath := preferredShellProfile(homeDir)
	line := fmt.Sprintf(`export PATH="%s:$PATH"`, ManagedDir())

	existing, err := os.ReadFile(profilePath)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("read shell profile %q: %w", profilePath, err)
	}
	if strings.Contains(string(existing), line) || strings.Contains(string(existing), ManagedDir()) {
		return nil
	}

	f, err := os.OpenFile(profilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return fmt.Errorf("open shell profile %q: %w", profilePath, err)
	}
	defer f.Close()

	if len(existing) > 0 && !strings.HasSuffix(string(existing), "\n") {
		if _, err := io.WriteString(f, "\n"); err != nil {
			return fmt.Errorf("write newline to shell profile %q: %w", profilePath, err)
		}
	}
	if _, err := io.WriteString(f, shellPathSnippetHeader+"\n"+line+"\n"); err != nil {
		return fmt.Errorf("append PATH snippet to shell profile %q: %w", profilePath, err)
	}
	return nil
}

func preferredShellProfile(homeDir string) string {
	shellName := filepath.Base(strings.TrimSpace(os.Getenv("SHELL")))
	bashProfile := filepath.Join(homeDir, ".bashrc")
	zshProfile := filepath.Join(homeDir, ".zshrc")

	switch shellName {
	case "bash":
		return bashProfile
	case "zsh":
		return zshProfile
	default:
		bashExists := fileExists(bashProfile)
		zshExists := fileExists(zshProfile)
		if bashExists && !zshExists {
			return bashProfile
		}
		if zshExists && !bashExists {
			return zshProfile
		}
		if runtime.GOOS == "linux" {
			return bashProfile
		}
		return zshProfile
	}
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func resolvedPath(path string) (string, error) {
	path = strings.TrimSpace(path)
	if path == "" {
		return "", fmt.Errorf("path is empty")
	}

	if realPath, err := filepath.EvalSymlinks(path); err == nil {
		return realPath, nil
	}

	absolutePath, err := filepath.Abs(path)
	if err != nil {
		return "", fmt.Errorf("resolve absolute path for %q: %w", path, err)
	}
	return filepath.Clean(absolutePath), nil
}

func shouldReplaceTarget(sourcePath, targetPath string) (bool, error) {
	targetInfo, err := os.Lstat(targetPath)
	if os.IsNotExist(err) {
		return true, nil
	}
	if err != nil {
		return false, fmt.Errorf("lstat managed binary %q: %w", targetPath, err)
	}
	if targetInfo.Mode()&os.ModeSymlink != 0 {
		return true, nil
	}
	if _, err := os.Stat(targetPath); err != nil {
		return false, fmt.Errorf("stat managed binary %q: %w", targetPath, err)
	}

	sourceVersion, err := binaryVersion(sourcePath)
	if err != nil {
		return false, fmt.Errorf("read source binary version %q: %w", sourcePath, err)
	}
	targetVersion, err := binaryVersion(targetPath)
	if err != nil {
		return true, nil
	}
	versionCmp := compareVersions(sourceVersion, targetVersion)
	if versionCmp > 0 {
		return true, nil
	}
	if versionCmp < 0 {
		return false, nil
	}
	sameContent, err := sameFileContent(sourcePath, targetPath)
	if err != nil {
		return false, fmt.Errorf("compare binary contents for %q and %q: %w", sourcePath, targetPath, err)
	}
	return !sameContent, nil
}

func binaryVersion(path string) (string, error) {
	cmd := exec.Command(path, "--version", "-f", "json")
	output, err := cmd.Output()
	if err != nil {
		return "", fmt.Errorf("run %q --version -f json: %w", path, err)
	}

	var info binaryVersionInfo
	if err := json.Unmarshal(output, &info); err != nil {
		return "", fmt.Errorf("decode version JSON from %q: %w", path, err)
	}
	if strings.TrimSpace(info.Version) == "" {
		return "", fmt.Errorf("binary %q returned empty version", path)
	}
	return strings.TrimSpace(info.Version), nil
}

func copyExecutable(sourcePath, targetPath string) error {
	sourceFile, err := os.Open(sourcePath)
	if err != nil {
		return fmt.Errorf("open source executable %q: %w", sourcePath, err)
	}
	defer sourceFile.Close()

	info, err := sourceFile.Stat()
	if err != nil {
		return fmt.Errorf("stat source executable %q: %w", sourcePath, err)
	}

	tempFile, err := os.CreateTemp(filepath.Dir(targetPath), ".agentteam-install-*")
	if err != nil {
		return fmt.Errorf("create temp executable near %q: %w", targetPath, err)
	}
	tempPath := tempFile.Name()
	defer os.Remove(tempPath)

	if _, err := io.Copy(tempFile, sourceFile); err != nil {
		tempFile.Close()
		return fmt.Errorf("copy %q to temp executable %q: %w", sourcePath, tempPath, err)
	}
	if err := tempFile.Chmod(info.Mode() | 0755); err != nil {
		tempFile.Close()
		return fmt.Errorf("chmod temp executable %q: %w", tempPath, err)
	}
	if err := tempFile.Close(); err != nil {
		return fmt.Errorf("close temp executable %q: %w", tempPath, err)
	}
	if err := os.Rename(tempPath, targetPath); err != nil {
		return fmt.Errorf("replace managed executable %q: %w", targetPath, err)
	}
	return nil
}

func copyExecutableAndRuntime(sourcePath, targetPath string) error {
	runtimeSync, err := prepareRuntimeTreeSync(sourcePath, targetPath)
	if err != nil {
		return err
	}
	if runtimeSync == nil {
		return copyExecutable(sourcePath, targetPath)
	}
	defer runtimeSync.cleanup()

	backupPath, existed, err := backupExecutable(targetPath)
	if err != nil {
		return err
	}
	if backupPath != "" {
		defer os.Remove(backupPath)
	}

	if err := copyExecutable(sourcePath, targetPath); err != nil {
		return err
	}
	if err := runtimeSync.commit(); err != nil {
		if restoreErr := restoreExecutable(targetPath, backupPath, existed); restoreErr != nil {
			return fmt.Errorf("%w; restore managed executable: %v", err, restoreErr)
		}
		return err
	}
	return nil
}

func syncRuntimeTree(sourceBinaryPath, targetBinaryPath string) error {
	runtimeSync, err := prepareRuntimeTreeSync(sourceBinaryPath, targetBinaryPath)
	if err != nil {
		return err
	}
	if runtimeSync == nil {
		return nil
	}
	defer runtimeSync.cleanup()
	return runtimeSync.commit()
}

type runtimeTreeSync struct {
	targetDir string
	tempDir   string
	backupDir string
}

func prepareRuntimeTreeSync(sourceBinaryPath, targetBinaryPath string) (*runtimeTreeSync, error) {
	sourceRuntimeDir := bundledRuntimeDirForBinary(sourceBinaryPath)
	sourceInfo, err := os.Stat(sourceRuntimeDir)
	if os.IsNotExist(err) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("stat bundled runtimes %q: %w", sourceRuntimeDir, err)
	}
	if !sourceInfo.IsDir() {
		return nil, fmt.Errorf("bundled runtimes path %q is not a directory", sourceRuntimeDir)
	}

	targetRuntimeDir := filepath.Join(filepath.Dir(targetBinaryPath), "runtimes")
	sourceRealPath, err := resolvedPath(sourceRuntimeDir)
	if err != nil {
		return nil, fmt.Errorf("resolve bundled runtimes path %q: %w", sourceRuntimeDir, err)
	}
	targetRealPath, err := resolvedPath(targetRuntimeDir)
	if err == nil && sourceRealPath == targetRealPath {
		return nil, nil
	}

	targetParent := filepath.Dir(targetRuntimeDir)
	tempDir, err := os.MkdirTemp(targetParent, ".agentteam-runtimes-*")
	if err != nil {
		return nil, fmt.Errorf("create temp runtime directory near %q: %w", targetRuntimeDir, err)
	}

	if err := copyDirectoryContents(sourceRuntimeDir, tempDir); err != nil {
		_ = os.RemoveAll(tempDir)
		return nil, fmt.Errorf("copy bundled runtimes from %q to %q: %w", sourceRuntimeDir, tempDir, err)
	}
	return &runtimeTreeSync{targetDir: targetRuntimeDir, tempDir: tempDir}, nil
}

func bundledRuntimeDirForBinary(sourceBinaryPath string) string {
	sourceDir := filepath.Dir(sourceBinaryPath)
	if filepath.Base(sourceDir) == "Helpers" && filepath.Base(filepath.Dir(sourceDir)) == "Contents" {
		return filepath.Join(filepath.Dir(sourceDir), "Resources", "runtimes")
	}
	if filepath.Base(sourceDir) == "MacOS" && filepath.Base(filepath.Dir(sourceDir)) == "Contents" {
		return filepath.Join(filepath.Dir(sourceDir), "Resources", "runtimes")
	}
	return filepath.Join(sourceDir, "runtimes")
}

func (s *runtimeTreeSync) commit() error {
	if s == nil || s.tempDir == "" {
		return nil
	}

	targetParent := filepath.Dir(s.targetDir)
	if _, err := os.Stat(s.targetDir); err == nil {
		backupDir, err := os.MkdirTemp(targetParent, ".agentteam-runtimes-previous-*")
		if err != nil {
			return fmt.Errorf("create runtime backup directory near %q: %w", s.targetDir, err)
		}
		if err := os.Remove(backupDir); err != nil {
			return fmt.Errorf("prepare runtime backup path %q: %w", backupDir, err)
		}
		if err := runtimeTreeRename(s.targetDir, backupDir); err != nil {
			return fmt.Errorf("back up existing runtime directory %q: %w", s.targetDir, err)
		}
		s.backupDir = backupDir
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("stat managed runtime directory %q: %w", s.targetDir, err)
	}

	if err := runtimeTreeRename(s.tempDir, s.targetDir); err != nil {
		if s.backupDir != "" {
			_ = runtimeTreeRename(s.backupDir, s.targetDir)
			s.backupDir = ""
		}
		return fmt.Errorf("replace managed runtime directory %q: %w", s.targetDir, err)
	}
	s.tempDir = ""
	if s.backupDir != "" {
		_ = os.RemoveAll(s.backupDir)
		s.backupDir = ""
	}
	return nil
}

func (s *runtimeTreeSync) cleanup() {
	if s == nil {
		return
	}
	if s.tempDir != "" {
		_ = os.RemoveAll(s.tempDir)
	}
	if s.backupDir != "" {
		_ = os.RemoveAll(s.backupDir)
	}
}

func backupExecutable(targetPath string) (string, bool, error) {
	info, err := os.Stat(targetPath)
	if os.IsNotExist(err) {
		return "", false, nil
	}
	if err != nil {
		return "", false, fmt.Errorf("stat managed executable %q: %w", targetPath, err)
	}
	if !info.Mode().IsRegular() {
		return "", false, fmt.Errorf("managed executable %q is not a regular file", targetPath)
	}

	backupFile, err := os.CreateTemp(filepath.Dir(targetPath), ".agentteam-executable-previous-*")
	if err != nil {
		return "", false, fmt.Errorf("create managed executable backup near %q: %w", targetPath, err)
	}
	backupPath := backupFile.Name()
	if err := backupFile.Close(); err != nil {
		_ = os.Remove(backupPath)
		return "", false, fmt.Errorf("close managed executable backup %q: %w", backupPath, err)
	}
	if err := copyFile(targetPath, backupPath, info.Mode().Perm()); err != nil {
		_ = os.Remove(backupPath)
		return "", false, fmt.Errorf("copy managed executable backup %q: %w", targetPath, err)
	}
	return backupPath, true, nil
}

func restoreExecutable(targetPath, backupPath string, existed bool) error {
	if !existed {
		if err := os.Remove(targetPath); err != nil && !os.IsNotExist(err) {
			return fmt.Errorf("remove failed new managed executable %q: %w", targetPath, err)
		}
		return nil
	}
	info, err := os.Stat(backupPath)
	if err != nil {
		return fmt.Errorf("stat managed executable backup %q: %w", backupPath, err)
	}
	if err := copyFile(backupPath, targetPath, info.Mode().Perm()); err != nil {
		return fmt.Errorf("restore previous managed executable %q: %w", targetPath, err)
	}
	return nil
}

func copyDirectoryContents(sourceDir, targetDir string) error {
	return filepath.WalkDir(sourceDir, func(path string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		rel, err := filepath.Rel(sourceDir, path)
		if err != nil {
			return err
		}
		if rel == "." {
			return nil
		}
		targetPath := filepath.Join(targetDir, rel)
		info, err := entry.Info()
		if err != nil {
			return err
		}
		mode := info.Mode()
		if entry.IsDir() {
			if err := os.MkdirAll(targetPath, mode.Perm()|0755); err != nil {
				return err
			}
			return os.Chmod(targetPath, mode.Perm()|0755)
		}
		if mode&os.ModeSymlink != 0 {
			linkTarget, err := os.Readlink(path)
			if err != nil {
				return err
			}
			return os.Symlink(linkTarget, targetPath)
		}
		if !mode.IsRegular() {
			return fmt.Errorf("unsupported runtime artifact %q with mode %s", path, mode)
		}
		return copyFile(path, targetPath, mode.Perm())
	})
}

func copyFile(sourcePath, targetPath string, mode fs.FileMode) error {
	if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
		return err
	}

	sourceFile, err := os.Open(sourcePath)
	if err != nil {
		return err
	}
	defer sourceFile.Close()

	targetFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode|0644)
	if err != nil {
		return err
	}
	if _, err := io.Copy(targetFile, sourceFile); err != nil {
		targetFile.Close()
		return err
	}
	if err := targetFile.Chmod(mode | 0644); err != nil {
		targetFile.Close()
		return err
	}
	return targetFile.Close()
}

func sameFileContent(sourcePath, targetPath string) (bool, error) {
	sourceData, err := os.ReadFile(sourcePath)
	if err != nil {
		return false, fmt.Errorf("read source executable %q: %w", sourcePath, err)
	}
	targetData, err := os.ReadFile(targetPath)
	if err != nil {
		return false, fmt.Errorf("read target executable %q: %w", targetPath, err)
	}
	return bytes.Equal(sourceData, targetData), nil
}

func compareVersions(a, b string) int {
	a = strings.TrimPrefix(strings.TrimSpace(a), "v")
	b = strings.TrimPrefix(strings.TrimSpace(b), "v")

	aClean, aPre := splitVersion(a)
	bClean, bPre := splitVersion(b)

	if cmp := compareSemverParts(aClean, bClean); cmp != 0 {
		return cmp
	}
	return comparePrerelease(aPre, bPre)
}

func splitVersion(v string) (clean, pre string) {
	buildStripped := v
	if idx := strings.Index(buildStripped, "+"); idx >= 0 {
		buildStripped = buildStripped[:idx]
	}
	if idx := strings.Index(buildStripped, "-"); idx >= 0 {
		return buildStripped[:idx], buildStripped[idx+1:]
	}
	return buildStripped, ""
}

func compareSemverParts(a, b string) int {
	aParts := strings.SplitN(a, ".", 3)
	bParts := strings.SplitN(b, ".", 3)
	maxLen := len(aParts)
	if len(bParts) > maxLen {
		maxLen = len(bParts)
	}

	for i := 0; i < maxLen; i++ {
		aVal := 0
		bVal := 0
		if i < len(aParts) {
			aVal = parseVersionInt(aParts[i])
		}
		if i < len(bParts) {
			bVal = parseVersionInt(bParts[i])
		}
		switch {
		case aVal < bVal:
			return -1
		case aVal > bVal:
			return 1
		}
	}
	return 0
}

func parseVersionInt(s string) int {
	value := 0
	for _, ch := range s {
		if ch < '0' || ch > '9' {
			break
		}
		value = value*10 + int(ch-'0')
	}
	return value
}

func comparePrerelease(a, b string) int {
	if a == "" && b != "" {
		return 1
	}
	if a != "" && b == "" {
		return -1
	}
	if a == b {
		return 0
	}

	aParts := strings.Split(a, ".")
	bParts := strings.Split(b, ".")
	maxLen := len(aParts)
	if len(bParts) > maxLen {
		maxLen = len(bParts)
	}
	for i := 0; i < maxLen; i++ {
		if i >= len(aParts) {
			return -1
		}
		if i >= len(bParts) {
			return 1
		}
		if cmp := comparePrereleaseIdentifier(aParts[i], bParts[i]); cmp != 0 {
			return cmp
		}
	}
	return 0
}

func comparePrereleaseIdentifier(a, b string) int {
	aNum, aIsNum := parseNumericIdentifier(a)
	bNum, bIsNum := parseNumericIdentifier(b)
	switch {
	case aIsNum && bIsNum:
		switch {
		case aNum < bNum:
			return -1
		case aNum > bNum:
			return 1
		default:
			return 0
		}
	case aIsNum:
		return -1
	case bIsNum:
		return 1
	case a < b:
		return -1
	case a > b:
		return 1
	default:
		return 0
	}
}

func parseNumericIdentifier(s string) (int, bool) {
	if s == "" {
		return 0, false
	}
	value := 0
	for _, ch := range s {
		if ch < '0' || ch > '9' {
			return 0, false
		}
		value = value*10 + int(ch-'0')
	}
	return value, true
}
