package s2s2

import (
	"fmt"
	"net/http"

	"code.justin.tv/amzn/TwitchS2S2/c7s"
	"code.justin.tv/amzn/TwitchS2S2/internal/s2s2err"
)

type authorizationErrorType string

const (
	authorizationErrorTypeAuthenticationRequired = authorizationErrorType("authentication_required")
	authorizationErrorTypeInvalidToken           = authorizationErrorType("invalid_token")
	authorizationErrorTypeInsufficientScope      = authorizationErrorType("insufficient_scope")
)

func parseAuthorizationError(res *http.Response) authorizationError {
	switch res.StatusCode {
	case http.StatusForbidden:
	case http.StatusUnauthorized:
	default:
		return nil
	}

	wwwAuthenticateChallenge, err := parseWWWAuthenticateChallenge(res)
	if err != nil {
		// unable to handle error - leave it to downstream
		return nil
	}

	baseAuthorizationError := &baseAuthorizationError{
		Config: &c7s.Config{
			CalleeRealm: wwwAuthenticateChallenge.Realm(),
			Issuer:      wwwAuthenticateChallenge.Issuer(),
		},
		RequiredScopes: wwwAuthenticateChallenge.Scopes(),
	}

	switch wwwAuthenticateChallenge.ErrorType() {
	case authorizationErrorTypeInvalidToken:
		return &invalidTokenError{
			baseAuthorizationError: baseAuthorizationError,
			Reason:                 wwwAuthenticateChallenge.ErrorDescription(),
		}
	case authorizationErrorTypeInsufficientScope:
		return &insufficientScopeError{baseAuthorizationError: baseAuthorizationError}
	}

	return nil
}

// all authorization errors should implement this interface. otherwise, errors
// should return a 500 Internal Server Error.
type authorizationError interface {
	Error() string
	AuthenticateChallenge() *wwwAuthenticateChallenge
	Status() int
}

type baseAuthorizationError struct {
	Config         *c7s.Config
	RequiredScopes []string
}

func (e *baseAuthorizationError) baseAuthenticateChallenge(errorType authorizationErrorType, errorDescription string) *wwwAuthenticateChallenge {
	wwwAuthenticateChallenge := newWWWAuthenticateChallenge()
	wwwAuthenticateChallenge.SetChallengeType("Bearer")
	wwwAuthenticateChallenge.SetRealm(e.Config.CalleeRealm)
	wwwAuthenticateChallenge.SetIssuer(e.Config.Issuer)
	if len(errorType) > 0 {
		wwwAuthenticateChallenge.SetError(string(errorType), errorDescription)
	}
	if len(e.RequiredScopes) > 0 {
		wwwAuthenticateChallenge.SetScopes(e.RequiredScopes)
	}
	return wwwAuthenticateChallenge
}

// authorizationError is returned when a request has no Authorization header.
type authenticationError struct {
	*baseAuthorizationError
}

func (e *authenticationError) Error() string {
	return s2s2err.S2S2ErrorString("Authorization header is required for this request.", "Authorization")
}

func (e *authenticationError) AuthenticateChallenge() *wwwAuthenticateChallenge {
	wwwAuthenticateChallenge := e.baseAuthenticateChallenge(
		authorizationErrorTypeAuthenticationRequired,
		e.Error(),
	)
	return wwwAuthenticateChallenge
}

func (e *authenticationError) Status() int {
	return http.StatusUnauthorized
}

// insufficientScopeError is returned when more scope is needed
type insufficientScopeError struct {
	*baseAuthorizationError

	Scopes []string
}

func (e *insufficientScopeError) Error() string {
	return s2s2err.S2S2ErrorString(fmt.Sprintf("More scopes are required for this request in addition to %v", e.Scopes), "Authorization")
}

func (e *insufficientScopeError) AuthenticateChallenge() *wwwAuthenticateChallenge {
	wwwAuthenticateChallenge := e.baseAuthenticateChallenge(
		authorizationErrorTypeInsufficientScope,
		e.Error(),
	)
	return wwwAuthenticateChallenge
}

func (e *insufficientScopeError) Status() int {
	return http.StatusForbidden
}

// invalidTokenError is returned if there is an issue with the
// authorization token provided, i.e. expired, etc.
type invalidTokenError struct {
	*baseAuthorizationError
	Reason string
}

func (e *invalidTokenError) Error() string {
	return s2s2err.S2S2ErrorString(e.Reason, "Authorization")
}

func (e *invalidTokenError) AuthenticateChallenge() *wwwAuthenticateChallenge {
	wwwAuthenticateChallenge := e.baseAuthenticateChallenge(
		authorizationErrorTypeInvalidToken,
		e.Error(),
	)
	return wwwAuthenticateChallenge
}

func (e *invalidTokenError) Status() int {
	return http.StatusUnauthorized
}
