package utils

import (
	"fmt"
	"net/http"
	"time"

	ctxu "github.com/docker/distribution/context"
	"github.com/docker/distribution/registry/api/errcode"
	"github.com/docker/distribution/registry/auth"
	"github.com/gorilla/mux"
	"golang.org/x/net/context"

	"github.com/theupdateframework/notary"
	"github.com/theupdateframework/notary/tuf/signed"
)

// ContextHandler defines an alternate HTTP handler interface which takes in
// a context for authorization and returns an HTTP application error.
type ContextHandler func(ctx context.Context, w http.ResponseWriter, r *http.Request) error

// rootHandler is an implementation of an HTTP request handler which handles
// authorization and calling out to the defined alternate http handler.
type rootHandler struct {
	handler ContextHandler
	auth    auth.AccessController
	actions []string
	context context.Context
	trust   signed.CryptoService
}

// AuthWrapper wraps a Handler with and Auth requirement
type AuthWrapper func(ContextHandler, ...string) *rootHandler

// RootHandlerFactory creates a new rootHandler factory  using the given
// Context creator and authorizer.  The returned factory allows creating
// new rootHandlers from the alternate http handler contextHandler and
// a scope.
func RootHandlerFactory(ctx context.Context, auth auth.AccessController, trust signed.CryptoService) func(ContextHandler, ...string) *rootHandler {
	return func(handler ContextHandler, actions ...string) *rootHandler {
		return &rootHandler{
			handler: handler,
			auth:    auth,
			actions: actions,
			context: ctx,
			trust:   trust,
		}
	}
}

// ServeHTTP serves an HTTP request and implements the http.Handler interface.
func (root *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	var (
		err  error
		ctx  = ctxu.WithRequest(root.context, r)
		log  = ctxu.GetRequestLogger(ctx)
		vars = mux.Vars(r)
	)
	ctx, w = ctxu.WithResponseWriter(ctx, w)
	ctx = ctxu.WithLogger(ctx, log)
	ctx = context.WithValue(ctx, notary.CtxKeyCryptoSvc, root.trust)

	defer func(ctx context.Context) {
		ctxu.GetResponseLogger(ctx).Info("response completed")
	}(ctx)

	if root.auth != nil {
		ctx = context.WithValue(ctx, notary.CtxKeyRepo, vars["gun"])
		if ctx, err = root.doAuth(ctx, vars["gun"], w, r); err != nil {
			// errors have already been logged/output to w inside doAuth
			// just return
			return
		}
	}
	if err := root.handler(ctx, w, r); err != nil {
		serveError(log, w, err)
	}
}

func serveError(log ctxu.Logger, w http.ResponseWriter, err error) {
	if httpErr, ok := err.(errcode.Error); ok {
		// info level logging for non-5XX http errors
		httpErrCode := httpErr.ErrorCode().Descriptor().HTTPStatusCode
		if httpErrCode >= http.StatusInternalServerError {
			// error level logging for 5XX http errors
			log.Errorf("%s: %s: %v", httpErr.ErrorCode().Error(), httpErr.Message, httpErr.Detail)
		} else {
			log.Infof("%s: %s: %v", httpErr.ErrorCode().Error(), httpErr.Message, httpErr.Detail)
		}
	}
	e := errcode.ServeJSON(w, err)
	if e != nil {
		log.Error(e)
	}
	return
}

func (root *rootHandler) doAuth(ctx context.Context, gun string, w http.ResponseWriter, r *http.Request) (context.Context, error) {
	var access []auth.Access
	if gun == "" {
		access = buildCatalogRecord(root.actions...)
	} else {
		access = buildAccessRecords(gun, root.actions...)
	}

	log := ctxu.GetRequestLogger(ctx)
	var authCtx context.Context
	var err error
	if authCtx, err = root.auth.Authorized(ctx, access...); err != nil {
		if challenge, ok := err.(auth.Challenge); ok {
			// Let the challenge write the response.
			challenge.SetHeaders(r, w)

			if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized.WithDetail(access)); err != nil {
				log.Errorf("failed to serve challenge response: %s", err.Error())
				return nil, err
			}
			return nil, err
		}
		errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized)
		return nil, err
	}
	return authCtx, nil
}

func buildAccessRecords(repo string, actions ...string) []auth.Access {
	requiredAccess := make([]auth.Access, 0, len(actions))
	for _, action := range actions {
		requiredAccess = append(requiredAccess, auth.Access{
			Resource: auth.Resource{
				Type: "repository",
				Name: repo,
			},
			Action: action,
		})
	}
	return requiredAccess
}

// buildCatalogRecord returns the only valid format for the catalog
// resource. Only admins can get this access level from the token
// server.
func buildCatalogRecord(actions ...string) []auth.Access {
	requiredAccess := []auth.Access{{
		Resource: auth.Resource{
			Type: "registry",
			Name: "catalog",
		},
		Action: "*",
	}}

	return requiredAccess
}

// CacheControlConfig is an interface for something that knows how to set cache
// control headers
type CacheControlConfig interface {
	// SetHeaders will actually set the cache control headers on a Headers object
	SetHeaders(headers http.Header)
}

// NewCacheControlConfig returns CacheControlConfig interface for either setting
// cache control or disabling cache control entirely
func NewCacheControlConfig(maxAgeInSeconds int, mustRevalidate bool) CacheControlConfig {
	if maxAgeInSeconds > 0 {
		return PublicCacheControl{MustReValidate: mustRevalidate, MaxAgeInSeconds: maxAgeInSeconds}
	}
	return NoCacheControl{}
}

// PublicCacheControl is a set of options that we will set to enable cache control
type PublicCacheControl struct {
	MustReValidate  bool
	MaxAgeInSeconds int
}

// SetHeaders sets the public headers with an optional must-revalidate header
func (p PublicCacheControl) SetHeaders(headers http.Header) {
	cacheControlValue := fmt.Sprintf("public, max-age=%v, s-maxage=%v",
		p.MaxAgeInSeconds, p.MaxAgeInSeconds)

	if p.MustReValidate {
		cacheControlValue = fmt.Sprintf("%s, must-revalidate", cacheControlValue)
	}
	headers.Set("Cache-Control", cacheControlValue)
	// delete the Pragma directive, because the only valid value in HTTP is
	// "no-cache"
	headers.Del("Pragma")
	if headers.Get("Last-Modified") == "" {
		SetLastModifiedHeader(headers, time.Time{})
	}
}

// NoCacheControl is an object which represents a directive to cache nothing
type NoCacheControl struct{}

// SetHeaders sets the public headers cache-control headers and pragma to no-cache
func (n NoCacheControl) SetHeaders(headers http.Header) {
	headers.Set("Cache-Control", "max-age=0, no-cache, no-store")
	headers.Set("Pragma", "no-cache")
}

// cacheControlResponseWriter wraps an existing response writer, and if Write is
// called, will try to set the cache control headers if it can
type cacheControlResponseWriter struct {
	http.ResponseWriter
	config     CacheControlConfig
	statusCode int
}

// WriteHeader stores the header before writing it, so we can tell if it's been set
// to a non-200 status code
func (c *cacheControlResponseWriter) WriteHeader(statusCode int) {
	c.statusCode = statusCode
	c.ResponseWriter.WriteHeader(statusCode)
}

// Write will set the cache headers if they haven't already been set and if the status
// code has either not been set or set to 200
func (c *cacheControlResponseWriter) Write(data []byte) (int, error) {
	if c.statusCode == http.StatusOK || c.statusCode == 0 {
		headers := c.ResponseWriter.Header()
		if headers.Get("Cache-Control") == "" {
			c.config.SetHeaders(headers)
		}
	}
	return c.ResponseWriter.Write(data)
}

type cacheControlHandler struct {
	http.Handler
	config CacheControlConfig
}

func (c cacheControlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	c.Handler.ServeHTTP(&cacheControlResponseWriter{ResponseWriter: w, config: c.config}, r)
}

// WrapWithCacheHandler wraps another handler in one that can add cache control headers
// given a 200 response
func WrapWithCacheHandler(ccc CacheControlConfig, handler http.Handler) http.Handler {
	if ccc != nil {
		return cacheControlHandler{Handler: handler, config: ccc}
	}
	return handler
}

// SetLastModifiedHeader takes a time and uses it to set the LastModified header using
// the right date format
func SetLastModifiedHeader(headers http.Header, lmt time.Time) {
	headers.Set("Last-Modified", lmt.Format(time.RFC1123))
}
