package libstack

import (
	"context"
	"fmt"
	"maps"
	"sync"

	"github.com/portainer/portainer/api/logs"

	"github.com/docker/cli/cli/command"
	configtypes "github.com/docker/cli/cli/config/types"
	"github.com/docker/cli/cli/flags"
	"github.com/docker/docker/registry"
	"github.com/rs/zerolog/log"
)

// DockerCliOptions holds the settings required to initialise a DockerCli.
type DockerCliOptions struct {
	Host       string
	Registries []configtypes.AuthConfig
	// Headers are injected as custom HTTP headers on every request the client makes.
	Headers map[string]string
}

// mu serialises calls to cli.Initialize across all deployer types (compose and
// swarm) to prevent concurrent initialisation of the Docker client config.
var mu sync.Mutex

// WithCli creates and initialises a DockerCli, injects registry credentials,
// and calls cliFn with the ready client. The client is closed after cliFn returns.
func WithCli(
	ctx context.Context, //nolint:staticcheck
	options DockerCliOptions,
	cliFn func(context.Context, *command.DockerCli) error,
) error {
	ctx = context.Background() //nolint:staticcheck

	cli, err := command.NewDockerCli(command.WithCombinedStreams(log.Logger))
	if err != nil {
		return fmt.Errorf("unable to create a Docker client: %w", err)
	}

	opts := flags.NewClientOptions()
	if options.Host != "" {
		opts.Hosts = []string{options.Host}
	}

	mu.Lock()
	if err := cli.Initialize(opts); err != nil {
		mu.Unlock()
		return fmt.Errorf("unable to initialize the Docker client: %w", err)
	}
	mu.Unlock()

	// Inject custom headers before the first Client() call, which is where the
	// Docker client is lazily created and reads ConfigFile().HTTPHeaders.
	if len(options.Headers) > 0 {
		cfg := cli.ConfigFile()
		if cfg.HTTPHeaders == nil {
			cfg.HTTPHeaders = make(map[string]string)
		}
		maps.Copy(cfg.HTTPHeaders, options.Headers)
	}

	defer logs.CloseAndLogErr(cli.Client())

	for _, r := range options.Registries {
		if r.ServerAddress == "" || r.ServerAddress == registry.DefaultNamespace {
			r.ServerAddress = registry.IndexServer
		}

		cli.ConfigFile().AuthConfigs[r.ServerAddress] = r
	}

	// Docker resolves credentials in the following priority:
	// 1. credHelpers – per-registry credential helpers
	// 2. credsStore  – global credential store used for all registries
	// 3. auths       – inline credentials defined in config.json
	//
	// Many Docker Desktop users (Windows/macOS) have a global credsStore configured
	// by default (e.g. "desktop.exe" on Windows or "osxkeychain" on macOS). These
	// global stores often do not include credentials for the custom registries
	// defined in Portainer stacks, leading to authentication failures.
	//
	// To avoid this, when inline credentials are provided for one or more registries,
	// we intentionally clear the global credsStore. This ensures Docker uses the
	// credentials configured in Portainer instead of falling back to an empty global
	// store.
	//
	// If no inline credentials are configured in Portainer, we keep the credsStore
	// so Docker can still use it as a fallback.
	// credHelpers are not affected as they are external services managed by the user.
	// @ref: https://linear.app/portainer/issue/BE-12237
	if len(options.Registries) > 0 {
		cli.ConfigFile().CredentialsStore = ""
	}

	return cliFn(ctx, cli)
}
