package dockerversion

import (
	"context"
	"runtime"
	"strings"
	"sync"

	"github.com/moby/moby/v2/pkg/parsers/kernel"
	"github.com/moby/moby/v2/pkg/useragent"
)

// uaStringKey is used as key type for user-agent string in net/context struct
type uaStringKey struct{}

// WithUpstreamUserAgent returns a new context carrying the upstream client's
// User-Agent string.
func WithUpstreamUserAgent(ctx context.Context, ua string) context.Context {
	if ua == "" {
		return ctx
	}
	return context.WithValue(ctx, uaStringKey{}, ua)
}

// DockerUserAgent is the User-Agent used by the daemon.
//
// It consists of the daemon's User-Agent, optional version metadata, and
// an optional upstream client comment:
//
//	[daemon user agent] [extra] [UpstreamClient(<upstream-user-agent>)]
//
// "UpstreamClient" is a Docker-defined convention. The upstream value is
// sanitized before inclusion. See [RFC9110], section 10.1.5.
//
// [RFC9110]: https://www.rfc-editor.org/rfc/rfc9110#section-10.1.5
func DockerUserAgent(ctx context.Context, extraVersions ...useragent.VersionInfo) string {
	ua := useragent.AppendVersions(getDaemonUserAgent(), extraVersions...)
	if upstreamUA := getUpstreamUserAgent(ctx); upstreamUA != "" {
		ua += " " + upstreamUA
	}
	return ua
}

var (
	daemonUAOnce sync.Once
	daemonUA     string
)

// getDaemonUserAgent returns the user-agent to use for requests made by
// the daemon.
//
// It includes:
//
// - the docker version
// - go version
// - git-commit
// - kernel version
// - os
// - architecture
func getDaemonUserAgent() string {
	daemonUAOnce.Do(func() {
		httpVersion := make([]useragent.VersionInfo, 0, 6)
		httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: Version})
		httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()})
		httpVersion = append(httpVersion, useragent.VersionInfo{Name: "git-commit", Version: GitCommit})
		if kernelVersion, err := kernel.GetKernelVersion(); err == nil {
			httpVersion = append(httpVersion, useragent.VersionInfo{Name: "kernel", Version: kernelVersion.String()})
		}
		httpVersion = append(httpVersion, useragent.VersionInfo{Name: "os", Version: runtime.GOOS})
		httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH})
		daemonUA = useragent.AppendVersions("", httpVersion...)
	})
	return daemonUA
}

// getUpstreamUserAgent returns the previously saved user-agent context stored
// in ctx, if one exists, and formats it as:
//
//	UpstreamClient(<upstream user agent string>)
//
// It returns an empty string if no user-agent is present in the context.
func getUpstreamUserAgent(ctx context.Context) string {
	upstreamUA, ok := ctx.Value(uaStringKey{}).(string)
	if !ok || upstreamUA == "" {
		return ""
	}

	return "UpstreamClient(" + escapeStr(upstreamUA) + ")"
}

// escapeStr escapes and sanitizes s for use in a User-Agent comment ([RFC9110]).
//
// [RFC9110]: https://www.rfc-editor.org/rfc/rfc9110#section-10.1.5
func escapeStr(s string) string {
	var b strings.Builder
	b.Grow(len(s))

	for i := range len(s) {
		switch c := s[i]; c {
		// TODO(thaJeztah): remove redundant escaping semicolons; see https://github.com/moby/moby/pull/52356#discussion_r3234266285
		case '(', ')', ';', '\\':
			b.WriteByte('\\')
			b.WriteByte(c)
		case '\t':
			b.WriteByte(c)
		default:
			if c >= 0x20 && c != 0x7f {
				b.WriteByte(c)
			}
		}
	}

	return b.String()
}