package plugin

import (
	"context"
	"crypto/tls"
	"net"
	"net/http"
	"time"

	"github.com/containerd/containerd/v2/core/remotes"
	"github.com/containerd/containerd/v2/core/remotes/docker"
	"github.com/containerd/log"
	"github.com/distribution/reference"
	"github.com/docker/docker/api/types/registry"
	"github.com/docker/docker/dockerversion"
	"github.com/pkg/errors"
)

// scope builds the correct auth scope for the registry client to authorize against
// By default the client currently only does a "repository:" scope with out a classifier, e.g. "(plugin)"
// Without this, the client will not be able to authorize the request
func scope(ref reference.Named, push bool) string {
	scope := "repository(plugin):" + reference.Path(reference.TrimNamed(ref)) + ":pull"
	if push {
		scope += ",push"
	}
	return scope
}

func (pm *Manager) newResolver(ctx context.Context, tracker docker.StatusTracker, auth *registry.AuthConfig, headers http.Header, httpFallback bool) (remotes.Resolver, error) {
	if headers == nil {
		headers = http.Header{}
	}
	headers.Add("User-Agent", dockerversion.DockerUserAgent(ctx))

	return docker.NewResolver(docker.ResolverOptions{
		Tracker: tracker,
		Headers: headers,
		Hosts:   pm.registryHostsFn(auth, httpFallback),
	}), nil
}

func registryHTTPClient(config *tls.Config) *http.Client {
	return &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyFromEnvironment,
			DialContext: (&net.Dialer{
				Timeout:   30 * time.Second,
				KeepAlive: 30 * time.Second,
			}).DialContext,
			TLSClientConfig:     config,
			TLSHandshakeTimeout: 10 * time.Second,
			IdleConnTimeout:     30 * time.Second,
		},
	}
}

func (pm *Manager) registryHostsFn(auth *registry.AuthConfig, httpFallback bool) docker.RegistryHosts {
	return func(hostname string) ([]docker.RegistryHost, error) {
		eps, err := pm.config.RegistryService.LookupPullEndpoints(hostname)
		if err != nil {
			return nil, errors.Wrapf(err, "error resolving repository for %s", hostname)
		}

		hosts := make([]docker.RegistryHost, 0, len(eps))

		for _, ep := range eps {
			// forced http fallback is used only for push since the containerd pusher only ever uses the first host we
			// pass to it.
			// So it is the callers responsibility to retry with this flag set.
			if httpFallback && ep.URL.Scheme != "http" {
				log.G(context.TODO()).WithField("registryHost", hostname).WithField("endpoint", ep).Debugf("Skipping non-http endpoint")
				continue
			}

			caps := docker.HostCapabilityPull | docker.HostCapabilityResolve
			if !ep.Mirror {
				caps = caps | docker.HostCapabilityPush
			}

			host, err := docker.DefaultHost(ep.URL.Host)
			if err != nil {
				return nil, err
			}

			client := registryHTTPClient(ep.TLSConfig)
			hosts = append(hosts, docker.RegistryHost{
				Host:         host,
				Scheme:       ep.URL.Scheme,
				Client:       client,
				Path:         "/v2",
				Capabilities: caps,
				Authorizer: docker.NewDockerAuthorizer(
					docker.WithAuthClient(client),
					docker.WithAuthCreds(func(_ string) (string, string, error) {
						if auth.IdentityToken != "" {
							return "", auth.IdentityToken, nil
						}
						return auth.Username, auth.Password, nil
					}),
				),
			})
		}
		log.G(context.TODO()).WithField("registryHost", hostname).WithField("hosts", hosts).Debug("Resolved registry hosts")

		return hosts, nil
	}
}
