package docker

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"github.com/moby/moby/api/types/registry"
	"github.com/moby/moby/client"
)

// ExtractPort parses the first exposed port from Docker EXPOSE entries.
// Entries are in the form "8080/tcp" or "8080". Returns 0 if none found.
func ExtractPort(exposedPorts []string) int {
	if len(exposedPorts) == 0 {
		return 0
	}
	raw := exposedPorts[0]
	if idx := strings.Index(raw, "/"); idx != -1 {
		raw = raw[:idx]
	}
	port, _ := strconv.Atoi(raw)
	return port
}

// ImageInfo holds metadata extracted from a Docker image inspection.
type ImageInfo struct {
	// ExposedPorts lists the ports declared via EXPOSE in the Dockerfile (e.g. ["8080/tcp"]).
	ExposedPorts []string

	// Healthcheck is the CMD string from the image's HEALTHCHECK instruction, if any.
	Healthcheck string

	// Labels are the key-value pairs defined in the image metadata.
	Labels map[string]string
}

// PullImage pulls an image from a registry. If authConfig is non-empty, it is
// used as the base64-encoded JSON auth payload for private registries.
// The image reference should be in the form "repository:tag".
func (c *Client) PullImage(ctx context.Context, imageRef string, tag string, authConfig string) error {
	ref := imageRef
	if tag != "" {
		ref = imageRef + ":" + tag
	}

	opts := client.ImagePullOptions{}
	if authConfig != "" {
		opts.RegistryAuth = authConfig
	}

	reader, err := c.api.ImagePull(ctx, ref, opts)
	if err != nil {
		return fmt.Errorf("pull image %s: %w", ref, err)
	}

	// Wait for the pull to complete.
	if err := reader.Wait(ctx); err != nil {
		return fmt.Errorf("wait for pull of %s: %w", ref, err)
	}

	return nil
}

// InspectImage retrieves metadata from a local image.
func (c *Client) InspectImage(ctx context.Context, imageRef string) (ImageInfo, error) {
	inspectResult, err := c.api.ImageInspect(ctx, imageRef)
	if err != nil {
		return ImageInfo{}, fmt.Errorf("inspect image %s: %w", imageRef, err)
	}

	info := ImageInfo{}

	// Extract labels from Config if available.
	if inspectResult.Config != nil {
		info.Labels = inspectResult.Config.Labels

		// Extract exposed ports from OCI config (map[string]struct{}).
		for port := range inspectResult.Config.ExposedPorts {
			info.ExposedPorts = append(info.ExposedPorts, port)
		}

		// Extract healthcheck command.
		if inspectResult.Config.Healthcheck != nil && len(inspectResult.Config.Healthcheck.Test) > 0 {
			// The Test slice is ["CMD", "arg1", "arg2", ...] or ["CMD-SHELL", "cmd string"].
			// Join all parts after the first element for a readable representation.
			if len(inspectResult.Config.Healthcheck.Test) > 1 {
				info.Healthcheck = joinArgs(inspectResult.Config.Healthcheck.Test[1:])
			}
		}
	}

	return info, nil
}

// EncodeRegistryAuth builds a base64-encoded JSON auth string suitable for
// Docker API calls. Pass empty strings for anonymous access.
func EncodeRegistryAuth(username, password, serverAddress string) (string, error) {
	cfg := registry.AuthConfig{
		Username:      username,
		Password:      password,
		ServerAddress: serverAddress,
	}

	data, err := json.Marshal(cfg)
	if err != nil {
		return "", fmt.Errorf("encode registry auth: %w", err)
	}

	return base64.URLEncoding.EncodeToString(data), nil
}

// joinArgs joins string arguments with spaces.
func joinArgs(args []string) string {
	return strings.Join(args, " ")
}
