package handlers

import (
	"context"
	"fmt"
	"net/http"

	"github.com/distribution/distribution/v3"
	"github.com/distribution/distribution/v3/internal/dcontext"
	"github.com/distribution/distribution/v3/registry/api/errcode"
	v2 "github.com/distribution/distribution/v3/registry/api/v2"
	"github.com/distribution/distribution/v3/registry/auth"
	"github.com/opencontainers/go-digest"
)

// Context should contain the request specific context for use in across
// handlers. Resources that don't need to be shared across handlers should not
// be on this object.
type Context struct {
	// App points to the application structure that created this context.
	*App
	context.Context

	// Repository is the repository for the current request. All requests
	// should be scoped to a single repository. This field may be nil.
	Repository distribution.Repository

	// RepositoryRemover provides method to delete a repository
	RepositoryRemover distribution.RepositoryRemover

	// Errors is a collection of errors encountered during the request to be
	// returned to the client API. If errors are added to the collection, the
	// handler *must not* start the response via http.ResponseWriter.
	Errors errcode.Errors

	urlBuilder *v2.URLBuilder

	// TODO(stevvooe): The goal is too completely factor this context and
	// dispatching out of the web application. Ideally, we should lean on
	// context.Context for injection of these resources.
}

// Value overrides context.Context.Value to ensure that calls are routed to
// correct context.
func (ctx *Context) Value(key any) any {
	return ctx.Context.Value(key)
}

func getName(ctx context.Context) (name string) {
	return dcontext.GetStringValue(ctx, "vars.name")
}

func getReference(ctx context.Context) (reference string) {
	return dcontext.GetStringValue(ctx, "vars.reference")
}

var errDigestNotAvailable = fmt.Errorf("digest not available in context")

func getDigest(ctx context.Context) (dgst digest.Digest, err error) {
	dgstStr := dcontext.GetStringValue(ctx, "vars.digest")

	if dgstStr == "" {
		dcontext.GetLogger(ctx).Errorf("digest not available")
		return "", errDigestNotAvailable
	}

	d, err := digest.Parse(dgstStr)
	if err != nil {
		dcontext.GetLogger(ctx).Errorf("error parsing digest=%q: %v", dgstStr, err)
		return "", err
	}

	return d, nil
}

func getUploadUUID(ctx context.Context) (uuid string) {
	return dcontext.GetStringValue(ctx, "vars.uuid")
}

const (
	// userKey is used to get the user object from
	// a user context
	userKey = "auth.user"

	// userNameKey is used to get the user name from
	// a user context
	userNameKey = "auth.user.name"
)

// getUserName attempts to resolve a username from the context and request. If
// a username cannot be resolved, the empty string is returned.
func getUserName(ctx context.Context, r *http.Request) string {
	username := dcontext.GetStringValue(ctx, userNameKey)

	// Fallback to request user with basic auth
	if username == "" {
		var ok bool
		uname, _, ok := basicAuth(r)
		if ok {
			username = uname
		}
	}

	return username
}

// withUser returns a context with the authorized user info.
func withUser(ctx context.Context, user auth.UserInfo) context.Context {
	return userInfoContext{
		Context: ctx,
		user:    user,
	}
}

type userInfoContext struct {
	context.Context
	user auth.UserInfo
}

func (uic userInfoContext) Value(key any) any {
	switch key {
	case userKey:
		return uic.user
	case userNameKey:
		return uic.user.Name
	}

	return uic.Context.Value(key)
}

// withResources returns a context with the authorized resources.
func withResources(ctx context.Context, resources []auth.Resource) context.Context {
	return resourceContext{
		Context:   ctx,
		resources: resources,
	}
}

type resourceContext struct {
	context.Context
	resources []auth.Resource
}

type resourceKey struct{}

func (rc resourceContext) Value(key any) any {
	if key == (resourceKey{}) {
		return rc.resources
	}

	return rc.Context.Value(key)
}

// authorizedResources returns the list of resources which have
// been authorized for this request.
func authorizedResources(ctx context.Context) []auth.Resource {
	if resources, ok := ctx.Value(resourceKey{}).([]auth.Resource); ok {
		return resources
	}

	return nil
}
