package daemon

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/containerd/containerd/v2/core/remotes/docker"
	hostconfig "github.com/containerd/containerd/v2/core/remotes/docker/config"
	cerrdefs "github.com/containerd/errdefs"
	"github.com/docker/docker/registry"
	"github.com/pkg/errors"
)

const (
	defaultPath = "/v2"
)

// RegistryHosts returns the registry hosts configuration for the host component
// of a distribution image reference.
func (daemon *Daemon) RegistryHosts(host string) ([]docker.RegistryHost, error) {
	hosts, err := hostconfig.ConfigureHosts(context.Background(), hostconfig.HostOptions{
		// TODO: Also check containerd path when updating containerd to use multiple host directories
		HostDir: hostconfig.HostDirFromRoot(registry.CertsDir()),
	})(host)
	if err == nil {
		// Merge in legacy configuration if provided
		if cfg := daemon.config().Config; len(cfg.Mirrors) > 0 || len(cfg.InsecureRegistries) > 0 {
			hosts, err = daemon.mergeLegacyConfig(host, hosts)
		}
	}

	return hosts, err
}

func (daemon *Daemon) mergeLegacyConfig(host string, hosts []docker.RegistryHost) ([]docker.RegistryHost, error) {
	// If no hosts provided, nothing to do.
	// If multiple hosts provided, then a mirror configuration is already provided and
	// should not overwrite with legacy config.
	if len(hosts) == 0 || len(hosts) > 1 {
		return hosts, nil
	}
	sc := daemon.registryService.ServiceConfig()
	if host == "docker.io" && len(sc.Mirrors) > 0 {
		hosts = mirrorsToRegistryHosts(sc.Mirrors, hosts[0])
	}
	hostDir := hostconfig.HostDirFromRoot(registry.CertsDir())
	for i := range hosts {
		t, ok := hosts[i].Client.Transport.(*http.Transport)
		if !ok {
			continue
		}
		if t.TLSClientConfig == nil {
			certsDir, err := hostDir(host)
			if err != nil && !cerrdefs.IsNotFound(err) {
				return nil, err
			} else if err == nil {
				c, err := loadTLSConfig(certsDir)
				if err != nil {
					return nil, err
				}
				t.TLSClientConfig = c
			}
		}
		if daemon.registryService.IsInsecureRegistry(hosts[i].Host) {
			if t.TLSClientConfig == nil {
				t.TLSClientConfig = &tls.Config{} //nolint: gosec // G402: TLS MinVersion too low.
			}
			t.TLSClientConfig.InsecureSkipVerify = true

			hosts[i].Client.Transport = docker.NewHTTPFallback(hosts[i].Client.Transport)
		}
	}
	return hosts, nil
}

func mirrorsToRegistryHosts(mirrors []string, dHost docker.RegistryHost) []docker.RegistryHost {
	var mirrorHosts []docker.RegistryHost
	for _, mirror := range mirrors {
		h := dHost
		h.Capabilities = docker.HostCapabilityPull | docker.HostCapabilityResolve

		u, err := url.Parse(mirror)
		if err != nil || u.Host == "" {
			u, err = url.Parse(fmt.Sprintf("dummy://%s", mirror))
		}
		if err == nil && u.Host != "" {
			h.Host = u.Host
			h.Path = strings.TrimSuffix(u.Path, "/")

			// For compatibility with legacy mirrors, ensure ends with /v2
			// NOTE: Use newer configuration to completely override the path
			if !strings.HasSuffix(h.Path, defaultPath) {
				h.Path = path.Join(h.Path, defaultPath)
			}
			if u.Scheme != "dummy" {
				h.Scheme = u.Scheme
			}
		} else {
			h.Host = mirror
			h.Path = defaultPath
		}

		mirrorHosts = append(mirrorHosts, h)
	}
	return append(mirrorHosts, dHost)
}

func loadTLSConfig(d string) (*tls.Config, error) {
	fs, err := os.ReadDir(d)
	if err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, os.ErrPermission) {
		return nil, errors.WithStack(err)
	}
	type keyPair struct {
		Certificate string
		Key         string
	}
	var (
		rootCAs  []string
		keyPairs []keyPair
	)
	for _, f := range fs {
		switch filepath.Ext(f.Name()) {
		case ".crt":
			rootCAs = append(rootCAs, filepath.Join(d, f.Name()))
		case ".cert":
			keyPairs = append(keyPairs, keyPair{
				Certificate: filepath.Join(d, f.Name()),
				Key:         filepath.Join(d, strings.TrimSuffix(f.Name(), ".cert")+".key"),
			})
		}
	}

	tc := &tls.Config{
		MinVersion: tls.VersionTLS12,
	}
	if len(rootCAs) > 0 {
		systemPool, err := x509.SystemCertPool()
		if err != nil {
			if runtime.GOOS == "windows" {
				systemPool = x509.NewCertPool()
			} else {
				return nil, errors.Wrapf(err, "unable to get system cert pool")
			}
		}
		tc.RootCAs = systemPool
	}

	for _, p := range rootCAs {
		dt, err := os.ReadFile(p)
		if err != nil {
			return nil, err
		}
		tc.RootCAs.AppendCertsFromPEM(dt)
	}

	for _, kp := range keyPairs {
		cert, err := tls.LoadX509KeyPair(kp.Certificate, kp.Key)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to load keypair for %s", kp.Certificate)
		}
		tc.Certificates = append(tc.Certificates, cert)
	}
	return tc, nil
}
