package callee

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

	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	"code.justin.tv/sse/malachai/pkg/events"
	"code.justin.tv/sse/malachai/pkg/internal/stats/statsiface"
	"code.justin.tv/sse/malachai/pkg/jwtvalidation"
	"code.justin.tv/sse/malachai/pkg/log"
	"code.justin.tv/sse/malachai/pkg/s2s/internal"
	"code.justin.tv/sse/malachai/pkg/signature"
)

// contextKey key type used in the request context. Use the provided getters
// to retrieve the value.
type contextKey string

// callerIDContextKey is used to retrieve the caller's service id from the
// request context. The value is of type *jwtvalidation.SigningEntity.
var callerIDContextKey = contextKey("s2s-caller-id")

const (
	metricNameRequestAuthenticated              = "s2s.request.authenticated"
	metricNameRequestUnauthenticatedRejected    = "s2s.request.unauthenticated.rejected"
	metricNameRequestUnauthenticatedPassthrough = "s2s.request.unauthenticated.passthrough"
)

// GetCallerID retrieves the caller's service id from the request context.
func GetCallerID(ctx context.Context) *jwtvalidation.SigningEntity {
	caller, ok := ctx.Value(callerIDContextKey).(*jwtvalidation.SigningEntity)
	if !ok {
		return nil
	}
	return caller
}

func setCallerID(parent context.Context, callerID *jwtvalidation.SigningEntity) context.Context {
	return context.WithValue(parent, callerIDContextKey, callerID)
}

var unidentifiedCaller = &jwtvalidation.SigningEntity{
	Caller: "_unidentified_caller_",
}

func newRequestVerifier(
	cfg *Config,
	validator signature.RequestValidator,
	eventsLogging events.EventWriter,
	lgr log.S2SLogger,
	statsClient statsiface.ReporterAPI,
) (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 r.ContentLength > cfg.MaxRequestBodySize {
				http.Error(w, fmt.Sprintf("content length %d larger than %d", r.ContentLength, cfg.MaxRequestBodySize), http.StatusRequestEntityTooLarge)
				return
			}

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

			err := func() (err error) {
				seeker, err := internal.WrapRequestBodyWithSeeker(r, cfg.MaxRequestBodySize)
				if err != nil {
					return
				}

				caller, err = validator.ValidateRequest(r)
				if err != nil {
					// if passthrough mode is disabled, reject the request
					if !cfg.PassthroughMode {
						caller = nil
						return
					}

					// if passthrough mode is enabled, set caller to unidentified
					// and continue
					err = nil
					caller = unidentifiedCaller
					statsClient.Report(metricNameRequestUnauthenticatedPassthrough, 1.0, telemetry.UnitCount)
				}

				_, err = seeker.Seek(0, io.SeekStart)
				return
			}()

			if caller == nil {
				eventLogger.WriteEvent(&events.Event{
					Category:   "RequestRejected",
					StatusCode: "401",
					Message:    err.Error(),
				})
				statsClient.Report(metricNameRequestUnauthenticatedRejected, 1.0, telemetry.UnitCount)
				http.Error(w, err.Error(), http.StatusUnauthorized)
				return
			}

			if err != nil {
				eventLogger.WriteEvent(&events.Event{
					Category:   "InternalServerError",
					StatusCode: "500",
					Message:    err.Error(),
				})
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			eventLogger.WriteEvent(&events.Event{
				Category: "RequestAuthenticated",
				CallerID: caller.Caller,
			})
			if caller != unidentifiedCaller {
				statsClient.Report(metricNameRequestAuthenticated, 1.0, telemetry.UnitCount)
			}
			ctx := setCallerID(r.Context(), caller)
			inner.ServeHTTP(w, r.WithContext(ctx))
		})
	}
	return
}
