package runtimegrpc

import (
	"context"
	"errors"

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/authorizationv1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/envelope"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

const (
	AuthorizationClientSecretMetadataKey       = "x-nexuscrm-authorization-client-secret-bin"
	AuthorizationCodeMetadataKey               = "x-nexuscrm-authorization-code-bin"
	AuthorizationCodeVerifierMetadataKey       = "x-nexuscrm-authorization-code-verifier-bin"
	AuthorizationRefreshTokenMetadataKey       = "x-nexuscrm-authorization-refresh-token-bin"
	IssuedAuthorizationAccessTokenMetadataKey  = "x-nexuscrm-issued-authorization-access-token-bin"
	IssuedAuthorizationRefreshTokenMetadataKey = "x-nexuscrm-issued-authorization-refresh-token-bin"
	IssuedAuthorizationCodeMetadataKey         = "x-nexuscrm-issued-authorization-code-bin"

	maxAuthorizationClientSecretBytes = 16 * 1024
	maxAuthorizationCodeBytes         = 4096
	maxAuthorizationCodeVerifierBytes = 1024
	maxAuthorizationRefreshTokenBytes = 64 * 1024
)

type AuthorizationExchangeCredentials struct {
	ClientSecret      string
	AuthorizationCode string
	CodeVerifier      string
	RefreshToken      string
}

type IssuedAuthorizationCredentials struct {
	AuthorizationCode string
	AccessToken       string
	RefreshToken      string
}

type BeginAuthorizationInvoker func(context.Context, *runtimev1.BeginAuthorizationRequest, ...grpc.CallOption) (*runtimev1.BeginAuthorizationResponse, error)
type CompleteAuthorizationInvoker func(context.Context, *runtimev1.CompleteAuthorizationRequest, ...grpc.CallOption) (*runtimev1.CompleteAuthorizationResponse, error)
type ExchangeAuthorizationTokenInvoker func(context.Context, *runtimev1.ExchangeAuthorizationTokenRequest, ...grpc.CallOption) (*runtimev1.ExchangeAuthorizationTokenResponse, error)

type AuthorizationLifecycleClient struct {
	begin    BeginAuthorizationInvoker
	complete CompleteAuthorizationInvoker
	exchange ExchangeAuthorizationTokenInvoker
}

func NewAuthorizationLifecycleClient(begin BeginAuthorizationInvoker, complete CompleteAuthorizationInvoker, exchange ExchangeAuthorizationTokenInvoker) (*AuthorizationLifecycleClient, error) {
	if begin == nil || complete == nil || exchange == nil {
		return nil, errors.New("runtimegrpc: complete authorization lifecycle invokers are required")
	}
	return &AuthorizationLifecycleClient{begin: begin, complete: complete, exchange: exchange}, nil
}

func (c *AuthorizationLifecycleClient) Begin(ctx context.Context, request semantic.BeginRequest) (semantic.BeginResponse, error) {
	wireRequest, err := mapping.ToWireBeginAuthorizationRequest(ctx, request)
	if err != nil {
		return semantic.BeginResponse{}, err
	}
	callCtx := withClientProtocol(ctx, semantic.ProtocolMajor, semantic.ProtocolMinor, semantic.CapabilityName)
	values, _ := metadata.FromOutgoingContext(callCtx)
	callCtx = metadata.NewOutgoingContext(callCtx, withoutSessionOutgoingMetadata(values.Copy()))
	response, err := c.begin(callCtx, wireRequest, authorizationCallOptions()...)
	if err != nil {
		return semantic.BeginResponse{}, mapping.FromPlatformStatusError(err, semantic.CapabilityName)
	}
	return mapping.FromWireBeginAuthorizationResponse(response)
}

func (c *AuthorizationLifecycleClient) Complete(ctx context.Context, credentials SessionPresentedCredentials, request semantic.CompleteRequest) (semantic.CompleteResponse, IssuedAuthorizationCredentials, error) {
	if err := validatePresentedSessionCredentials(false, credentials); err != nil {
		return semantic.CompleteResponse{}, IssuedAuthorizationCredentials{}, err
	}
	wireRequest, err := mapping.ToWireCompleteAuthorizationRequest(ctx, request)
	if err != nil {
		return semantic.CompleteResponse{}, IssuedAuthorizationCredentials{}, err
	}
	callCtx := withPresentedSessionCredentials(withClientProtocol(ctx, semantic.ProtocolMajor, semantic.ProtocolMinor, semantic.CapabilityName), credentials)
	var header metadata.MD
	options := append(authorizationCallOptions(), grpc.Header(&header))
	response, err := c.complete(callCtx, wireRequest, options...)
	if err != nil {
		return semantic.CompleteResponse{}, IssuedAuthorizationCredentials{}, mapping.FromPlatformStatusError(err, semantic.CapabilityName)
	}
	semanticResponse, err := mapping.FromWireCompleteAuthorizationResponse(response)
	if err != nil {
		return semantic.CompleteResponse{}, IssuedAuthorizationCredentials{}, err
	}
	issued, err := issuedAuthorizationCode(header, semanticResponse)
	return semanticResponse, issued, err
}

func (c *AuthorizationLifecycleClient) Exchange(ctx context.Context, credentials AuthorizationExchangeCredentials, request semantic.ExchangeRequest) (semantic.ExchangeResponse, IssuedAuthorizationCredentials, error) {
	if !validAuthorizationExchangeCredentials(request, credentials) {
		return semantic.ExchangeResponse{}, IssuedAuthorizationCredentials{}, &semantic.Failure{Category: semantic.FailureUnauthenticated}
	}
	wireRequest, err := mapping.ToWireExchangeAuthorizationTokenRequest(ctx, request)
	if err != nil {
		return semantic.ExchangeResponse{}, IssuedAuthorizationCredentials{}, err
	}
	callCtx := withClientProtocol(ctx, semantic.ProtocolMajor, semantic.ProtocolMinor, semantic.CapabilityName)
	values, _ := metadata.FromOutgoingContext(callCtx)
	values = withoutSessionOutgoingMetadata(values.Copy())
	if credentials.ClientSecret != "" {
		values.Set(AuthorizationClientSecretMetadataKey, credentials.ClientSecret)
	}
	if credentials.AuthorizationCode != "" {
		values.Set(AuthorizationCodeMetadataKey, credentials.AuthorizationCode)
	}
	if credentials.CodeVerifier != "" {
		values.Set(AuthorizationCodeVerifierMetadataKey, credentials.CodeVerifier)
	}
	if credentials.RefreshToken != "" {
		values.Set(AuthorizationRefreshTokenMetadataKey, credentials.RefreshToken)
	}
	callCtx = metadata.NewOutgoingContext(callCtx, values)
	var header metadata.MD
	options := append(authorizationCallOptions(), grpc.Header(&header))
	wireResponse, err := c.exchange(callCtx, wireRequest, options...)
	if err != nil {
		return semantic.ExchangeResponse{}, IssuedAuthorizationCredentials{}, mapping.FromPlatformStatusError(err, semantic.CapabilityName)
	}
	response, err := mapping.FromWireExchangeAuthorizationTokenResponse(wireResponse)
	if err != nil {
		return semantic.ExchangeResponse{}, IssuedAuthorizationCredentials{}, err
	}
	if err := semantic.ValidateExchangeResponseForRequest(request, response); err != nil {
		return semantic.ExchangeResponse{}, IssuedAuthorizationCredentials{}, err
	}
	issued, err := issuedAuthorizationCredentials(header, response)
	return response, issued, err
}

func authorizationCallOptions() []grpc.CallOption {
	return []grpc.CallOption{grpc.MaxCallSendMsgSize(envelope.MaxRequestWireBytes), grpc.MaxCallRecvMsgSize(envelope.MaxResponseWireBytes), grpc.WaitForReady(false)}
}

func validAuthorizationExchangeCredentials(request semantic.ExchangeRequest, value AuthorizationExchangeCredentials) bool {
	if len(value.ClientSecret) > maxAuthorizationClientSecretBytes || len(value.AuthorizationCode) > maxAuthorizationCodeBytes ||
		len(value.CodeVerifier) > maxAuthorizationCodeVerifierBytes || len(value.RefreshToken) > maxAuthorizationRefreshTokenBytes {
		return false
	}
	switch request.GrantType {
	case semantic.GrantTypeAuthorizationCode:
		return value.AuthorizationCode != "" && value.RefreshToken == ""
	case semantic.GrantTypeRefreshToken:
		return value.RefreshToken != "" && value.AuthorizationCode == "" && value.CodeVerifier == ""
	case semantic.GrantTypeClientCredentials:
		return value.ClientSecret != "" && value.AuthorizationCode == "" && value.CodeVerifier == "" && value.RefreshToken == ""
	default:
		return false
	}
}

func issuedAuthorizationCredentials(header metadata.MD, response semantic.ExchangeResponse) (IssuedAuthorizationCredentials, error) {
	access, refresh := header.Get(IssuedAuthorizationAccessTokenMetadataKey), header.Get(IssuedAuthorizationRefreshTokenMetadataKey)
	if response.ProtocolError != nil {
		if len(access) != 0 || len(refresh) != 0 {
			return IssuedAuthorizationCredentials{}, &semantic.Failure{Category: semantic.FailureInternal}
		}
		return IssuedAuthorizationCredentials{}, nil
	}
	if response.Token == nil || len(access) != 1 || response.Token.RefreshTokenIssued != (len(refresh) == 1) {
		return IssuedAuthorizationCredentials{}, &semantic.Failure{Category: semantic.FailureInternal}
	}
	return IssuedAuthorizationCredentials{AccessToken: access[0], RefreshToken: func() string {
		if len(refresh) == 1 {
			return refresh[0]
		}
		return ""
	}()}, nil
}

func issuedAuthorizationCode(header metadata.MD, response semantic.CompleteResponse) (IssuedAuthorizationCredentials, error) {
	codes := header.Get(IssuedAuthorizationCodeMetadataKey)
	if response.ProtocolError != nil {
		if len(codes) != 0 {
			return IssuedAuthorizationCredentials{}, &semantic.Failure{Category: semantic.FailureInternal}
		}
		return IssuedAuthorizationCredentials{}, nil
	}
	if !response.AuthorizationCodeIssued || len(codes) != 1 || !validSensitive(codes[0], maxAuthorizationCodeBytes) {
		return IssuedAuthorizationCredentials{}, &semantic.Failure{Category: semantic.FailureInternal}
	}
	return IssuedAuthorizationCredentials{AuthorizationCode: codes[0]}, nil
}

func SetIssuedAuthorizationCredentials(ctx context.Context, credentials IssuedAuthorizationCredentials) error {
	if ctx == nil {
		return &semantic.Failure{Category: semantic.FailureInternal}
	}
	method, _ := ctx.Value(methodContextKey{}).(string)
	values := metadata.MD{}
	switch method {
	case runtimev1.AuthorizationLifecycleService_CompleteAuthorization_FullMethodName:
		if !validSensitive(credentials.AuthorizationCode, maxAuthorizationCodeBytes) || credentials.AccessToken != "" || credentials.RefreshToken != "" {
			return &semantic.Failure{Category: semantic.FailureInternal}
		}
		values.Set(IssuedAuthorizationCodeMetadataKey, credentials.AuthorizationCode)
	case runtimev1.AuthorizationLifecycleService_ExchangeAuthorizationToken_FullMethodName:
		if credentials.AuthorizationCode != "" || !validSensitive(credentials.AccessToken, maxSessionAccessTokenBytes) || len(credentials.RefreshToken) > maxAuthorizationRefreshTokenBytes {
			return &semantic.Failure{Category: semantic.FailureInternal}
		}
		values.Set(IssuedAuthorizationAccessTokenMetadataKey, credentials.AccessToken)
		if credentials.RefreshToken != "" {
			values.Set(IssuedAuthorizationRefreshTokenMetadataKey, credentials.RefreshToken)
		}
	default:
		return &semantic.Failure{Category: semantic.FailureInternal}
	}
	if err := grpc.SetHeader(ctx, values); err != nil {
		return &semantic.Failure{Category: semantic.FailureUnavailable}
	}
	return nil
}
