package s2s2

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	logging "code.justin.tv/amzn/TwitchLogging"
	"code.justin.tv/amzn/TwitchS2S2/c7s"
	"code.justin.tv/amzn/TwitchS2S2/internal/authorization"
	"code.justin.tv/amzn/TwitchS2S2/internal/authorization/authorizationiface"
	"code.justin.tv/amzn/TwitchS2S2/internal/httpwrap"
	"code.justin.tv/amzn/TwitchS2S2/internal/origin"
	"code.justin.tv/amzn/TwitchS2S2/internal/token"
	"code.justin.tv/video/metrics-middleware/v2/operation"
	"github.com/aws/aws-lambda-go/lambdacontext"
	"github.com/twitchtv/twirp"
)

// S2S2 handles constructing RoundTrippers and middlewares to secure and
// authenticate against s2s enabled services.
type S2S2 struct {
	accessTokens     token.Tokens
	authorizations   authorizationiface.AuthorizationsAPI
	clientServiceURI string
	config           *c7s.Config
	logger           logging.Logger
	operationStarter *operation.Starter
}

// ServiceURI returns the ServiceURI of this service
func (s *S2S2) ServiceURI() string {
	return s.clientServiceURI
}

// CapabilityScope is an input of RequireScopes that represents a scope
func (s *S2S2) CapabilityScope(name string) string {
	return s.clientServiceURI + "#" + name
}

// HTTPClient is the interface twirp expects
type HTTPClient interface {
	Do(*http.Request) (*http.Response, error)
}

// HTTPClient returns an HTTP client that handles authentication and
// authorization to a S2S2 secured service.
func (s *S2S2) HTTPClient(inner HTTPClient) HTTPClient {
	h := &authenticatingHTTPClient{
		AccessTokens: s.accessTokens,
		Inner:        inner,
	}

	if lambdaClient, ok := inner.(interface {
		LambdaName() string
	}); ok {
		// if the wrapped transport is talking to an AWS Lambda function, this will
		// require special configuration.
		h.LambdaName = lambdaClient.LambdaName()
	}

	return h
}

// RoundTripper is the RoundTripper that this package implements.
// We will add additional methods in the future.
type RoundTripper interface {
	http.RoundTripper
}

// RoundTripper returns a RoundTripper for usage with an *http.Client for
// authentication and authorization.
func (s *S2S2) RoundTripper(inner HTTPClient) RoundTripper {
	return httpwrap.RoundTripperFromHTTPClient(s.HTTPClient(inner))
}

// RoundTripperWrapper wraps implementations of http.RoundTripper with S2S2.
// This is commonly used with code.justin.tv/foundation/twitchclient.
func (s *S2S2) RoundTripperWrapper(inner http.RoundTripper) http.RoundTripper {
	return s.RoundTripper(httpwrap.HTTPClientFromRoundTripper(inner))
}

// RequireAuthentication rejects requests if caller is not authenticated with an
// access token.
//
// See PassthroughIfAuthorizationNotPresented for passthrough mode of this.
//
// Error codes are taken mostly from https://tools.ietf.org/html/rfc6750#section-3.1
//
// Returns 401 with error invalid_token if:
// - Authorization header is not present.
// - Access token is malformed.
// - Access token is invalid (expired, used before valid, wrong audince).
func (s *S2S2) RequireAuthentication(inner http.Handler) Handler {
	return &passthroughableHandler{
		Handler:            s.requireAuthentication(s.logRequest(inner)),
		OperationStarter:   s.operationStarter,
		PassthroughHandler: inner,
	}
}

func (s *S2S2) requireAuthentication(inner http.Handler) http.Handler {
	return s.injectRequestInformation(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		authz, ok := s.parseAuthorizationHeaderAndValidate(w, r, nil)
		if !ok {
			return
		}

		inner.ServeHTTP(w, r.WithContext(SetRequestSubject(r.Context(), &authorizedSubject{
			Subject: authz.Subject,
			scope:   authz.Scope,
			tokenID: authz.JWTID,
		})))
	}))
}

// RequireScopes rejects requests with a 401 if caller does not have a set of
// scopes in their call.
//
// See PassthroughIfAuthorizationNotPresented for passthrough mode of this.
//
// Error codes are taken mostly from https://tools.ietf.org/html/rfc6750#section-3.1
//
// Returns 401 with error invalid_token if:
// - Authorization header is not present.
// - Access token is malformed.
// - Access token is invalid (expired, used before valid, wrong audince).
//
// Returns 403 with error insufficient_scope if:
// - Scope describe in the access token doesn't match the required scope.
func (s *S2S2) RequireScopes(inner http.Handler, scopes ...string) Handler {
	return &passthroughableHandler{
		Handler:            s.requireScopes(s.logRequest(inner), scopes...),
		OperationStarter:   s.operationStarter,
		PassthroughHandler: inner,
	}
}

func (s *S2S2) requireScopes(inner http.Handler, scopes ...string) http.Handler {
	return s.injectRequestInformation(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		authz, ok := s.parseAuthorizationHeaderAndValidate(w, r, scopes)
		if !ok {
			return
		}

		if !authz.Scope.IsSuperSetOf(scopes) {
			s.renderAuthorizationError(w, &insufficientScopeError{
				baseAuthorizationError: &baseAuthorizationError{Config: s.config, RequiredScopes: scopes},
				Scopes:                 scopes,
			})
			return
		}

		inner.ServeHTTP(w, r.WithContext(SetRequestSubject(r.Context(), &authorizedSubject{
			Subject: authz.Subject,
			scope:   authz.Scope,
			tokenID: authz.JWTID,
		})))
	}))
}

func (s *S2S2) injectRequestInformation(inner http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		inner.ServeHTTP(w, r.WithContext(setCurrentRequest(r.Context(), r)))
	})
}

// toMilliseconds returns the duration as an integer millisecond count.
// verbatim replacement of the .Milliseconds function in go1.13
// https://golang.org/pkg/time/#Duration.Milliseconds
// TODO: Sujonat, revert once 1.12 no longer supported
func toMilliseconds(d time.Duration) int64 {
	return int64(d) / 1e6
}

// logRequest logs all inbound requests using the given logger
func (s *S2S2) logRequest(inner http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		startTime := time.Now()

		inner.ServeHTTP(w, r)
		if s.config.EnableAccessLogging {
			s.logger.Log("S2S2AuthorizedResponseSent",
				"MethodName", r.Method,
				"Url", r.URL.RawQuery,
				"UserAgent", r.UserAgent(),
				"Timestamp", startTime.Format(time.RFC3339),
				"RequestProcessingTime", strconv.FormatInt(toMilliseconds(time.Since(startTime)), 10),
				"AuthenticatedS2S2CallerURI", RequestSubject(r.Context()).ID(),
				"S2s2TokenId", RequestSubject(r.Context()).TokenID(),
			)
		}
	})
}

func (s *S2S2) parseAuthorizationHeaderAndValidate(w http.ResponseWriter, r *http.Request, requiredScopes []string) (*authorization.Authorization, bool) {
	baseAuthorizationError := &baseAuthorizationError{Config: s.config, RequiredScopes: requiredScopes}

	rawAuthorization, err := parseAuthorizationHeader(r)
	if err != nil {
		switch err {
		case errMissingAuthorizationHeader:
			s.renderAuthorizationError(w, &authenticationError{baseAuthorizationError: baseAuthorizationError})
		case errInvalidAuthorizationHeader:
			s.renderAuthorizationError(w, &invalidTokenError{
				baseAuthorizationError: baseAuthorizationError,
				Reason:                 err.Error(),
			})
		default:
			s.renderError(w, err)
		}
		return nil, false
	}

	authz, err := s.authorizations.Validate(r.Context(), rawAuthorization.TokenType, rawAuthorization.Token)
	if err != nil {
		switch err.(type) {
		case *authorization.ErrInvalidToken:
			s.renderAuthorizationError(w, &invalidTokenError{
				baseAuthorizationError: baseAuthorizationError,
				Reason:                 err.Error(),
			})
		default:
			s.renderError(w, err)
		}
		return nil, false
	}

	var us string
	if lambdaContext, ok := lambdacontext.FromContext(r.Context()); ok {
		us = lambdaContext.InvokedFunctionArn
	} else {
		us = origin.CanonicalWebOriginFromRequestHost(r)
		if us == origin.UndefinedWebOrigin {
			// do not expect to get here
			s.renderError(w, fmt.Errorf("could not determine web origin from Host<%s>", r.Host))
			return nil, false
		}
	}
	if !authz.Audience.Contains(us) {
		s.renderAuthorizationError(w, &invalidTokenError{
			baseAuthorizationError: baseAuthorizationError,
			Reason:                 fmt.Sprintf("Token with audience<%s> not intended for '%s'", strings.Join(authz.Audience.All(), ","), us),
		})
		return nil, false
	}
	return authz, true
}

type jsonError struct {
	Code string `json:"code"`
	Msg  string `json:"msg"`
}

func (s *S2S2) renderAuthorizationError(w http.ResponseWriter, authErr authorizationError) {
	w.Header().Set("WWW-Authenticate", authErr.AuthenticateChallenge().HeaderValue())
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(authErr.Status())

	if err := json.NewEncoder(w).Encode(jsonError{
		Code: errorCodeString(authErr.Status()),
		Msg:  authErr.Error(),
	}); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (s *S2S2) renderError(w http.ResponseWriter, reason error) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusInternalServerError)
	if err := json.NewEncoder(w).Encode(jsonError{
		Code: errorCodeString(http.StatusInternalServerError),
		Msg:  reason.Error(),
	}); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// TwirpHooks provides a TWIRP ServerHooks middleware
func (s *S2S2) TwirpHooks() *twirp.ServerHooks {
	return &twirp.ServerHooks{
		RequestReceived: func(ctx context.Context) (context.Context, error) {
			ctx = setStartTime(ctx)
			return ctx, nil
		},
		ResponseSent: func(ctx context.Context) {
			if s.config.EnableAccessLogging {
				statusCode, _ := twirp.StatusCode(ctx)
				serviceName, _ := twirp.ServiceName(ctx)
				methodName, _ := twirp.MethodName(ctx)
				startTime := startTime(ctx)
				duration := toMilliseconds(time.Since(startTime))
				s.logger.Log("S2S2AuthorizedResponseSent",
					"StatusCode", statusCode,
					"ServiceName", serviceName,
					"MethodName", methodName,
					"AuthenticatedS2S2CallerURI", RequestSubject(ctx).ID(),
					"TimeStamp", startTime.Format(time.RFC3339),
					"RequestProcessingTime", duration,
				)
			}

		},
	}
}
