package agentbinaries

import (
	"archive/tar"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/rs/zerolog/log"
)

type HostAgentBinary struct {
	Platform  string
	Arch      string
	Filenames []string
}

var requiredHostAgentBinaries = []HostAgentBinary{
	{Platform: "linux", Arch: "amd64", Filenames: []string{"pulse-host-agent-linux-amd64"}},
	{Platform: "linux", Arch: "arm64", Filenames: []string{"pulse-host-agent-linux-arm64"}},
	{Platform: "linux", Arch: "armv7", Filenames: []string{"pulse-host-agent-linux-armv7"}},
	{Platform: "linux", Arch: "armv6", Filenames: []string{"pulse-host-agent-linux-armv6"}},
	{Platform: "linux", Arch: "386", Filenames: []string{"pulse-host-agent-linux-386"}},
	{Platform: "darwin", Arch: "amd64", Filenames: []string{"pulse-host-agent-darwin-amd64"}},
	{Platform: "darwin", Arch: "arm64", Filenames: []string{"pulse-host-agent-darwin-arm64"}},
	{
		Platform:  "windows",
		Arch:      "amd64",
		Filenames: []string{"pulse-host-agent-windows-amd64", "pulse-host-agent-windows-amd64.exe"},
	},
	{
		Platform:  "windows",
		Arch:      "arm64",
		Filenames: []string{"pulse-host-agent-windows-arm64", "pulse-host-agent-windows-arm64.exe"},
	},
	{
		Platform:  "windows",
		Arch:      "386",
		Filenames: []string{"pulse-host-agent-windows-386", "pulse-host-agent-windows-386.exe"},
	},
}

var downloadMu sync.Mutex

// HostAgentSearchPaths returns the directories to search for host agent binaries.
func HostAgentSearchPaths() []string {
	primary := strings.TrimSpace(os.Getenv("PULSE_BIN_DIR"))
	if primary == "" {
		primary = "/opt/pulse/bin"
	}

	dirs := []string{primary, "./bin", "."}
	seen := make(map[string]struct{}, len(dirs))
	result := make([]string, 0, len(dirs))
	for _, dir := range dirs {
		clean := filepath.Clean(dir)
		if _, ok := seen[clean]; ok {
			continue
		}
		seen[clean] = struct{}{}
		result = append(result, clean)
	}
	return result
}

// EnsureHostAgentBinaries verifies that all host agent binaries are present locally.
// If any are missing, it attempts to restore them from the matching GitHub release.
// The returned map contains any binaries that remain missing after the attempt.
func EnsureHostAgentBinaries(version string) map[string]HostAgentBinary {
	binDirs := HostAgentSearchPaths()
	missing := findMissingHostAgentBinaries(binDirs)
	if len(missing) == 0 {
		return nil
	}

	downloadMu.Lock()
	defer downloadMu.Unlock()

	// Re-check after acquiring the lock in case another goroutine restored them.
	missing = findMissingHostAgentBinaries(binDirs)
	if len(missing) == 0 {
		return nil
	}

	missingPlatforms := make([]string, 0, len(missing))
	for key := range missing {
		missingPlatforms = append(missingPlatforms, key)
	}
	sort.Strings(missingPlatforms)

	log.Warn().
		Strs("missing_platforms", missingPlatforms).
		Msg("Host agent binaries missing - attempting to download bundle from GitHub release")

	if err := DownloadAndInstallHostAgentBinaries(version, binDirs[0]); err != nil {
		log.Error().
			Err(err).
			Str("target_dir", binDirs[0]).
			Strs("missing_platforms", missingPlatforms).
			Msg("Failed to automatically install host agent binaries; download endpoints will return 404s")
		return missing
	}

	if remaining := findMissingHostAgentBinaries(binDirs); len(remaining) > 0 {
		stillMissing := make([]string, 0, len(remaining))
		for key := range remaining {
			stillMissing = append(stillMissing, key)
		}
		sort.Strings(stillMissing)
		log.Warn().
			Strs("missing_platforms", stillMissing).
			Msg("Host agent binaries still missing after automatic restoration attempt")
		return remaining
	}

	log.Info().Msg("Host agent binaries restored from GitHub release bundle")
	return nil
}

// DownloadAndInstallHostAgentBinaries fetches the universal host agent bundle for the given version and installs it.
func DownloadAndInstallHostAgentBinaries(version string, targetDir string) error {
	normalizedVersion := normalizeVersionTag(version)
	if normalizedVersion == "" || strings.EqualFold(normalizedVersion, "vdev") {
		return fmt.Errorf("cannot download host agent bundle for non-release version %q", version)
	}

	if err := os.MkdirAll(targetDir, 0o755); err != nil {
		return fmt.Errorf("failed to ensure bin directory %s: %w", targetDir, err)
	}

	url := fmt.Sprintf("https://github.com/rcourtman/Pulse/releases/download/%[1]s/pulse-%[1]s.tar.gz", normalizedVersion)
	tempFile, err := os.CreateTemp("", "pulse-host-agent-*.tar.gz")
	if err != nil {
		return fmt.Errorf("failed to create temporary archive file: %w", err)
	}
	defer os.Remove(tempFile.Name())

	client := &http.Client{Timeout: 2 * time.Minute}
	resp, err := client.Get(url)
	if err != nil {
		return fmt.Errorf("failed to download host agent bundle from %s: %w", url, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return fmt.Errorf("unexpected status %d downloading %s: %s", resp.StatusCode, url, strings.TrimSpace(string(body)))
	}

	if _, err := io.Copy(tempFile, resp.Body); err != nil {
		return fmt.Errorf("failed to save host agent bundle: %w", err)
	}

	if err := tempFile.Close(); err != nil {
		return fmt.Errorf("failed to close temporary bundle file: %w", err)
	}

	if err := extractHostAgentBinaries(tempFile.Name(), targetDir); err != nil {
		return err
	}

	return nil
}

func findMissingHostAgentBinaries(binDirs []string) map[string]HostAgentBinary {
	missing := make(map[string]HostAgentBinary)
	for _, binary := range requiredHostAgentBinaries {
		if !hostAgentBinaryExists(binDirs, binary.Filenames) {
			key := fmt.Sprintf("%s-%s", binary.Platform, binary.Arch)
			missing[key] = binary
		}
	}
	return missing
}

func hostAgentBinaryExists(binDirs, filenames []string) bool {
	for _, dir := range binDirs {
		for _, name := range filenames {
			path := filepath.Join(dir, name)
			if info, err := os.Stat(path); err == nil && !info.IsDir() {
				return true
			}
		}
	}
	return false
}

func normalizeVersionTag(version string) string {
	v := strings.TrimSpace(version)
	if v == "" {
		return ""
	}
	v = strings.TrimPrefix(v, "v")
	return "v" + v
}

func extractHostAgentBinaries(archivePath, targetDir string) error {
	file, err := os.Open(archivePath)
	if err != nil {
		return fmt.Errorf("failed to open host agent bundle: %w", err)
	}
	defer file.Close()

	gzReader, err := gzip.NewReader(file)
	if err != nil {
		return fmt.Errorf("failed to create gzip reader: %w", err)
	}
	defer gzReader.Close()

	tr := tar.NewReader(gzReader)
	type pendingLink struct {
		path   string
		target string
	}
	var symlinks []pendingLink

	for {
		header, err := tr.Next()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return fmt.Errorf("failed to read host agent bundle: %w", err)
		}

		if header == nil {
			continue
		}

		if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeSymlink {
			continue
		}

		if !strings.HasPrefix(header.Name, "bin/") {
			continue
		}

		base := path.Base(header.Name)
		if !strings.HasPrefix(base, "pulse-host-agent-") {
			continue
		}

		destPath := filepath.Join(targetDir, base)

		switch header.Typeflag {
		case tar.TypeReg:
			if err := writeHostAgentFile(destPath, tr, header.FileInfo().Mode()); err != nil {
				return err
			}
		case tar.TypeSymlink:
			symlinks = append(symlinks, pendingLink{
				path:   destPath,
				target: header.Linkname,
			})
		}
	}

	for _, link := range symlinks {
		if err := os.Remove(link.path); err != nil && !os.IsNotExist(err) {
			return fmt.Errorf("failed to replace existing symlink %s: %w", link.path, err)
		}
		if err := os.Symlink(link.target, link.path); err != nil {
			// Fallback: copy the referenced file if symlinks are not permitted
			source := filepath.Join(targetDir, link.target)
			if err := copyHostAgentFile(source, link.path); err != nil {
				return fmt.Errorf("failed to create symlink %s -> %s: %w", link.path, link.target, err)
			}
		}
	}

	return nil
}

func writeHostAgentFile(destination string, reader io.Reader, mode os.FileMode) error {
	if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
		return fmt.Errorf("failed to create directory for %s: %w", destination, err)
	}

	tmpFile, err := os.CreateTemp(filepath.Dir(destination), "pulse-host-agent-*")
	if err != nil {
		return fmt.Errorf("failed to create temporary file for %s: %w", destination, err)
	}
	defer os.Remove(tmpFile.Name())

	if _, err := io.Copy(tmpFile, reader); err != nil {
		tmpFile.Close()
		return fmt.Errorf("failed to extract %s: %w", destination, err)
	}

	if err := tmpFile.Chmod(normalizeExecutableMode(mode)); err != nil {
		tmpFile.Close()
		return fmt.Errorf("failed to set permissions on %s: %w", destination, err)
	}

	if err := tmpFile.Close(); err != nil {
		return fmt.Errorf("failed to finalize %s: %w", destination, err)
	}

	if err := os.Rename(tmpFile.Name(), destination); err != nil {
		return fmt.Errorf("failed to install %s: %w", destination, err)
	}

	return nil
}

func copyHostAgentFile(source, destination string) error {
	src, err := os.Open(source)
	if err != nil {
		return fmt.Errorf("failed to open %s for fallback copy: %w", source, err)
	}
	defer src.Close()

	if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
		return fmt.Errorf("failed to prepare directory for %s: %w", destination, err)
	}

	dst, err := os.OpenFile(destination, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
	if err != nil {
		return fmt.Errorf("failed to create fallback copy %s: %w", destination, err)
	}
	defer dst.Close()

	if _, err := io.Copy(dst, src); err != nil {
		return fmt.Errorf("failed to copy %s to %s: %w", source, destination, err)
	}

	return nil
}

func normalizeExecutableMode(mode os.FileMode) os.FileMode {
	perms := mode.Perm()
	if perms&0o111 == 0 {
		perms |= 0o755
	}
	return (mode &^ os.ModePerm) | perms
}
