package runtimeidentity

import (
	"context"
	stderrors "errors"
	"net/http"
	"slices"
	"strings"
	"time"

	featureauth "github.com/nexuscrm/auth"
	"github.com/nexuscrm/backend/internal/runtimeapi"
	kernelauth "github.com/nexuscrm/kernel/pkg/auth"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-api-host/runtimegrpc"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/sessionv1"
)

type FederatedIdentity struct {
	LoginIdentifier string
}

type FederatedIDTokenVerifier func(context.Context, string) (FederatedIdentity, error)
type FederatedPrincipalResolver func(context.Context, string) (string, error)

type sessionAuthAuthority interface {
	AuthorizingSessionAdmissionReady() bool
	Login(context.Context, string, string, string, string, *featureauth.SessionOptions) (*featureauth.LoginResult, error)
	CreateSessionForUser(context.Context, string, string, string, *featureauth.SessionOptions) (*models.SessionToken, error)
	CreateControlledSessionForUser(context.Context, string, string, string, *featureauth.SessionOptions) (*featureauth.SessionTokenResult, error)
	AdmitAuthorizingSession(context.Context, string) (*featureauth.AuthorizingSessionValidation, error)
	AdmitAuthorizingSessionWithRenewalPreparation(context.Context, string, func(*featureauth.AuthorizingSessionValidation) error) (*featureauth.AuthorizingSessionValidation, string, error)
	AdmitAuthorizingSessionWithControlPreparation(context.Context, string, string, func(*featureauth.AuthorizingSessionValidation) error) (*featureauth.AuthorizingSessionValidation, string, time.Time, error)
	Logout(context.Context, string) error
	BrowserLogout(context.Context, string, string) error
}

type sessionOAuthAuthority interface {
	ResolveOAuthPolicy(context.Context, featureauth.OAuthPolicyRequest) (*featureauth.OAuthPolicyDecision, error)
	IsUserPermittedForApp(context.Context, *models.SystemConnectedApp, string) (bool, error)
}

type SessionDocumentPolicyReader func(context.Context, *kernelauth.Claims, semantic.DocumentPolicyRequest) (*semantic.DocumentPolicy, error)

type SessionLifecycleConfig struct {
	DocumentPolicy       SessionDocumentPolicyReader
	Auth                 sessionAuthAuthority
	OAuth                sessionOAuthAuthority
	Security             IssuanceSecuritySource
	AppInstallationID    string
	AuthorizedWorkloadID string
	FederatedVerifiers   map[string]FederatedIDTokenVerifier
	FederatedScopes      map[string]string
	FederatedPrincipals  FederatedPrincipalResolver
	Now                  func() time.Time
}

// SessionLifecycle reuses the existing durable Auth/OAuth owners behind the
// neutral Runtime contract. It owns no repository, token format, L1 dialect,
// provider HTTP route, or caller-selected authority fallback.
type SessionLifecycle struct {
	documentPolicy       SessionDocumentPolicyReader
	auth                 sessionAuthAuthority
	oauth                sessionOAuthAuthority
	security             IssuanceSecuritySource
	appInstallationID    string
	authorizedWorkloadID string
	federatedVerifiers   map[string]FederatedIDTokenVerifier
	federatedScopes      map[string]string
	federatedPrincipals  FederatedPrincipalResolver
	now                  func() time.Time
}

func NewSessionLifecycle(config SessionLifecycleConfig) (*SessionLifecycle, error) {
	if config.Auth == nil || !config.Auth.AuthorizingSessionAdmissionReady() ||
		config.OAuth == nil || config.Security == nil ||
		strings.TrimSpace(config.AppInstallationID) != config.AppInstallationID ||
		config.AppInstallationID == "" ||
		strings.TrimSpace(config.AuthorizedWorkloadID) != config.AuthorizedWorkloadID ||
		config.AuthorizedWorkloadID == "" {
		return nil, stderrors.New("runtime session lifecycle: auth, OAuth, security, installation, and workload authorities are required")
	}
	if config.Now == nil {
		config.Now = time.Now
	}
	verifiers := make(map[string]FederatedIDTokenVerifier, len(config.FederatedVerifiers))
	scopes := make(map[string]string, len(config.FederatedVerifiers))
	for rawProvider, verifier := range config.FederatedVerifiers {
		provider := strings.TrimSpace(rawProvider)
		scope, found := config.FederatedScopes[rawProvider]
		if provider == "" || provider != rawProvider || verifier == nil || !found ||
			strings.TrimSpace(scope) != scope || len(strings.Fields(scope)) != 1 {
			return nil, stderrors.New("runtime session lifecycle: federated verifier registry is invalid")
		}
		verifiers[provider] = verifier
		scopes[provider] = scope
	}
	if len(config.FederatedScopes) != len(verifiers) {
		return nil, stderrors.New("runtime session lifecycle: federated verifier registry is invalid")
	}
	if len(verifiers) != 0 && config.FederatedPrincipals == nil {
		return nil, stderrors.New("runtime session lifecycle: federated principal authority is required")
	}
	return &SessionLifecycle{
		auth: config.Auth, oauth: config.OAuth, security: config.Security, documentPolicy: config.DocumentPolicy,
		appInstallationID:    config.AppInstallationID,
		authorizedWorkloadID: config.AuthorizedWorkloadID,
		federatedVerifiers:   verifiers, federatedScopes: scopes,
		federatedPrincipals: config.FederatedPrincipals,
		now:                 config.Now,
	}, nil
}

func (s *SessionLifecycle) CreateSession(
	ctx context.Context,
	authority runtimeapi.RuntimeWorkloadCall,
	credentials runtimegrpc.SessionCreateCredentials,
	request semantic.CreateRequest,
) (semantic.CreateResponse, runtimegrpc.SessionIssuedCredentials, error) {
	if s == nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureUnavailable)
	}
	if err := semantic.ValidateCreateRequest(ctx, request); err != nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, err
	}
	if err := s.admit(ctx, authority, request.AppInstallationID); err != nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, err
	}
	switch request.Method {
	case semantic.AuthenticationMethodPassword:
		return s.createPasswordSession(ctx, credentials, request)
	case semantic.AuthenticationMethodFederatedIDToken:
		return s.createFederatedSession(ctx, credentials, request)
	default:
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, &semantic.Failure{
			Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentOperation,
		}
	}
}

func (s *SessionLifecycle) createPasswordSession(
	ctx context.Context,
	credentials runtimegrpc.SessionCreateCredentials,
	request semantic.CreateRequest,
) (semantic.CreateResponse, runtimegrpc.SessionIssuedCredentials, error) {
	decision, err := s.oauth.ResolveOAuthPolicy(ctx, featureauth.OAuthPolicyRequest{
		UseCase: featureauth.OAuthPolicyPasswordGrant, ClientID: request.ApplicationClientID,
		GrantType: featureauth.OAuthGrantPassword, RequestedScope: request.RequestedScope,
		PresentedSecret: credentials.ClientSecret,
	})
	if err != nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, classifySessionOwnerError(ctx, err)
	}
	if decision == nil || decision.App == nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureUnavailable)
	}
	sessionClass := decision.SessionIssuerClass
	browserLogin := request.IssueControlProof &&
		request.ApplicationClientID == featureauth.BuiltInBrowserClientID &&
		strings.TrimSpace(decision.App.ConsumerKey) == featureauth.BuiltInBrowserClientID
	if request.IssueControlProof && !browserLogin {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, &semantic.Failure{
			Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentOperation,
		}
	}
	if browserLogin && strings.TrimSpace(sessionClass) == "" {
		sessionClass = featureauth.OAuthSessionClassPlatformBrowser
	}
	opts := &featureauth.SessionOptions{
		ConnectedAppId: decision.App.ID, Scope: decision.EffectiveScope,
		GrantType: featureauth.OAuthGrantPassword, SessionClass: sessionClass,
		IssueControlProof: browserLogin && featureauth.IsInteractiveBrowserSession(
			sessionClass, decision.EffectiveScope, decision.App.ID, featureauth.OAuthGrantPassword,
		),
	}
	if request.IssueControlProof != opts.IssueControlProof {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, &semantic.Failure{
			Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentOperation,
		}
	}
	result, err := s.auth.Login(
		ctx, credentials.Identity, credentials.Secret,
		request.SourceAddress, request.UserAgent, opts,
	)
	if err != nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, classifySessionOwnerError(ctx, err)
	}
	if result == nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureUnavailable)
	}
	permitted, permissionErr := s.oauth.IsUserPermittedForApp(ctx, decision.App, result.User.ID)
	if permissionErr != nil || !permitted {
		revokeErr := s.auth.Logout(ctx, result.Token)
		if permissionErr != nil || revokeErr != nil {
			return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureUnavailable)
		}
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureForbidden)
	}
	return createdSessionResponse(request, result.User, result.ExpiresAt, result.Token, result.ControlProof)
}

func (s *SessionLifecycle) createFederatedSession(
	ctx context.Context,
	credentials runtimegrpc.SessionCreateCredentials,
	request semantic.CreateRequest,
) (semantic.CreateResponse, runtimegrpc.SessionIssuedCredentials, error) {
	verifier := s.federatedVerifiers[request.FederatedProviderID]
	if verifier == nil || s.federatedPrincipals == nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureForbidden)
	}
	identity, err := verifier(ctx, credentials.Secret)
	if err != nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, classifySessionOwnerError(ctx, err)
	}
	identifier := strings.TrimSpace(identity.LoginIdentifier)
	if identifier == "" || identifier != identity.LoginIdentifier {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureUnauthenticated)
	}
	principalID, err := s.federatedPrincipals(ctx, identifier)
	if err != nil || strings.TrimSpace(principalID) == "" || strings.TrimSpace(principalID) != principalID {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, classifySessionOwnerError(ctx, err)
	}
	options := &featureauth.SessionOptions{
		Scope:        s.federatedScopes[request.FederatedProviderID],
		GrantType:    "federated_id_token",
		SessionClass: featureauth.OAuthSessionClassPlatformBrowser,
	}
	if request.IssueControlProof {
		result, createErr := s.auth.CreateControlledSessionForUser(
			ctx, principalID, request.SourceAddress, request.UserAgent, options,
		)
		if createErr != nil {
			return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, classifySessionOwnerError(ctx, createErr)
		}
		if result == nil || result.SessionToken == nil {
			return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureUnavailable)
		}
		validation, validateErr := s.auth.AdmitAuthorizingSession(ctx, result.SessionToken.AccessToken)
		if validateErr != nil || validation == nil || validation.Claims == nil {
			_ = s.auth.BrowserLogout(ctx, result.SessionToken.AccessToken, result.ControlProof)
			return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, classifySessionOwnerError(ctx, validateErr)
		}
		return createdSessionResponse(
			request, validation.Claims.User, result.ExpiresAt, result.SessionToken.AccessToken,
			result.ControlProof,
		)
	}
	token, err := s.auth.CreateSessionForUser(
		ctx, principalID, request.SourceAddress, request.UserAgent, options,
	)
	if err != nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, classifySessionOwnerError(ctx, err)
	}
	if token == nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureUnavailable)
	}
	validation, err := s.auth.AdmitAuthorizingSession(ctx, token.AccessToken)
	if err != nil || validation == nil || validation.Claims == nil {
		_ = s.auth.Logout(ctx, token.AccessToken)
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, classifySessionOwnerError(ctx, err)
	}
	return createdSessionResponse(
		request, validation.Claims.User, validation.Claims.ExpiresAt.Time,
		token.AccessToken, "",
	)
}

func (s *SessionLifecycle) ReadSession(
	ctx context.Context,
	authority runtimeapi.RuntimeWorkloadCall,
	credentials runtimegrpc.SessionPresentedCredentials,
	request semantic.ReadRequest,
) (semantic.ReadResponse, runtimegrpc.SessionIssuedCredentials, error) {
	if s == nil {
		return semantic.ReadResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureUnavailable)
	}
	if err := semantic.ValidateReadRequest(ctx, request); err != nil {
		return semantic.ReadResponse{}, runtimegrpc.SessionIssuedCredentials{}, err
	}
	if err := s.admit(ctx, authority, request.AppInstallationID); err != nil {
		return semantic.ReadResponse{}, runtimegrpc.SessionIssuedCredentials{}, err
	}
	var documentPolicy *semantic.DocumentPolicy
	prepare := func(admitted *featureauth.AuthorizingSessionValidation) error {
		if admitted == nil || admitted.Claims == nil {
			return sessionLifecycleFailure(semantic.FailureUnavailable)
		}
		if request.DocumentPolicy != nil {
			if s.documentPolicy == nil {
				return sessionLifecycleFailure(semantic.FailureUnavailable)
			}
			var err error
			documentPolicy, err = s.documentPolicy(ctx, admitted.Claims, *request.DocumentPolicy)
			if err != nil {
				return err
			}
		}
		// Validate the complete policy decision before renewal can commit.
		// AllowRenewal permits (but does not require) a replacement receipt.
		return semantic.ValidateReadResponse(request, semantic.ReadResponse{
			Session:        sessionProjection(admitted.Claims.User, admitted.Claims.ExpiresAt.Time),
			DocumentPolicy: documentPolicy,
		})
	}
	var (
		validation  *featureauth.AuthorizingSessionValidation
		replacement string
		proofExpiry time.Time
		err         error
	)
	switch {
	case request.UseControlProof:
		validation, replacement, proofExpiry, err = s.auth.AdmitAuthorizingSessionWithControlPreparation(
			ctx, credentials.AccessToken, credentials.ControlProof, prepare,
		)
	case request.AllowRenewal:
		validation, replacement, err = s.auth.AdmitAuthorizingSessionWithRenewalPreparation(
			ctx, credentials.AccessToken, prepare,
		)
	default:
		validation, err = s.auth.AdmitAuthorizingSession(ctx, credentials.AccessToken)
		if err == nil {
			err = prepare(validation)
		}
	}
	if err != nil {
		return semantic.ReadResponse{}, runtimegrpc.SessionIssuedCredentials{}, classifySessionOwnerError(ctx, err)
	}
	if validation == nil || validation.Claims == nil {
		return semantic.ReadResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureUnavailable)
	}
	response := semantic.ReadResponse{
		Session:                      sessionProjection(validation.Claims.User, validation.Claims.ExpiresAt.Time),
		ReplacementAccessTokenIssued: replacement != "",
	}
	response.DocumentPolicy = documentPolicy
	if !proofExpiry.IsZero() {
		response.ControlProofExpiresAtUnixSecond = proofExpiry.Unix()
	}
	if err := semantic.ValidateReadResponse(request, response); err != nil {
		return semantic.ReadResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureInternal)
	}
	return response, runtimegrpc.SessionIssuedCredentials{AccessToken: replacement}, nil
}

func (s *SessionLifecycle) RevokeSession(
	ctx context.Context,
	authority runtimeapi.RuntimeWorkloadCall,
	credentials runtimegrpc.SessionPresentedCredentials,
	request semantic.RevokeRequest,
) error {
	if s == nil {
		return sessionLifecycleFailure(semantic.FailureUnavailable)
	}
	if err := semantic.ValidateRevokeRequest(ctx, request); err != nil {
		return err
	}
	if err := s.admit(ctx, authority, request.AppInstallationID); err != nil {
		return err
	}
	var err error
	if request.UseControlProof {
		err = s.auth.BrowserLogout(ctx, credentials.AccessToken, credentials.ControlProof)
	} else {
		err = s.auth.Logout(ctx, credentials.AccessToken)
	}
	if err != nil {
		return classifySessionOwnerError(ctx, err)
	}
	return nil
}

func (s *SessionLifecycle) admit(ctx context.Context, trusted runtimeapi.RuntimeWorkloadCall, appInstallationID string) error {
	if trusted.OrgID == "" || trusted.CallerWorkloadID == "" || trusted.SecurityEpoch == 0 ||
		trusted.Capability != semantic.CapabilityName {
		return sessionLifecycleFailure(semantic.FailureUnauthenticated)
	}
	if appInstallationID != s.appInstallationID ||
		trusted.CallerWorkloadID != s.authorizedWorkloadID {
		return sessionLifecycleFailure(semantic.FailureForbidden)
	}
	state, err := s.security(ctx, appInstallationID, trusted.CallerWorkloadID)
	if err != nil {
		return classifySessionOwnerError(ctx, err)
	}
	if !state.StateFound {
		return sessionLifecycleFailure(semantic.FailureUnavailable)
	}
	if !state.BindingFound || !state.BindingActive || state.OrgID != trusted.OrgID ||
		!slices.Contains(state.Capabilities, semantic.CapabilityName) {
		return sessionLifecycleFailure(semantic.FailureForbidden)
	}
	if state.SecurityEpoch == 0 {
		return sessionLifecycleFailure(semantic.FailureUnavailable)
	}
	if trusted.SecurityEpoch != state.SecurityEpoch || !state.LeaseFound ||
		state.LeaseSecurityEpoch != state.SecurityEpoch || state.LeaseExpiresAt.IsZero() ||
		!s.now().Before(state.LeaseExpiresAt) {
		served := trusted.SecurityEpoch
		if trusted.SecurityEpoch == state.SecurityEpoch {
			served = state.LeaseSecurityEpoch
		}
		return &semantic.Failure{
			Category: semantic.FailureStaleState, StaleAxis: semantic.StaleAxisSecurity,
			RequiredRevision: state.SecurityEpoch, ServedRevision: served,
		}
	}
	return nil
}

func createdSessionResponse(
	request semantic.CreateRequest,
	user kernelauth.UserSession,
	expiresAt time.Time,
	accessToken, controlProof string,
) (semantic.CreateResponse, runtimegrpc.SessionIssuedCredentials, error) {
	if strings.TrimSpace(accessToken) == "" || (controlProof != "") != request.IssueControlProof {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureInternal)
	}
	response := semantic.CreateResponse{
		Session:           sessionProjection(user, expiresAt),
		AccessTokenIssued: true, ControlProofIssued: controlProof != "",
	}
	if err := semantic.ValidateCreateResponse(request, response); err != nil {
		return semantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, sessionLifecycleFailure(semantic.FailureInternal)
	}
	return response, runtimegrpc.SessionIssuedCredentials{
		AccessToken: accessToken, ControlProof: controlProof,
	}, nil
}

func sessionProjection(user kernelauth.UserSession, expiresAt time.Time) semantic.Session {
	roleID := ""
	if user.RoleId != nil {
		roleID = *user.RoleId
	}
	return semantic.Session{
		Principal: semantic.Principal{
			PrincipalID: user.ID, DisplayName: user.Name, Email: user.Email,
			ProfileID: user.ProfileId, RoleID: roleID,
		},
		ExpiresAtUnixSecond: expiresAt.Unix(),
	}
}

func classifySessionOwnerError(ctx context.Context, err error) error {
	if ctxErr := ctx.Err(); ctxErr != nil {
		return ctxErr
	}
	if err == nil {
		return sessionLifecycleFailure(semantic.FailureUnavailable)
	}
	if oauthError, ok := featureauth.OAuthProtocolErrorFrom(err); ok {
		switch oauthError.Code {
		case "invalid_client", "invalid_grant":
			return sessionLifecycleFailure(semantic.FailureUnauthenticated)
		case "unauthorized_client", "access_denied":
			return sessionLifecycleFailure(semantic.FailureForbidden)
		default:
			return &semantic.Failure{Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentOperation}
		}
	}
	var statusError interface{ HTTPStatus() int }
	if stderrors.As(err, &statusError) {
		switch statusError.HTTPStatus() {
		case http.StatusUnauthorized, http.StatusNotFound:
			return sessionLifecycleFailure(semantic.FailureUnauthenticated)
		case http.StatusForbidden:
			return sessionLifecycleFailure(semantic.FailureForbidden)
		}
	}
	return sessionLifecycleFailure(semantic.FailureUnavailable)
}

func sessionLifecycleFailure(category semantic.FailureCategory) *semantic.Failure {
	return &semantic.Failure{Category: category}
}
