// Package regclient is used to access OCI registries.
package regclient

import (
	"io"
	"log/slog"
	"time"

	"fmt"

	"github.com/regclient/regclient/config"
	"github.com/regclient/regclient/internal/version"
	"github.com/regclient/regclient/scheme"
	"github.com/regclient/regclient/scheme/ocidir"
	"github.com/regclient/regclient/scheme/reg"
)

const (
	// DefaultUserAgent sets the header on http requests.
	DefaultUserAgent = "regclient/regclient"
	// DockerCertDir default location for docker certs.
	DockerCertDir = "/etc/docker/certs.d"
	// DockerRegistry is the well known name of Docker Hub, "docker.io".
	DockerRegistry = config.DockerRegistry
	// DockerRegistryAuth is the name of Docker Hub seen in docker's config.json.
	DockerRegistryAuth = config.DockerRegistryAuth
	// DockerRegistryDNS is the actual registry DNS name for Docker Hub.
	DockerRegistryDNS = config.DockerRegistryDNS
)

// RegClient is used to access OCI distribution-spec registries.
type RegClient struct {
	hosts       map[string]*config.Host
	hostDefault *config.Host
	regOpts     []reg.Opts
	schemes     map[string]scheme.API
	slog        *slog.Logger
	userAgent   string
}

// Opt functions are used by [New] to create a [*RegClient].
type Opt func(*RegClient)

// New returns a registry client.
func New(opts ...Opt) *RegClient {
	var rc = RegClient{
		hosts:     map[string]*config.Host{},
		userAgent: DefaultUserAgent,
		regOpts:   []reg.Opts{},
		schemes:   map[string]scheme.API{},
		slog:      slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})),
	}

	info := version.GetInfo()
	if info.VCSTag != "" {
		rc.userAgent = fmt.Sprintf("%s (%s)", rc.userAgent, info.VCSTag)
	} else {
		rc.userAgent = fmt.Sprintf("%s (%s)", rc.userAgent, info.VCSRef)
	}

	// inject Docker Hub settings
	_ = rc.hostSet(*config.HostNewName(config.DockerRegistryAuth))

	for _, opt := range opts {
		opt(&rc)
	}

	// configure regOpts
	hostList := []*config.Host{}
	for _, h := range rc.hosts {
		hostList = append(hostList, h)
	}
	rc.regOpts = append(rc.regOpts,
		reg.WithConfigHosts(hostList),
		reg.WithConfigHostDefault(rc.hostDefault),
		reg.WithSlog(rc.slog),
		reg.WithUserAgent(rc.userAgent),
	)

	// setup scheme's
	rc.schemes["reg"] = reg.New(rc.regOpts...)
	rc.schemes["ocidir"] = ocidir.New(
		ocidir.WithSlog(rc.slog),
	)

	rc.slog.Debug("regclient initialized",
		slog.String("VCSRef", info.VCSRef),
		slog.String("VCSTag", info.VCSTag))

	return &rc
}

// WithBlobLimit sets the max size for chunked blob uploads which get stored in memory.
//
// Deprecated: replace with WithRegOpts(reg.WithBlobLimit(limit)), see [WithRegOpts] and [reg.WithBlobLimit].
func WithBlobLimit(limit int64) Opt {
	return func(rc *RegClient) {
		rc.regOpts = append(rc.regOpts, reg.WithBlobLimit(limit))
	}
}

// WithBlobSize overrides default blob sizes.
//
// Deprecated: replace with WithRegOpts(reg.WithBlobSize(chunk, max)), see [WithRegOpts] and [reg.WithBlobSize].
func WithBlobSize(chunk, max int64) Opt {
	return func(rc *RegClient) {
		rc.regOpts = append(rc.regOpts, reg.WithBlobSize(chunk, max))
	}
}

// WithCertDir adds a path of certificates to trust similar to Docker's /etc/docker/certs.d.
//
// Deprecated: replace with WithRegOpts(reg.WithCertDirs(path)), see [WithRegOpts] and [reg.WithCertDirs].
func WithCertDir(path ...string) Opt {
	return func(rc *RegClient) {
		rc.regOpts = append(rc.regOpts, reg.WithCertDirs(path))
	}
}

// WithConfigHost adds a list of config host settings.
func WithConfigHost(configHost ...config.Host) Opt {
	return func(rc *RegClient) {
		rc.hostLoad("host", configHost)
	}
}

// WithConfigHostDefault adds default settings for new hosts.
func WithConfigHostDefault(configHost config.Host) Opt {
	return func(rc *RegClient) {
		rc.hostDefault = &configHost
	}
}

// WithConfigHosts adds a list of config host settings.
//
// Deprecated: replace with [WithConfigHost].
func WithConfigHosts(configHosts []config.Host) Opt {
	return WithConfigHost(configHosts...)
}

// WithDockerCerts adds certificates trusted by docker in /etc/docker/certs.d.
func WithDockerCerts() Opt {
	return WithCertDir(DockerCertDir)
}

// WithDockerCreds adds configuration from users docker config with registry logins.
// This changes the default value from the config file, and should be added after the config file is loaded.
func WithDockerCreds() Opt {
	return func(rc *RegClient) {
		configHosts, err := config.DockerLoad()
		if err != nil {
			rc.slog.Warn("Failed to load docker creds",
				slog.String("err", err.Error()))
			return
		}
		rc.hostLoad("docker", configHosts)
	}
}

// WithDockerCredsFile adds configuration from a named docker config file with registry logins.
// This changes the default value from the config file, and should be added after the config file is loaded.
func WithDockerCredsFile(fname string) Opt {
	return func(rc *RegClient) {
		configHosts, err := config.DockerLoadFile(fname)
		if err != nil {
			rc.slog.Warn("Failed to load docker creds",
				slog.String("err", err.Error()))
			return
		}
		rc.hostLoad("docker-file", configHosts)
	}
}

// WithRegOpts passes through opts to the reg scheme.
func WithRegOpts(opts ...reg.Opts) Opt {
	return func(rc *RegClient) {
		if len(opts) == 0 {
			return
		}
		rc.regOpts = append(rc.regOpts, opts...)
	}
}

// WithRetryDelay specifies the time permitted for retry delays.
//
// Deprecated: replace with WithRegOpts(reg.WithDelay(delayInit, delayMax)), see [WithRegOpts] and [reg.WithDelay].
func WithRetryDelay(delayInit, delayMax time.Duration) Opt {
	return func(rc *RegClient) {
		rc.regOpts = append(rc.regOpts, reg.WithDelay(delayInit, delayMax))
	}
}

// WithRetryLimit specifies the number of retries for non-fatal errors.
//
// Deprecated: replace with WithRegOpts(reg.WithRetryLimit(retryLimit)), see [WithRegOpts] and [reg.WithRetryLimit].
func WithRetryLimit(retryLimit int) Opt {
	return func(rc *RegClient) {
		rc.regOpts = append(rc.regOpts, reg.WithRetryLimit(retryLimit))
	}
}

// WithSlog configures the slog Logger.
func WithSlog(slog *slog.Logger) Opt {
	return func(rc *RegClient) {
		rc.slog = slog
	}
}

// WithUserAgent specifies the User-Agent http header.
func WithUserAgent(ua string) Opt {
	return func(rc *RegClient) {
		rc.userAgent = ua
	}
}

func (rc *RegClient) hostLoad(src string, hosts []config.Host) {
	for _, configHost := range hosts {
		if configHost.Name == "" {
			if configHost.Pass != "" {
				configHost.Pass = "***"
			}
			if configHost.Token != "" {
				configHost.Token = "***"
			}
			rc.slog.Warn("Ignoring registry config without a name",
				slog.Any("entry", configHost))
			continue
		}
		if configHost.Name == DockerRegistry || configHost.Name == DockerRegistryDNS || configHost.Name == DockerRegistryAuth {
			configHost.Name = DockerRegistry
			if configHost.Hostname == "" || configHost.Hostname == DockerRegistry || configHost.Hostname == DockerRegistryAuth {
				configHost.Hostname = DockerRegistryDNS
			}
		}
		tls, _ := configHost.TLS.MarshalText()
		rc.slog.Debug("Loading config",
			slog.Int64("blobChunk", configHost.BlobChunk),
			slog.Int64("blobMax", configHost.BlobMax),
			slog.String("helper", configHost.CredHelper),
			slog.String("hostname", configHost.Hostname),
			slog.Any("mirrors", configHost.Mirrors),
			slog.String("name", configHost.Name),
			slog.String("pathPrefix", configHost.PathPrefix),
			slog.Bool("repoAuth", configHost.RepoAuth),
			slog.String("source", src),
			slog.String("tls", string(tls)),
			slog.String("user", configHost.User))
		err := rc.hostSet(configHost)
		if err != nil {
			rc.slog.Warn("Failed to update host config",
				slog.String("host", configHost.Name),
				slog.String("user", configHost.User),
				slog.String("error", err.Error()))
		}
	}
}

func (rc *RegClient) hostSet(newHost config.Host) error {
	name := newHost.Name
	var err error
	if _, ok := rc.hosts[name]; !ok {
		// merge newHost with default host settings
		rc.hosts[name] = config.HostNewDefName(rc.hostDefault, name)
		err = rc.hosts[name].Merge(newHost, nil)
	} else {
		// merge newHost with existing settings
		err = rc.hosts[name].Merge(newHost, rc.slog)
	}
	if err != nil {
		return err
	}
	return nil
}
