package callee

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

	"code.justin.tv/sse/malachai/pkg/capabilities"
	"code.justin.tv/sse/malachai/pkg/events"
	"code.justin.tv/sse/malachai/pkg/jwtvalidation"
	"code.justin.tv/sse/malachai/pkg/log"
)

// capabilitiesContextKey is the key for the value in the request context
// containing the list of capabilities whitelisted for the caller.
var capabilitiesContextKey = contextKey("s2s-caller-capabilities")

// GetCapabilities retrieves the list of capabilities whitelisted for the
// caller from the request context.
func GetCapabilities(ctx context.Context) *capabilities.Capabilities {
	caps, ok := ctx.Value(capabilitiesContextKey).(*capabilities.Capabilities)
	if !ok {
		return nil
	}
	return caps
}

func setCapabilities(parent context.Context, caps *capabilities.Capabilities) context.Context {
	return context.WithValue(parent, capabilitiesContextKey, caps)
}

// calleeNameContextKey is the key for the value in the request context
// containing the name of the callee service.
var calleeNameContextKey = contextKey("s2s-callee-name")

// GetCalleeName retrieves the name of the callee service from the request
// context
func GetCalleeName(ctx context.Context) string {
	calleeName, ok := ctx.Value(calleeNameContextKey).(string)
	if !ok {
		return ""
	}
	return calleeName
}

func setCalleeName(parent context.Context, calleeName string) context.Context {
	return context.WithValue(parent, calleeNameContextKey, calleeName)
}

// newCapabilitiesInjector returns middleware that injects the caller's
// capabilities (as a capabilities.Capabilities object) to the request context
func newCapabilitiesInjector(cfg *Config, mgr *calleeCapabilitiesClient, eventsLogger events.EventWriter, lgr log.S2SLogger) (fn func(http.Handler) http.Handler) {
	fn = func(inner http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			var caller *jwtvalidation.SigningEntity
			if caller = GetCallerID(r.Context()); caller == nil {
				http.Error(w, "caller identity not set in request context.", http.StatusInternalServerError)
				return
			}

			eventLogger := eventsLogger.WithFields(&events.Event{
				URLPath:   r.URL.EscapedPath(),
				CallerID:  caller.Caller,
				Timestamp: events.Time(time.Now()),
			})

			caps, err := mgr.get(caller.Caller)
			if err != nil {
				// if the error is anything other than a 404, return 503
				if err != capabilities.ErrCapabilitiesNotFound {
					http.Error(w, fmt.Sprintf("error retrieving caller capabilities: %s", err.Error()), http.StatusInternalServerError)
					return
				}

				// if passthrough mode is disabled, reject the request as unauthorized
				if !cfg.PassthroughMode {
					eventLogger.WriteEvent(&events.Event{
						Category:   "NoAuthorizingCapabilities",
						Message:    "capabilities not found for caller",
						StatusCode: "403",
					})

					http.Error(w, "capabilities not found for caller", http.StatusForbidden)
					return
				}

				// if passthrough mode is enabled, accept the request and
				// inject empty capabilities into the context
				caps = &capabilities.Capabilities{
					Callee: mgr.cfg.callee,
					Caller: caller.Caller,
				}
			}

			ctx := setCapabilities(r.Context(), caps)
			ctx = setCalleeName(ctx, mgr.cfg.callee)

			eventLogger.WriteEvent(&events.Event{
				Category: "CapabilitiesAuthorized",
			})

			inner.ServeHTTP(w, r.WithContext(ctx))
		})
	}
	return
}
