package runtimegrpc

import (
	"context"
	"errors"
	"strings"

	sessionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/sessionv1"
	"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 (
	SessionIdentityMetadataKey           = "x-nexuscrm-session-identity-bin"
	SessionSecretMetadataKey             = "x-nexuscrm-session-secret-bin"
	SessionClientSecretMetadataKey       = "x-nexuscrm-session-client-secret-bin"
	SessionControlProofMetadataKey       = "x-nexuscrm-session-control-proof-bin"
	IssuedSessionAccessTokenMetadataKey  = "x-nexuscrm-issued-session-access-token-bin"
	IssuedSessionControlProofMetadataKey = "x-nexuscrm-issued-session-control-proof-bin"

	maxSessionIdentityBytes     = 1024
	maxSessionSecretBytes       = 64 * 1024
	maxSessionClientSecretBytes = 16 * 1024
	maxSessionControlProofBytes = 4096
	maxSessionAccessTokenBytes  = 64 * 1024
)

type CreateSessionInvoker func(context.Context, *runtimev1.CreateSessionRequest, ...grpc.CallOption) (*runtimev1.CreateSessionResponse, error)
type ReadSessionInvoker func(context.Context, *runtimev1.ReadSessionRequest, ...grpc.CallOption) (*runtimev1.ReadSessionResponse, error)
type RevokeSessionInvoker func(context.Context, *runtimev1.RevokeSessionRequest, ...grpc.CallOption) (*runtimev1.RevokeSessionResponse, error)

// SessionCreateCredentials is transport-only sensitive input. Identity is the
// password-login identifier and Secret is either the password or a federated
// ID token, selected by the semantic request method.
type SessionCreateCredentials struct {
	Identity     string
	Secret       string
	ClientSecret string
}

// SessionPresentedCredentials is transport-only current session authority.
type SessionPresentedCredentials struct {
	AccessToken  string
	ControlProof string
}

// SessionIssuedCredentials is returned from response metadata and is never
// decoded into a semantic message.
type SessionIssuedCredentials struct {
	AccessToken  string
	ControlProof string
}

type SessionLifecycleClient struct {
	create CreateSessionInvoker
	read   ReadSessionInvoker
	revoke RevokeSessionInvoker
}

func NewSessionLifecycleClient(create CreateSessionInvoker, read ReadSessionInvoker, revoke RevokeSessionInvoker) (*SessionLifecycleClient, error) {
	if create == nil || read == nil || revoke == nil {
		return nil, errors.New("runtimegrpc: session create, read and revoke invokers are required")
	}
	return &SessionLifecycleClient{create: create, read: read, revoke: revoke}, nil
}

func (client *SessionLifecycleClient) CreateSession(
	ctx context.Context,
	credentials SessionCreateCredentials,
	request sessionsemantic.CreateRequest,
) (sessionsemantic.CreateResponse, SessionIssuedCredentials, error) {
	if client == nil || client.create == nil {
		return sessionsemantic.CreateResponse{}, SessionIssuedCredentials{}, sessionUnavailable()
	}
	if err := validateCreateSessionCredentials(request, credentials); err != nil {
		return sessionsemantic.CreateResponse{}, SessionIssuedCredentials{}, err
	}
	wireRequest, err := mapping.ToWireCreateSessionRequest(ctx, request)
	if err != nil {
		return sessionsemantic.CreateResponse{}, SessionIssuedCredentials{}, err
	}
	callCtx := withClientProtocol(ctx, sessionsemantic.ProtocolMajor, sessionsemantic.ProtocolMinor, sessionsemantic.CapabilityName)
	values, _ := metadata.FromOutgoingContext(callCtx)
	values = withoutSessionOutgoingMetadata(values.Copy())
	values.Set(SessionSecretMetadataKey, credentials.Secret)
	if credentials.Identity != "" {
		values.Set(SessionIdentityMetadataKey, credentials.Identity)
	}
	if credentials.ClientSecret != "" {
		values.Set(SessionClientSecretMetadataKey, credentials.ClientSecret)
	}
	callCtx = metadata.NewOutgoingContext(callCtx, values)
	var header metadata.MD
	wireResponse, err := client.create(callCtx, wireRequest,
		grpc.Header(&header),
		grpc.MaxCallSendMsgSize(envelope.MaxRequestWireBytes),
		grpc.MaxCallRecvMsgSize(envelope.MaxResponseWireBytes),
		grpc.WaitForReady(false),
	)
	if err != nil {
		return sessionsemantic.CreateResponse{}, SessionIssuedCredentials{}, mapping.FromPlatformStatusError(err, sessionsemantic.CapabilityName)
	}
	response, err := mapping.FromWireCreateSessionResponse(request, wireResponse)
	if err != nil {
		return sessionsemantic.CreateResponse{}, SessionIssuedCredentials{}, err
	}
	issued, err := issuedSessionCredentials(header, response.AccessTokenIssued, response.ControlProofIssued)
	if err != nil {
		return sessionsemantic.CreateResponse{}, SessionIssuedCredentials{}, err
	}
	return response, issued, nil
}

func (client *SessionLifecycleClient) ReadSession(
	ctx context.Context,
	credentials SessionPresentedCredentials,
	request sessionsemantic.ReadRequest,
) (sessionsemantic.ReadResponse, SessionIssuedCredentials, error) {
	if client == nil || client.read == nil {
		return sessionsemantic.ReadResponse{}, SessionIssuedCredentials{}, sessionUnavailable()
	}
	if err := validatePresentedSessionCredentials(request.UseControlProof, credentials); err != nil {
		return sessionsemantic.ReadResponse{}, SessionIssuedCredentials{}, err
	}
	wireRequest, err := mapping.ToWireReadSessionRequest(ctx, request)
	if err != nil {
		return sessionsemantic.ReadResponse{}, SessionIssuedCredentials{}, err
	}
	callCtx := withPresentedSessionCredentials(
		withClientProtocol(ctx, sessionsemantic.ProtocolMajor, sessionsemantic.ProtocolMinor, sessionsemantic.CapabilityName),
		credentials,
	)
	var header metadata.MD
	wireResponse, err := client.read(callCtx, wireRequest,
		grpc.Header(&header),
		grpc.MaxCallSendMsgSize(envelope.MaxRequestWireBytes),
		grpc.MaxCallRecvMsgSize(envelope.MaxResponseWireBytes),
		grpc.WaitForReady(false),
	)
	if err != nil {
		return sessionsemantic.ReadResponse{}, SessionIssuedCredentials{}, mapping.FromPlatformStatusError(err, sessionsemantic.CapabilityName)
	}
	response, err := mapping.FromWireReadSessionResponse(request, wireResponse)
	if err != nil {
		return sessionsemantic.ReadResponse{}, SessionIssuedCredentials{}, err
	}
	issued, err := issuedSessionCredentials(header, response.ReplacementAccessTokenIssued, false)
	if err != nil {
		return sessionsemantic.ReadResponse{}, SessionIssuedCredentials{}, err
	}
	return response, issued, nil
}

func (client *SessionLifecycleClient) RevokeSession(
	ctx context.Context,
	credentials SessionPresentedCredentials,
	request sessionsemantic.RevokeRequest,
) error {
	if client == nil || client.revoke == nil {
		return sessionUnavailable()
	}
	if err := validatePresentedSessionCredentials(request.UseControlProof, credentials); err != nil {
		return err
	}
	wireRequest, err := mapping.ToWireRevokeSessionRequest(ctx, request)
	if err != nil {
		return err
	}
	callCtx := withPresentedSessionCredentials(
		withClientProtocol(ctx, sessionsemantic.ProtocolMajor, sessionsemantic.ProtocolMinor, sessionsemantic.CapabilityName),
		credentials,
	)
	wireResponse, err := client.revoke(callCtx, wireRequest,
		grpc.MaxCallSendMsgSize(envelope.MaxRequestWireBytes),
		grpc.MaxCallRecvMsgSize(envelope.MaxResponseWireBytes),
		grpc.WaitForReady(false),
	)
	if err != nil {
		return mapping.FromPlatformStatusError(err, sessionsemantic.CapabilityName)
	}
	_, err = mapping.FromWireRevokeSessionResponse(wireResponse)
	return err
}

func validateCreateSessionCredentials(request sessionsemantic.CreateRequest, credentials SessionCreateCredentials) error {
	if len(credentials.Identity) > maxSessionIdentityBytes ||
		len(credentials.Secret) > maxSessionSecretBytes ||
		len(credentials.ClientSecret) > maxSessionClientSecretBytes ||
		credentials.Secret == "" {
		return &sessionsemantic.Failure{Category: sessionsemantic.FailureUnauthenticated}
	}
	switch request.Method {
	case sessionsemantic.AuthenticationMethodPassword:
		if credentials.Identity == "" {
			return &sessionsemantic.Failure{Category: sessionsemantic.FailureUnauthenticated}
		}
	case sessionsemantic.AuthenticationMethodFederatedIDToken:
		if credentials.Identity != "" || credentials.ClientSecret != "" {
			return &sessionsemantic.Failure{Category: sessionsemantic.FailureInvalidRequest, InvalidComponent: sessionsemantic.InvalidComponentOperation}
		}
	}
	return nil
}

func validatePresentedSessionCredentials(useControlProof bool, credentials SessionPresentedCredentials) error {
	if !validSensitive(credentials.AccessToken, maxSessionAccessTokenBytes) ||
		len(credentials.ControlProof) > maxSessionControlProofBytes ||
		(useControlProof != (credentials.ControlProof != "")) {
		return &sessionsemantic.Failure{Category: sessionsemantic.FailureUnauthenticated}
	}
	return nil
}

func withPresentedSessionCredentials(ctx context.Context, credentials SessionPresentedCredentials) context.Context {
	values, _ := metadata.FromOutgoingContext(ctx)
	values = withoutSessionOutgoingMetadata(values.Copy())
	values.Set(EndUserAuthorizationMetadataKey, "Bearer "+credentials.AccessToken)
	if credentials.ControlProof != "" {
		values.Set(SessionControlProofMetadataKey, credentials.ControlProof)
	}
	return metadata.NewOutgoingContext(ctx, values)
}

func withoutSessionOutgoingMetadata(values metadata.MD) metadata.MD {
	for _, key := range sessionSensitiveMetadataKeys() {
		values.Delete(key)
	}
	return values
}

func issuedSessionCredentials(values metadata.MD, requireAccessToken, requireControlProof bool) (SessionIssuedCredentials, error) {
	accessTokens := values.Get(IssuedSessionAccessTokenMetadataKey)
	controlProofs := values.Get(IssuedSessionControlProofMetadataKey)
	if (requireAccessToken && len(accessTokens) != 1) || (!requireAccessToken && len(accessTokens) != 0) ||
		(requireControlProof && len(controlProofs) != 1) || (!requireControlProof && len(controlProofs) != 0) {
		return SessionIssuedCredentials{}, &sessionsemantic.Failure{Category: sessionsemantic.FailureInternal}
	}
	issued := SessionIssuedCredentials{}
	if len(accessTokens) == 1 {
		issued.AccessToken = accessTokens[0]
		if !validSensitive(issued.AccessToken, maxSessionAccessTokenBytes) {
			return SessionIssuedCredentials{}, &sessionsemantic.Failure{Category: sessionsemantic.FailureInternal}
		}
	}
	if len(controlProofs) == 1 {
		issued.ControlProof = controlProofs[0]
		if !validSensitive(issued.ControlProof, maxSessionControlProofBytes) {
			return SessionIssuedCredentials{}, &sessionsemantic.Failure{Category: sessionsemantic.FailureInternal}
		}
	}
	return issued, nil
}

// SetIssuedSessionCredentials installs access and control credentials in
// protected response metadata. Runtime handlers call it before returning a
// semantic response whose issued booleans match the non-empty values.
func SetIssuedSessionCredentials(ctx context.Context, credentials SessionIssuedCredentials) error {
	if ctx == nil || !validSensitive(credentials.AccessToken, maxSessionAccessTokenBytes) ||
		(credentials.ControlProof != "" && !validSensitive(credentials.ControlProof, maxSessionControlProofBytes)) {
		return &sessionsemantic.Failure{Category: sessionsemantic.FailureInternal}
	}
	method, _ := ctx.Value(methodContextKey{}).(string)
	switch method {
	case runtimev1.SessionLifecycleService_CreateSession_FullMethodName:
		// CreateSession may establish both the public bearer and the browser
		// control proof.
	case runtimev1.SessionLifecycleService_ReadSession_FullMethodName:
		// Renewal rotates only the public bearer. The browser proof remains
		// bound to the durable session and must never be re-issued here.
		if credentials.ControlProof != "" {
			return &sessionsemantic.Failure{Category: sessionsemantic.FailureInternal}
		}
	default:
		// The trusted host method context is the response-side ownership
		// fence. A Runtime handler cannot turn an unrelated RPC into a
		// credential disclosure channel by calling this helper.
		return &sessionsemantic.Failure{Category: sessionsemantic.FailureInternal}
	}
	values := metadata.Pairs(IssuedSessionAccessTokenMetadataKey, credentials.AccessToken)
	if credentials.ControlProof != "" {
		values.Set(IssuedSessionControlProofMetadataKey, credentials.ControlProof)
	}
	if err := grpc.SetHeader(ctx, values); err != nil {
		return &sessionsemantic.Failure{Category: sessionsemantic.FailureUnavailable}
	}
	return nil
}

func validSensitive(value string, maximum int) bool {
	return value != "" && len(value) <= maximum && strings.TrimSpace(value) == value
}

func sessionUnavailable() error {
	return &sessionsemantic.Failure{Category: sessionsemantic.FailureUnavailable}
}
