package authorization

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"

	logging "code.justin.tv/amzn/TwitchLogging"
	"code.justin.tv/amzn/TwitchS2S2/internal/oidc/oidciface"
	"code.justin.tv/amzn/TwitchS2S2/internal/opwrap"
	"code.justin.tv/sse/jwt"
	"code.justin.tv/video/metrics-middleware/v2/operation"
)

// ErrInvalidToken is returned
type ErrInvalidToken struct {
	Field  string
	Reason string
}

func (e *ErrInvalidToken) Error() string {
	return fmt.Sprintf("Issue with access token field %s: %s", e.Field, e.Reason)
}

// Authorizations returns information about access tokens
type Authorizations struct {
	OIDC             oidciface.OIDCAPI
	Logger           logging.Logger
	OperationStarter *operation.Starter
	// Called when a token is authenticated
	OnTokenAuthenticated func(context.Context, *Authorization)
	// Called when a valid token is rejected
	OnTokenRejected func(context.Context, *ErrInvalidToken)
}

// Validate an authorization
func (a *Authorizations) Validate(ctx context.Context, authorizationType, authorization string) (_ *Authorization, err error) {
	ctx, op := a.OperationStarter.StartOp(ctx, opwrap.ValidateAuthorization)
	defer opwrap.EndWithError(op, err)

	authz, err := a.validate(ctx, authorizationType, authorization)
	if err != nil {
		if invalidTokenErr, ok := err.(*ErrInvalidToken); ok {
			a.OnTokenRejected(ctx, invalidTokenErr)
		}
		return nil, err
	}
	return authz, nil
}

func (a *Authorizations) validate(ctx context.Context, authorizationType, authorization string) (*Authorization, error) {
	// TODO: validate authorization type

	fragment, err := jwt.Parse([]byte(authorization))
	if err != nil {
		return nil, err
	}

	var header struct {
		KeyID string `json:"kid"`
	}
	if err = b64JSONDecoder(fragment.Header()).Decode(&header); err != nil {
		return nil, err
	}

	pubkey, err := a.OIDC.ValidationKey(ctx, header.KeyID)
	if err != nil {
		return nil, &ErrInvalidToken{
			Field:  "kid",
			Reason: err.Error(),
		}
	}

	if err := fragment.Validate(onlyValidationAlgorithm{pubkey}); err != nil {
		return nil, &ErrInvalidToken{
			Field:  "kid",
			Reason: err.Error(),
		}
	}

	var validatedAuthorization Authorization
	if err := b64JSONDecoder(fragment.Claims()).Decode(&validatedAuthorization); err != nil {
		return nil, &ErrInvalidToken{
			Field:  "claims",
			Reason: err.Error(),
		}
	}

	if !validatedAuthorization.Active {
		return nil, &ErrInvalidToken{
			Field:  "active",
			Reason: "Access token is not active",
		}
	}

	if err := validatedAuthorization.ValidateTimeClaims(); err != nil {
		return nil, err
	}

	if validatedAuthorization.Issuer != a.OIDC.Configuration().IssuerID {
		return nil, &ErrInvalidToken{
			Field:  "iss",
			Reason: fmt.Sprintf("Token has an invalid issuer<%s> - we expect %s", validatedAuthorization.Issuer, a.OIDC.Configuration().IssuerID),
		}
	}

	a.OnTokenAuthenticated(ctx, &validatedAuthorization)

	return &validatedAuthorization, nil
}

func b64JSONDecoder(bs []byte) *json.Decoder {
	return json.NewDecoder(base64.NewDecoder(base64.URLEncoding, bytes.NewReader(bs)))
}
