package s2s2

import (
	"context"
	"errors"
	"net/http"
	"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/cacheauthorization"
	"code.justin.tv/amzn/TwitchS2S2/internal/interfaces"
	"code.justin.tv/amzn/TwitchS2S2/internal/oidc"
	"code.justin.tv/amzn/TwitchS2S2/internal/oidc/oidccache"
	"code.justin.tv/amzn/TwitchS2S2/internal/sigv4"
	"code.justin.tv/amzn/TwitchS2S2/internal/token"
	"code.justin.tv/amzn/TwitchS2S2/internal/token/cachetoken"
	"code.justin.tv/amzn/TwitchS2S2/internal/token/s2stoken"
	"code.justin.tv/video/metrics-middleware/v2/operation"
	"github.com/aws/aws-sdk-go/aws/credentials"
	v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
)

// This file is used for github.com/google/wire to wire clients together.
// See ./wire.go for wire.Build statements.
// See ./wire_gen.go for generation instructions.
// Customer configurable clients are configured in ./options.go

func newS2S2(
	accessTokens *accessTokens,
	cachedAuthorizations *cacheauthorization.CachedAuthorizations,
	config *c7s.Config,
	operationStarter *operation.Starter,
	clientServiceURI clientServiceURI,
	logger logging.Logger,
) *S2S2 {
	return &S2S2{
		accessTokens:     accessTokens,
		authorizations:   cachedAuthorizations,
		clientServiceURI: string(clientServiceURI),
		config:           config,
		logger:           logger,
		operationStarter: operationStarter,
	}
}

func newAuthorizations(
	oidc *oidccache.Cache,
	logger logging.Logger,
	operationStarter *operation.Starter,
	onTokenAuthenticatedHook onTokenAuthenticatedHook,
	onTokenRejectedHook onTokenRejectedHook,
) *authorization.Authorizations {
	return &authorization.Authorizations{
		OIDC:                 oidc,
		Logger:               logger,
		OperationStarter:     operationStarter,
		OnTokenAuthenticated: onTokenAuthenticatedHook,
		OnTokenRejected:      onTokenRejectedHook,
	}
}

type onTokenAuthenticatedHook func(context.Context, *authorization.Authorization)

// hook to log new authenticated tokens
// request information is injected via S2S2.injectRequestInformation
func newOnTokenAuthenticatedHook(
	logger logging.Logger,
) onTokenAuthenticatedHook {
	return onTokenAuthenticatedHook(func(ctx context.Context, a *authorization.Authorization) {
		if currentRequest, ok := currentRequest(ctx); ok {
			logger.Log(
				"S2S2TokenAuthenticated",
				"audience", a.Audience.String(),
				"subject", a.Subject.ID(),
				"scope", a.Scope.String(),
				"tokenIdentifier", a.JWTID,
				"issuer", a.Issuer,
				"issuedAt", a.IssuedAt.Format(time.RFC3339),
				"expiry", a.Expiration.Format(time.RFC3339),
				"amazonTraceID", currentRequest.AmazonTraceID,
				"remoteAddress", currentRequest.RemoteAddress,
				"forwardedFor", currentRequest.ForwardedFor,
				"forwardedProto", currentRequest.ForwardedProto,
				"forwardedPort", currentRequest.ForwardedPort,
				"host", currentRequest.Host,
			)
		}
	})
}

type onTokenRejectedHook func(context.Context, *authorization.ErrInvalidToken)

// hook to log new rejected tokens
// request information is injected via S2S2.injectRequestInformation
func newOnTokenRejectedHook(
	logger logging.Logger,
) onTokenRejectedHook {
	return onTokenRejectedHook(func(ctx context.Context, err *authorization.ErrInvalidToken) {
		if currentRequest, ok := currentRequest(ctx); ok {
			logger.Log(
				"S2S2TokenRejected",
				"field", err.Field,
				"reason", err.Reason,
				"remoteAddress", currentRequest.RemoteAddress,
				"forwardedFor", currentRequest.ForwardedFor,
				"forwardedProto", currentRequest.ForwardedProto,
				"forwardedPort", currentRequest.ForwardedPort,
				"host", currentRequest.Host,
			)
		}
	})
}

func newCachedAuthorizations(
	authorizations *authorization.Authorizations,
	config *c7s.Config,
) *cacheauthorization.CachedAuthorizations {
	return cacheauthorization.New(authorizations, config)
}

func newOIDC(
	client *sigv4.RoundTripper,
	config *c7s.Config,
	operationStarter *operation.Starter,
) (*oidc.OIDC, error) {
	oidc := &oidc.OIDC{
		Config:           config,
		Client:           &http.Client{Transport: client},
		OperationStarter: operationStarter,
	}
	if err := oidc.Init(context.TODO()); err != nil {
		return nil, err
	}
	return oidc, nil
}

func newOIDCCache(
	oidc *oidc.OIDC,
) (*oidccache.Cache, error) {
	cache := &oidccache.Cache{OIDC: oidc}
	if err := cache.RefreshIfStale(context.TODO()); err != nil {
		return nil, err
	}
	return cache, nil
}

type sigV4HTTPClient struct {
	*http.Client
}

func newSigV4HTTPClient(
	rt *sigv4.RoundTripper,
) *sigV4HTTPClient {
	return &sigV4HTTPClient{Client: &http.Client{Transport: rt}}
}

func newSigV4RoundTripper(
	roundTripper http.RoundTripper,
	config *c7s.Config,
	credentials *credentials.Credentials,
) *sigv4.RoundTripper {
	return &sigv4.RoundTripper{
		RoundTripper: roundTripper,
		AWSRegion:    config.AWSRegion,
		AWSService:   "execute-api",
		Signer:       v4.NewSigner(credentials),
	}
}

func newHTTPClient(
	transport http.RoundTripper,
) interfaces.HTTPClient {
	return &http.Client{Transport: transport}
}

type assertions struct {
	*cachetoken.Cache
}

func newAssertions(
	config *c7s.Config,
	oidc *oidccache.Cache,
	sigv4HTTPClient *sigV4HTTPClient,
	operationStarter *operation.Starter,
	providedClientServiceURI providedClientServiceURI,
) *assertions {
	return &assertions{
		Cache: cachetoken.New(
			&s2stoken.Assertions{
				ClientServiceURI: string(providedClientServiceURI),
				Config:           config,
				OIDC:             oidc,
				OperationStarter: operationStarter,
				SigV4Client:      sigv4HTTPClient,
			},
			defaultCacheKey,
			defaultCacheValueStale,
			defaultCacheValueExpired,
		),
	}
}

type clientCredentials struct {
	*cachetoken.Cache
}

func newClientCredentials(
	assertions *assertions,
	client interfaces.HTTPClient,
	config *c7s.Config,
	oidc *oidccache.Cache,
	operationStarter *operation.Starter,
) *clientCredentials {
	return &clientCredentials{
		Cache: cachetoken.New(
			&s2stoken.ClientCredentials{
				Assertions:       assertions,
				Client:           client,
				Config:           config,
				OIDC:             oidc,
				OperationStarter: operationStarter,
			},
			defaultCacheKey,
			defaultCacheValueStale,
			defaultCacheValueExpired,
		),
	}
}

type accessTokens struct {
	*cachetoken.Cache
}

func newAccessTokens(
	client interfaces.HTTPClient,
	clientCredentials *clientCredentials,
	oidc *oidccache.Cache,
	operationStarter *operation.Starter,
) *accessTokens {
	return &accessTokens{
		Cache: cachetoken.New(
			&s2stoken.AccessTokens{
				Client:            client,
				ClientCredentials: clientCredentials,
				OIDC:              oidc,
				OperationStarter:  operationStarter,
			},
			defaultCacheKey,
			accessTokenStale,
			accessTokenExpired,
		),
	}
}

var errClientServiceNameNotSet = errors.New("TwitchS2S2:clientServiceName not set")
var errClientServiceNameAndURIBothSet = errors.New("Only one of TwitchS2S2:clientServiceName TwitchS2S2:clientServiceUri can be set")

type providedClientServiceURI string

func newProvidedClientServiceURI(
	config *c7s.Config,
) (providedClientServiceURI, error) {
	if config.ClientServiceName != "" && config.ClientServiceURI != "" {
		return providedClientServiceURI(""), errClientServiceNameAndURIBothSet
	}

	if config.ClientServiceName != "" {
		return providedClientServiceURI(config.ServiceURIByNameBase + "/" + config.ClientServiceName), nil
	} else if config.ClientServiceURI != "" {
		return providedClientServiceURI(config.ClientServiceURI), nil
	}
	return providedClientServiceURI(""), errClientServiceNameNotSet
}

type clientServiceURI string

// retrieves the official clientServiceURI.
// service owners will generally configure a service uri that specifies the
// service by service name, not ID. this will normalize that to the service ID
// uri.
func newClientServiceURI(
	accessTokens *accessTokens,
	authorizations *authorization.Authorizations,
) (clientServiceURI, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	accessToken, err := accessTokens.Token(ctx, token.NewOptions().WithHost("https://test"))
	if err != nil {
		return clientServiceURI(""), err
	}

	authorization, err := authorizations.Validate(ctx, "", accessToken.AccessToken)
	if err != nil {
		return clientServiceURI(""), err
	}

	return clientServiceURI(authorization.Subject.ID()), nil
}
