package checker

import (
	"bufio"
	"context"
	"errors"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"syscall"
	"time"

	"pentagi/cmd/installer/state"

	"github.com/moby/moby/client"
)

type DockerVersion struct {
	Version string
	Valid   bool
}

type ImageInfo struct {
	Name string
	Tag  string
	Hash string
}

func checkFileExists(path string) bool {
	_, err := os.Stat(path)
	return !os.IsNotExist(err)
}

func checkFileIsReadable(path string) bool {
	file, err := os.Open(path)
	if err != nil {
		return false
	}
	defer file.Close()
	return true
}

// checkDirIsWritable checks if we can write to a directory
func checkDirIsWritable(dirPath string) bool {
	// try to create a temporary file in the directory
	tempFile, err := os.CreateTemp(dirPath, ".pentagi_test_*")
	if err != nil {
		return false
	}
	tempPath := tempFile.Name()
	tempFile.Close()

	// clean up the test file
	os.Remove(tempPath)
	return true
}

func getEnvVar(appState state.State, key, defaultValue string) string {
	if appState == nil {
		return defaultValue
	}

	if envVar, exist := appState.GetVar(key); exist && envVar.Value != "" {
		return envVar.Value
	} else if envVar.Default != "" {
		return envVar.Default
	}

	return defaultValue
}

// getProxyURL retrieves the proxy URL from application state if configured
func getProxyURL(appState state.State) string {
	if appState == nil {
		return ""
	}
	return getEnvVar(appState, "PROXY_URL", "")
}

func createDockerClient(host, certPath string, tlsVerify bool) (*client.Client, error) {
	var opts []client.Opt

	if host != "" {
		opts = append(opts, client.WithHost(host))
	}

	if tlsVerify && certPath != "" {
		opts = append(opts, client.WithTLSClientConfig(
			filepath.Join(certPath, "ca.pem"),
			filepath.Join(certPath, "cert.pem"),
			filepath.Join(certPath, "key.pem"),
		))
	}

	return client.New(opts...)
}

// createDockerClientFromEnv creates a docker client and returns the error type
func createDockerClientFromEnv(ctx context.Context) (*client.Client, DockerErrorType) {
	// first check if docker command exists
	_, err := exec.LookPath("docker")
	if err != nil {
		return nil, DockerErrorNotInstalled
	}

	cli, err := client.New(client.FromEnv)
	if err != nil {
		return nil, DockerErrorAPIError
	}

	// try to ping the daemon
	_, err = cli.Ping(ctx, client.PingOptions{NegotiateAPIVersion: true})
	if err != nil {
		cli.Close() // close client on error
		// check if it's a connection error (daemon not running)
		if strings.Contains(err.Error(), "Cannot connect to the Docker daemon") ||
			strings.Contains(err.Error(), "Is the docker daemon running") ||
			strings.Contains(err.Error(), "connection refused") ||
			strings.Contains(err.Error(), "no such host") ||
			strings.Contains(err.Error(), "dial unix") {
			return nil, DockerErrorNotRunning
		}
		// check for permission errors
		if strings.Contains(err.Error(), "permission denied") ||
			strings.Contains(err.Error(), "Got permission denied") {
			return nil, DockerErrorPermission
		}
		// other API errors
		return nil, DockerErrorAPIError
	}

	return cli, DockerErrorNone
}

type DockerErrorType string

// DockerErrorType constants
const (
	DockerErrorNone         DockerErrorType = ""
	DockerErrorNotInstalled DockerErrorType = "not_installed"
	DockerErrorNotRunning   DockerErrorType = "not_running"
	DockerErrorAPIError     DockerErrorType = "api_error"
	DockerErrorPermission   DockerErrorType = "permission"
)

func checkDockerVersion(ctx context.Context, cli *client.Client) DockerVersion {
	version, err := cli.ServerVersion(ctx, client.ServerVersionOptions{})
	if err != nil {
		return DockerVersion{Version: "", Valid: false}
	}

	versionStr := version.Version
	valid := checkVersionCompatibility(versionStr, "20.0.0")

	return DockerVersion{Version: versionStr, Valid: valid}
}

func checkDockerCliVersion() DockerVersion {
	_, err := exec.LookPath("docker")
	if err != nil {
		return DockerVersion{Version: "", Valid: false}
	}

	cmd := exec.Command("docker", "version", "--format", "{{.Client.Version}}")
	output, err := cmd.Output()
	if err != nil && len(output) == 0 {
		return DockerVersion{Version: "", Valid: false}
	}

	versionStr := extractVersionFromOutput(string(output))
	valid := checkVersionCompatibility(versionStr, "20.0.0")

	return DockerVersion{Version: versionStr, Valid: valid}
}

func checkDockerComposeVersion() DockerVersion {
	return checkDockerComposeVersionWithRunner(func(name string, args ...string) ([]byte, error) {
		return exec.Command(name, args...).Output()
	})
}

func checkDockerComposeVersionWithRunner(run func(name string, args ...string) ([]byte, error)) DockerVersion {
	output, err := run("docker", "compose", "version")
	if err != nil && len(output) == 0 {
		return DockerVersion{Version: "", Valid: false}
	}

	versionStr := extractVersionFromOutput(string(output))
	valid := checkVersionCompatibility(versionStr, "1.25.0")

	return DockerVersion{Version: versionStr, Valid: valid}
}

func extractVersionFromOutput(output string) string {
	re := regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
	matches := re.FindStringSubmatch(output)
	if len(matches) > 1 {
		return matches[1]
	}
	return ""
}

func checkVersionCompatibility(version, minVersion string) bool {
	if version == "" || minVersion == "" {
		return false
	}

	versionParts := strings.Split(version, ".")
	minVersionParts := strings.Split(minVersion, ".")

	for i := 0; i < len(versionParts) && i < len(minVersionParts); i++ {
		v, err1 := strconv.Atoi(versionParts[i])
		minV, err2 := strconv.Atoi(minVersionParts[i])

		if err1 != nil || err2 != nil {
			return false
		}

		if v > minV {
			return true
		}
		if v < minV {
			return false
		}
	}

	return len(versionParts) >= len(minVersionParts)
}

func checkContainerExists(ctx context.Context, cli *client.Client, name string) (exists, running bool) {
	containers, err := cli.ContainerList(ctx, client.ContainerListOptions{All: true})
	if err != nil {
		return false, false
	}

	for _, cont := range containers.Items {
		for _, containerName := range cont.Names {
			if strings.TrimPrefix(containerName, "/") == name {
				return true, cont.State == "running"
			}
		}
	}

	return false, false
}

// checkVolumesExist checks if any of the specified volumes exist
// it matches both exact names and volumes with compose project prefix (e.g., "pentagi_pentagi-data")
func checkVolumesExist(ctx context.Context, cli *client.Client, volumeNames []string) bool {
	if cli == nil || len(volumeNames) == 0 {
		return false
	}

	volumes, err := cli.VolumeList(ctx, client.VolumeListOptions{})
	if err != nil {
		return false
	}

	// collect all volume names from Docker
	existingVolumes := make([]string, 0, len(volumes.Items))
	for _, vol := range volumes.Items {
		existingVolumes = append(existingVolumes, vol.Name)
	}

	// check if any of the requested volumes exist
	// matches both exact names and volumes with compose prefix (project_volume-name)
	for _, volumeName := range volumeNames {
		for _, existingVolume := range existingVolumes {
			// exact match or suffix match with underscore separator
			if existingVolume == volumeName || strings.HasSuffix(existingVolume, "_"+volumeName) {
				return true
			}
		}
	}

	return false
}

func checkCPUResources() bool {
	return runtime.NumCPU() >= 2
}

// determineComponentNeeds checks which components need to be started based on their status
func determineComponentNeeds(c *CheckResult) (needsForPentagi, needsForGraphiti, needsForLangfuse, needsForObservability bool) {
	needsForPentagi = !c.PentagiRunning
	needsForGraphiti = c.GraphitiConnected && !c.GraphitiExternal && !c.GraphitiRunning
	needsForLangfuse = c.LangfuseConnected && !c.LangfuseExternal && !c.LangfuseRunning
	needsForObservability = c.ObservabilityConnected && !c.ObservabilityExternal && !c.ObservabilityRunning
	return
}

// calculateRequiredMemoryGB calculates the total memory required based on which components need to be started
func calculateRequiredMemoryGB(needsForPentagi, needsForGraphiti, needsForLangfuse, needsForObservability bool) float64 {
	requiredGB := MinFreeMemGB
	if needsForPentagi {
		requiredGB += MinFreeMemGBForPentagi
	}
	if needsForGraphiti {
		requiredGB += MinFreeMemGBForGraphiti
	}
	if needsForLangfuse {
		requiredGB += MinFreeMemGBForLangfuse
	}
	if needsForObservability {
		requiredGB += MinFreeMemGBForObservability
	}
	return requiredGB
}

func checkMemoryResources(needsForPentagi, needsForGraphiti, needsForLangfuse, needsForObservability bool) bool {
	if !needsForPentagi && !needsForGraphiti && !needsForLangfuse && !needsForObservability {
		return true
	}

	requiredGB := calculateRequiredMemoryGB(needsForPentagi, needsForGraphiti, needsForLangfuse, needsForObservability)

	// check available memory using different methods depending on OS
	switch runtime.GOOS {
	case "linux":
		return checkLinuxMemory(requiredGB)
	case "darwin":
		return checkDarwinMemory(requiredGB)
	default:
		return true // assume OK for other systems
	}
}

// getAvailableMemoryGB returns the available memory in GB for the current OS
func getAvailableMemoryGB() float64 {
	switch runtime.GOOS {
	case "linux":
		return getLinuxAvailableMemoryGB()
	case "darwin":
		return getDarwinAvailableMemoryGB()
	default:
		return 0.0 // unknown for other systems
	}
}

// getLinuxAvailableMemoryGB reads available memory from /proc/meminfo on Linux
func getLinuxAvailableMemoryGB() float64 {
	file, err := os.Open("/proc/meminfo")
	if err != nil {
		return 0.0
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	var memFree, memAvailable int64

	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "MemAvailable:") {
			fields := strings.Fields(line)
			if len(fields) >= 2 {
				if val, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
					memAvailable = val * 1024 // Convert KB to bytes
				}
			}
			break
		}
		if strings.HasPrefix(line, "MemFree:") {
			fields := strings.Fields(line)
			if len(fields) >= 2 {
				if val, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
					memFree = val * 1024 // Convert KB to bytes
				}
			}
		}
	}
	if err := scanner.Err(); err != nil {
		return 0.0
	}

	availableMemGB := float64(memAvailable) / (1024 * 1024 * 1024)
	if availableMemGB > 0 {
		return availableMemGB
	}

	return float64(memFree) / (1024 * 1024 * 1024)
}

// getDarwinAvailableMemoryGB parses vm_stat output to get available memory on macOS
func getDarwinAvailableMemoryGB() float64 {
	cmd := exec.Command("vm_stat")
	output, err := cmd.Output()
	if err != nil {
		return 0.0
	}

	lines := strings.Split(string(output), "\n")
	var pageSize, freePages, inactivePages, purgeablePages int64 = 4096, 0, 0, 0 // default page size

	for _, line := range lines {
		if strings.Contains(line, "page size of") {
			re := regexp.MustCompile(`(\d+) bytes`)
			if matches := re.FindStringSubmatch(line); len(matches) > 1 {
				if val, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
					pageSize = val
				}
			}
		}
		if strings.HasPrefix(line, "Pages free:") {
			re := regexp.MustCompile(`(\d+)`)
			if matches := re.FindStringSubmatch(line); len(matches) > 1 {
				if val, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
					freePages = val
				}
			}
		}
		if strings.HasPrefix(line, "Pages inactive:") {
			re := regexp.MustCompile(`(\d+)`)
			if matches := re.FindStringSubmatch(line); len(matches) > 1 {
				if val, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
					inactivePages = val
				}
			}
		}
		if strings.HasPrefix(line, "Pages purgeable:") {
			re := regexp.MustCompile(`(\d+)`)
			if matches := re.FindStringSubmatch(line); len(matches) > 1 {
				if val, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
					purgeablePages = val
				}
			}
		}
	}

	// Available memory = free + inactive + purgeable (can be reclaimed)
	availablePages := freePages + inactivePages + purgeablePages
	return float64(availablePages*pageSize) / (1024 * 1024 * 1024)
}

func checkLinuxMemory(requiredGB float64) bool {
	file, err := os.Open("/proc/meminfo")
	if err != nil {
		return true // assume OK if can't check
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	var memFree, memAvailable int64

	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "MemAvailable:") {
			fields := strings.Fields(line)
			if len(fields) >= 2 {
				if val, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
					memAvailable = val * 1024 // Convert KB to bytes
				}
			}
			break
		}
		if strings.HasPrefix(line, "MemFree:") {
			fields := strings.Fields(line)
			if len(fields) >= 2 {
				if val, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
					memFree = val * 1024 // Convert KB to bytes
				}
			}
		}
	}
	if err := scanner.Err(); err != nil {
		return true // assume OK if can't check
	}

	availableMemGB := float64(memAvailable) / (1024 * 1024 * 1024)
	if availableMemGB > 0 {
		return availableMemGB >= requiredGB
	}

	freeMemGB := float64(memFree) / (1024 * 1024 * 1024)
	return freeMemGB >= requiredGB
}

func checkDarwinMemory(requiredGB float64) bool {
	cmd := exec.Command("vm_stat")
	output, err := cmd.Output()
	if err != nil {
		return true // assume OK if can't check
	}

	lines := strings.Split(string(output), "\n")
	var pageSize, freePages, inactivePages, purgeablePages int64 = 4096, 0, 0, 0 // default page size

	for _, line := range lines {
		if strings.Contains(line, "page size of") {
			re := regexp.MustCompile(`(\d+) bytes`)
			if matches := re.FindStringSubmatch(line); len(matches) > 1 {
				if val, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
					pageSize = val
				}
			}
		}
		if strings.HasPrefix(line, "Pages free:") {
			re := regexp.MustCompile(`(\d+)`)
			if matches := re.FindStringSubmatch(line); len(matches) > 1 {
				if val, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
					freePages = val
				}
			}
		}
		if strings.HasPrefix(line, "Pages inactive:") {
			re := regexp.MustCompile(`(\d+)`)
			if matches := re.FindStringSubmatch(line); len(matches) > 1 {
				if val, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
					inactivePages = val
				}
			}
		}
		if strings.HasPrefix(line, "Pages purgeable:") {
			re := regexp.MustCompile(`(\d+)`)
			if matches := re.FindStringSubmatch(line); len(matches) > 1 {
				if val, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
					purgeablePages = val
				}
			}
		}
	}

	// Available memory = free + inactive + purgeable (can be reclaimed)
	availablePages := freePages + inactivePages + purgeablePages
	availableMemGB := float64(availablePages*pageSize) / (1024 * 1024 * 1024)
	return availableMemGB >= requiredGB
}

// calculateRequiredDiskGB calculates the disk space required based on worker images and local components
func calculateRequiredDiskGB(workerImageExists bool, localComponents int) float64 {
	// adjust required space based on components and worker images
	if !workerImageExists {
		// need to download worker images (can be large)
		return MinFreeDiskGBForWorkerImages
	} else if localComponents > 0 {
		// have local components that need space for containers/volumes
		return MinFreeDiskGBForComponents + float64(localComponents)*MinFreeDiskGBPerComponents
	}
	// default minimum disk space required
	return MinFreeDiskGB
}

// countLocalComponentsToInstall counts how many components need to be installed locally
func countLocalComponentsToInstall(
	pentagiInstalled,
	graphitiConnected, graphitiExternal, graphitiInstalled,
	langfuseConnected, langfuseExternal, langfuseInstalled,
	obsConnected, obsExternal, obsInstalled bool,
) int {
	localComponents := 0
	if !pentagiInstalled {
		localComponents++
	}
	if graphitiConnected && !graphitiExternal && !graphitiInstalled {
		localComponents++
	}
	if langfuseConnected && !langfuseExternal && !langfuseInstalled {
		localComponents++
	}
	if obsConnected && !obsExternal && !obsInstalled {
		localComponents++
	}
	return localComponents
}

func checkDiskSpaceWithContext(
	ctx context.Context,
	workerImageExists, pentagiInstalled,
	graphitiConnected, graphitiExternal, graphitiInstalled,
	langfuseConnected, langfuseExternal, langfuseInstalled,
	obsConnected, obsExternal, obsInstalled bool,
) bool {
	// determine required disk space based on what needs to be installed locally
	localComponents := countLocalComponentsToInstall(
		pentagiInstalled,
		graphitiConnected, graphitiExternal, graphitiInstalled,
		langfuseConnected, langfuseExternal, langfuseInstalled,
		obsConnected, obsExternal, obsInstalled,
	)

	requiredGB := calculateRequiredDiskGB(workerImageExists, localComponents)

	// check disk space using different methods depending on OS
	switch runtime.GOOS {
	case "linux":
		return checkLinuxDiskSpace(ctx, requiredGB)
	case "darwin":
		return checkDarwinDiskSpace(ctx, requiredGB)
	default:
		return true // assume OK for other systems
	}
}

// getAvailableDiskGB returns the available disk space in GB for the current OS
func getAvailableDiskGB(ctx context.Context) float64 {
	switch runtime.GOOS {
	case "linux":
		return getLinuxAvailableDiskGB(ctx)
	case "darwin":
		return getDarwinAvailableDiskGB(ctx)
	default:
		return 0.0 // unknown for other systems
	}
}

// getLinuxAvailableDiskGB uses df command to get available disk space on Linux
func getLinuxAvailableDiskGB(ctx context.Context) float64 {
	cmd := exec.CommandContext(ctx, "df", "-BG", ".")
	output, err := cmd.Output()
	if err != nil {
		return 0.0
	}

	lines := strings.Split(string(output), "\n")
	if len(lines) < 2 {
		return 0.0
	}

	fields := strings.Fields(lines[1])
	if len(fields) < 4 {
		return 0.0
	}

	availableStr := strings.TrimSuffix(fields[3], "G")
	if available, err := strconv.ParseFloat(availableStr, 64); err == nil {
		return available
	}

	return 0.0
}

// getDarwinAvailableDiskGB uses df command to get available disk space on macOS
func getDarwinAvailableDiskGB(ctx context.Context) float64 {
	cmd := exec.CommandContext(ctx, "df", "-g", ".")
	output, err := cmd.Output()
	if err != nil {
		return 0.0
	}

	lines := strings.Split(string(output), "\n")
	if len(lines) < 2 {
		return 0.0
	}

	fields := strings.Fields(lines[1])
	if len(fields) < 4 {
		return 0.0
	}

	if available, err := strconv.ParseFloat(fields[3], 64); err == nil {
		return available
	}

	return 0.0
}

func checkLinuxDiskSpace(ctx context.Context, requiredGB float64) bool {
	cmd := exec.CommandContext(ctx, "df", "-BG", ".")
	output, err := cmd.Output()
	if err != nil {
		return true // assume OK if can't check
	}

	lines := strings.Split(string(output), "\n")
	if len(lines) < 2 {
		return true
	}

	fields := strings.Fields(lines[1])
	if len(fields) < 4 {
		return true
	}

	availableStr := strings.TrimSuffix(fields[3], "G")
	if available, err := strconv.ParseFloat(availableStr, 64); err == nil {
		return available >= requiredGB
	}

	return true
}

func checkDarwinDiskSpace(ctx context.Context, requiredGB float64) bool {
	cmd := exec.CommandContext(ctx, "df", "-g", ".")
	output, err := cmd.Output()
	if err != nil {
		return true // assume OK if can't check
	}

	lines := strings.Split(string(output), "\n")
	if len(lines) < 2 {
		return true
	}

	fields := strings.Fields(lines[1])
	if len(fields) < 4 {
		return true
	}

	if available, err := strconv.ParseFloat(fields[3], 64); err == nil {
		return available >= requiredGB
	}

	return true
}

func getNetworkFailures(ctx context.Context, proxyURL string, dockerClient, workerClient *client.Client) []string {
	var failures []string

	// 1. DNS resolution test
	if !checkDNSResolution("docker.io") {
		// Using hardcoded string here to avoid circular dependency with locale package
		failures = append(failures, "• DNS resolution failed for docker.io")
	}

	// 2. HTTP connectivity test
	if !checkHTTPConnectivity(ctx, proxyURL) {
		// Using hardcoded string here to avoid circular dependency with locale package
		failures = append(failures, "• Cannot reach external services via HTTPS")
	}

	// 3. Docker pull test (only if both clients are available)
	if dockerClient != nil && workerClient != nil && !checkDockerPullConnectivity(ctx, dockerClient, workerClient) {
		// Using hardcoded string here to avoid circular dependency with locale package
		failures = append(failures, "• Cannot pull Docker images from registry")
	}

	return failures
}

func checkImageExists(ctx context.Context, cli *client.Client, imageName string) bool {
	imageInfo := getImageInfo(ctx, cli, imageName)
	return imageInfo != nil && imageInfo.Hash != ""
}

func getImageInfo(ctx context.Context, cli *client.Client, imageName string) *ImageInfo {
	if cli == nil {
		return nil
	}

	images, err := cli.ImageList(ctx, client.ImageListOptions{})
	if err != nil {
		return nil
	}

	imageInfo := parseImageRef(imageName, "")
	if imageInfo == nil {
		return nil
	}

	fullImageName := imageInfo.Name + ":" + imageInfo.Tag
	for _, img := range images.Items {
		for _, tag := range img.RepoTags {
			if tag == imageName || tag == fullImageName {
				imageInfo.Hash = img.ID
				break
			}
		}
	}

	return imageInfo
}

// isDockerHubHost reports whether a leading path element is one of the names
// Docker Hub answers to. It is a closed list and not a shape test: "contains a
// dot or a colon" describes every registry there is.
func isDockerHubHost(head string) bool {
	switch head {
	case "docker.io", "index.docker.io", "registry-1.docker.io", "registry.hub.docker.com":
		return true
	default:
		return false
	}
}

// parseImageRef splits a Docker image reference into the repository and tag the update
// server matches on, plus whatever digest the reference or the daemon carried.
//
// The shape is [registry[:port]/]path[:tag][@digest]. The colon of a registry port and the
// colon of a tag are told apart by position, not by content: a tag colon is the last colon
// after the last slash, so a version-numbered tag such as "11.4.0" is not taken for a port.
// A mangled pair fails silently: the update server matches on the repository and tag pair,
// and a pair it does not track is offered nothing, which reads as "nothing to update".
func parseImageRef(imageRef, imageID string) *ImageInfo {
	if imageRef == "" {
		return nil
	}

	info := &ImageInfo{
		Hash: imageID,
	}

	originalRef := imageRef

	// A digest pins the reference and is not part of the name.
	if base, digest, found := strings.Cut(imageRef, "@"); found {
		imageRef = base
		if digest != "" {
			info.Hash = digest
		}
	}

	// The registry host is dropped ONLY for Docker Hub, and keeping it everywhere
	// else is the whole point.
	//
	// The service stores a reference the way compose writes it: bare for Docker
	// Hub, host and all for anywhere else — `gcr.io/cadvisor/cadvisor`, not
	// `cadvisor/cadvisor`. Those two are different images: the second is a Docker
	// Hub repository that has nothing to do with cAdvisor. The match is an exact
	// string comparison, so a reference of another registry reported without its
	// host is answered `repository_not_tracked` and takes its whole stack's
	// resolution down to `not_tracked`.
	if head, rest, found := strings.Cut(imageRef, "/"); found {
		if isDockerHubHost(head) {
			imageRef = rest
		}
	}

	// Docker Hub spells an official image "library/nginx" in full; every other place spells
	// it "nginx", and that is the form the server is asked about.
	imageRef = strings.TrimPrefix(imageRef, "library/")

	// The tag is what follows the last colon, and only when that colon comes after the last
	// slash — otherwise it belongs to a registry port that was kept as part of the name.
	name, tag := imageRef, ""
	if colon := strings.LastIndex(imageRef, ":"); colon > strings.LastIndex(imageRef, "/") {
		name, tag = imageRef[:colon], imageRef[colon+1:]
	}

	info.Name, info.Tag = name, tag
	if info.Tag == "" {
		info.Tag = "latest"
	}
	if info.Name == "" {
		info.Name = originalRef
		info.Tag = "latest"
	}

	return info
}

func checkDNSResolution(hostname string) bool {
	_, err := net.LookupHost(hostname)
	return err == nil
}

func checkHTTPConnectivity(ctx context.Context, proxyURL string) bool {
	client := &http.Client{
		Timeout: 5 * time.Second,
	}

	if proxyURL != "" {
		if proxyURLParsed, err := url.Parse(proxyURL); err == nil {
			client.Transport = &http.Transport{
				Proxy: http.ProxyURL(proxyURLParsed),
			}
		}
	}

	req, err := http.NewRequestWithContext(ctx, "GET", "https://docker.io", nil)
	if err != nil {
		return false
	}

	resp, err := client.Do(req)
	if err != nil {
		return false
	}
	defer resp.Body.Close()

	return resp.StatusCode < 500 // accept any non-server-error response
}

func checkDockerPullConnectivity(ctx context.Context, dockerClient, workerClient *client.Client) bool {
	// test with main docker client
	if dockerClient != nil {
		if checkSingleDockerPull(ctx, dockerClient, DefaultImage) {
			return true
		}
	}

	// test with worker client
	if workerClient != nil {
		if checkSingleDockerPull(ctx, workerClient, DefaultImage) {
			return true
		}
	}

	return false
}

func checkSingleDockerPull(ctx context.Context, cli *client.Client, imageName string) bool {
	// try to pull the image
	reader, err := cli.ImagePull(ctx, imageName, client.ImagePullOptions{})
	if err != nil {
		return false
	}
	defer reader.Close()

	// read a bit from the response to ensure it's working
	buf := make([]byte, 1024)
	_, err = reader.Read(buf)
	return err == nil || errors.Is(err, io.EOF) || errors.Is(err, syscall.EIO)
}
