package runtimeidentity

import (
	"context"
	"encoding/json"
	stderrors "errors"
	"slices"
	"strings"
	"time"

	featureauth "github.com/nexuscrm/auth"
	"github.com/nexuscrm/backend/internal/runtimeapi"
	kernelerrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-api-host/runtimegrpc"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/authorizationv1"
	federatedloginsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/federatedloginv1"
)

type authorizationOAuthAuthority interface {
	GetConnectedApp(context.Context, string) (*models.SystemConnectedApp, error)
	CreateAuthorizeRequestHandle(context.Context, featureauth.AuthorizeRequestCreateRequest) (*featureauth.AuthorizeRequestCreateResult, error)
	ResumeAuthorizeRequest(context.Context, featureauth.AuthorizeRequestResumeRequest) (*featureauth.AuthorizeRequestResumeResult, error)
	ExchangeTokenForRequest(context.Context, featureauth.TokenExchangeRequest) (*models.SessionToken, error)
	ExchangeRefreshToken(context.Context, featureauth.RefreshTokenExchangeRequest) (*models.SessionToken, error)
	BeginSSOLoginTransaction(context.Context, string) (*featureauth.SSOLoginTransaction, error)
	CompleteSSOLoginTransaction(context.Context, string, string) error
	ExchangeClientCredentials(context.Context, featureauth.ClientCredentialsExchangeRequest) (*featureauth.ClientCredentialsExchangeResult, error)
}

type AuthorizationLifecycleConfig struct {
	Auth                 sessionAuthAuthority
	OAuth                authorizationOAuthAuthority
	Security             IssuanceSecuritySource
	AppInstallationID    string
	AuthorizedWorkloadID string
	Now                  func() time.Time
}

// AuthorizationLifecycle is a thin cross-process adapter over the existing
// durable OAuth owner. It owns no OAuth state, repository, HTTP redirect shape,
// provider exchange, or product-specific route. The same adapter also projects
// the pre-session federated-login transaction because both journeys share the
// exact AppInstallation/workload/security admission and durable OAuth owner.
type AuthorizationLifecycle struct {
	auth                 sessionAuthAuthority
	oauth                authorizationOAuthAuthority
	security             IssuanceSecuritySource
	appInstallationID    string
	authorizedWorkloadID string
	now                  func() time.Time
}

func NewAuthorizationLifecycle(config AuthorizationLifecycleConfig) (*AuthorizationLifecycle, 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 authorization lifecycle: auth, OAuth, security, installation, and workload authorities are required")
	}
	if config.Now == nil {
		config.Now = time.Now
	}
	return &AuthorizationLifecycle{auth: config.Auth, oauth: config.OAuth, security: config.Security,
		appInstallationID: config.AppInstallationID, authorizedWorkloadID: config.AuthorizedWorkloadID, now: config.Now}, nil
}

func (s *AuthorizationLifecycle) BeginAuthorization(ctx context.Context, authority runtimeapi.RuntimeWorkloadCall, request semantic.BeginRequest) (semantic.BeginResponse, error) {
	if s == nil {
		return semantic.BeginResponse{}, authorizationFailure(semantic.FailureUnavailable)
	}
	if err := semantic.ValidateBeginRequest(ctx, request); err != nil {
		return semantic.BeginResponse{}, err
	}
	if err := s.admit(ctx, authority, request.AppInstallationID); err != nil {
		return semantic.BeginResponse{}, err
	}
	trustedRedirect := s.trustedRedirect(ctx, request.ClientID, request.RedirectURI)
	result, err := s.oauth.CreateAuthorizeRequestHandle(ctx, featureauth.AuthorizeRequestCreateRequest{
		ClientID: request.ClientID, RedirectURI: request.RedirectURI, ResponseType: request.ResponseType,
		Scope: request.Scope, State: request.State, CodeChallenge: request.CodeChallenge,
		CodeChallengeMethod: request.CodeChallengeMethod, Issuer: request.Issuer, Resource: request.Resource,
		ResourcePresent: request.ResourcePresent,
	})
	if err != nil {
		if protocol := authorizationProtocolError(err, trustedRedirect, request.State); protocol != nil {
			response := semantic.BeginResponse{ProtocolError: protocol}
			if validateErr := semantic.ValidateBeginResponse(response); validateErr != nil {
				return semantic.BeginResponse{}, authorizationFailure(semantic.FailureInternal)
			}
			return response, nil
		}
		return semantic.BeginResponse{}, classifyAuthorizationOwnerError(ctx, err)
	}
	if result == nil {
		return semantic.BeginResponse{}, authorizationFailure(semantic.FailureInternal)
	}
	response := semantic.BeginResponse{RequestHandle: result.Handle, ExpiresAtUnixSecond: result.ExpiresAt.Unix()}
	if err := semantic.ValidateBeginResponse(response); err != nil {
		return semantic.BeginResponse{}, authorizationFailure(semantic.FailureInternal)
	}
	return response, nil
}

func (s *AuthorizationLifecycle) BeginFederatedLogin(ctx context.Context, authority runtimeapi.RuntimeWorkloadCall, request federatedloginsemantic.BeginRequest) (federatedloginsemantic.BeginResponse, error) {
	if s == nil {
		return federatedloginsemantic.BeginResponse{}, federatedLoginFailure(federatedloginsemantic.FailureUnavailable)
	}
	if err := federatedloginsemantic.ValidateBeginRequest(ctx, request); err != nil {
		return federatedloginsemantic.BeginResponse{}, err
	}
	if err := s.admit(ctx, authority, request.AppInstallationID); err != nil {
		return federatedloginsemantic.BeginResponse{}, federatedloginsemantic.NormalizeBeginOperationFailure(err)
	}
	transaction, err := s.oauth.BeginSSOLoginTransaction(ctx, request.RedirectURI)
	if err != nil {
		return federatedloginsemantic.BeginResponse{}, federatedloginsemantic.NormalizeBeginOperationFailure(classifyAuthorizationOwnerError(ctx, err))
	}
	if transaction == nil {
		return federatedloginsemantic.BeginResponse{}, federatedLoginFailure(federatedloginsemantic.FailureInternal)
	}
	response := federatedloginsemantic.BeginResponse{
		State: transaction.State, ExpiresAtUnixSecond: transaction.ExpiresAt.Unix(),
	}
	if err := federatedloginsemantic.ValidateBeginResponse(response); err != nil {
		return federatedloginsemantic.BeginResponse{}, federatedLoginFailure(federatedloginsemantic.FailureInternal)
	}
	return response, nil
}

func (s *AuthorizationLifecycle) ConsumeFederatedLogin(ctx context.Context, authority runtimeapi.RuntimeWorkloadCall, request federatedloginsemantic.ConsumeRequest) error {
	if s == nil {
		return federatedLoginFailure(federatedloginsemantic.FailureUnavailable)
	}
	if err := validateFederatedLoginConsumeRequest(ctx, request); err != nil {
		return err
	}
	if err := s.admit(ctx, authority, request.AppInstallationID); err != nil {
		return federatedloginsemantic.NormalizeConsumeOperationFailure(err)
	}
	if err := s.oauth.CompleteSSOLoginTransaction(ctx, request.State, request.RedirectURI); err != nil {
		return federatedloginsemantic.NormalizeConsumeOperationFailure(classifyAuthorizationOwnerError(ctx, err))
	}
	return nil
}

func validateFederatedLoginConsumeRequest(ctx context.Context, request federatedloginsemantic.ConsumeRequest) error {
	validationRequest := request
	if canonical, ok := featureauth.CanonicalLegacySSOLoginState(request.State); ok {
		// Only the exact state shape emitted before the provider-neutral Runtime
		// contract is admitted during the transaction TTL. Keep the original
		// value for the durable owner so it can address the pre-upgrade row.
		validationRequest.State = canonical
	}
	return federatedloginsemantic.ValidateConsumeRequest(ctx, validationRequest)
}

func (s *AuthorizationLifecycle) CompleteAuthorization(ctx context.Context, authority runtimeapi.RuntimeWorkloadCall, credentials runtimegrpc.SessionPresentedCredentials, request semantic.CompleteRequest) (semantic.CompleteResponse, runtimegrpc.IssuedAuthorizationCredentials, error) {
	if s == nil {
		return semantic.CompleteResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, authorizationFailure(semantic.FailureUnavailable)
	}
	if err := semantic.ValidateCompleteRequest(ctx, request); err != nil {
		return semantic.CompleteResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, err
	}
	if err := s.admit(ctx, authority, request.AppInstallationID); err != nil {
		return semantic.CompleteResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, err
	}
	validation, err := s.auth.AdmitAuthorizingSession(ctx, credentials.AccessToken)
	if err != nil || validation == nil || validation.Claims == nil {
		return semantic.CompleteResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, classifyAuthorizationOwnerError(ctx, err)
	}
	user := validation.Claims.User
	if !featureauth.IsInteractiveBrowserSession(user.SessionClass, user.Scope, user.ConnectedAppId, user.GrantType) {
		return semantic.CompleteResponse{ProtocolError: &semantic.ProtocolError{Code: "access_denied", SafeDescription: "interactive login is required"}}, runtimegrpc.IssuedAuthorizationCredentials{}, nil
	}
	result, err := s.oauth.ResumeAuthorizeRequest(ctx, featureauth.AuthorizeRequestResumeRequest{Handle: request.RequestHandle, UserID: user.ID})
	if err != nil {
		redirect, state := "", ""
		if result != nil {
			redirect, state = result.RedirectURI, result.State
		}
		if protocol := authorizationProtocolError(err, redirect, state); protocol != nil {
			return semantic.CompleteResponse{ProtocolError: protocol}, runtimegrpc.IssuedAuthorizationCredentials{}, nil
		}
		return semantic.CompleteResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, classifyAuthorizationOwnerError(ctx, err)
	}
	if result == nil {
		return semantic.CompleteResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, authorizationFailure(semantic.FailureInternal)
	}
	response := semantic.CompleteResponse{AuthorizationCodeIssued: true, RedirectURI: result.RedirectURI, State: result.State}
	if err := semantic.ValidateCompleteResponse(response); err != nil {
		return semantic.CompleteResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, authorizationFailure(semantic.FailureInternal)
	}
	return response, runtimegrpc.IssuedAuthorizationCredentials{AuthorizationCode: result.Code}, nil
}

func (s *AuthorizationLifecycle) ExchangeAuthorizationToken(ctx context.Context, authority runtimeapi.RuntimeWorkloadCall, credentials runtimegrpc.AuthorizationExchangeCredentials, request semantic.ExchangeRequest) (semantic.ExchangeResponse, runtimegrpc.IssuedAuthorizationCredentials, error) {
	if s == nil {
		return semantic.ExchangeResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, authorizationFailure(semantic.FailureUnavailable)
	}
	if err := semantic.ValidateExchangeRequest(ctx, request); err != nil {
		return semantic.ExchangeResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, err
	}
	if err := s.admit(ctx, authority, request.AppInstallationID); err != nil {
		return semantic.ExchangeResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, err
	}
	if !validAuthorizationExchangeCredentials(request.GrantType, credentials) {
		return semantic.ExchangeResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, authorizationFailure(semantic.FailureUnauthenticated)
	}
	var token *models.SessionToken
	var err error
	switch request.GrantType {
	case semantic.GrantTypeAuthorizationCode:
		token, err = s.oauth.ExchangeTokenForRequest(ctx, featureauth.TokenExchangeRequest{ClientID: request.ClientID,
			ClientSecret: credentials.ClientSecret, Code: credentials.AuthorizationCode, RedirectURI: request.RedirectURI,
			CodeVerifier: credentials.CodeVerifier, Issuer: request.Issuer, Resource: request.Resource, ResourcePresent: request.ResourcePresent})
	case semantic.GrantTypeRefreshToken:
		token, err = s.oauth.ExchangeRefreshToken(ctx, featureauth.RefreshTokenExchangeRequest{ClientID: request.ClientID,
			ClientSecret: credentials.ClientSecret, RefreshToken: credentials.RefreshToken, RequestedScope: request.RequestedScope, Issuer: request.Issuer})
	case semantic.GrantTypeClientCredentials:
		var result *featureauth.ClientCredentialsExchangeResult
		result, err = s.oauth.ExchangeClientCredentials(ctx, featureauth.ClientCredentialsExchangeRequest{
			ClientID: request.ClientID, ClientSecret: credentials.ClientSecret, RequestedScope: request.RequestedScope,
			Issuer: request.Issuer, Resource: request.Resource, ResourcePresent: request.ResourcePresent,
			UserAgent: "RuntimeAuthorizationLifecycle",
		})
		if result != nil {
			token = result.Token
		}
	}
	if err != nil {
		if protocol := authorizationProtocolError(err, "", ""); protocol != nil {
			return semantic.ExchangeResponse{ProtocolError: protocol}, runtimegrpc.IssuedAuthorizationCredentials{}, nil
		}
		return semantic.ExchangeResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, classifyAuthorizationOwnerError(ctx, err)
	}
	if token == nil || strings.TrimSpace(token.AccessToken) == "" {
		return semantic.ExchangeResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, authorizationFailure(semantic.FailureInternal)
	}
	response := semantic.ExchangeResponse{Token: &semantic.Token{TokenType: token.TokenType, ExpiresInSecond: int64(token.ExpiresIn),
		Scope: token.Scope, AccessTokenIssued: true, RefreshTokenIssued: token.RefreshToken != ""}}
	if err := semantic.ValidateExchangeResponseForRequest(request, response); err != nil {
		return semantic.ExchangeResponse{}, runtimegrpc.IssuedAuthorizationCredentials{}, authorizationFailure(semantic.FailureInternal)
	}
	return response, runtimegrpc.IssuedAuthorizationCredentials{AccessToken: token.AccessToken, RefreshToken: token.RefreshToken}, nil
}

func validAuthorizationExchangeCredentials(grant semantic.GrantType, credentials runtimegrpc.AuthorizationExchangeCredentials) bool {
	switch grant {
	case semantic.GrantTypeAuthorizationCode:
		return credentials.AuthorizationCode != "" && credentials.RefreshToken == ""
	case semantic.GrantTypeRefreshToken:
		return credentials.RefreshToken != "" && credentials.AuthorizationCode == "" && credentials.CodeVerifier == ""
	case semantic.GrantTypeClientCredentials:
		return credentials.ClientSecret != "" && credentials.AuthorizationCode == "" && credentials.CodeVerifier == "" && credentials.RefreshToken == ""
	default:
		return false
	}
}

func (s *AuthorizationLifecycle) trustedRedirect(ctx context.Context, clientID, redirectURI string) string {
	if strings.TrimSpace(clientID) == "" || strings.TrimSpace(redirectURI) == "" {
		return ""
	}
	app, err := s.oauth.GetConnectedApp(ctx, clientID)
	if err != nil || app == nil || featureauth.ValidateRedirectURIShape(redirectURI) != nil {
		return ""
	}
	for _, candidate := range connectedAppRedirects(app) {
		if candidate == redirectURI {
			return redirectURI
		}
	}
	return ""
}

func connectedAppRedirects(app *models.SystemConnectedApp) []string {
	if app == nil {
		return nil
	}
	if app.RedirectURIs != nil {
		raw := strings.TrimSpace(*app.RedirectURIs)
		if raw != "" {
			var values []string
			if strings.HasPrefix(raw, "[") && json.Unmarshal([]byte(raw), &values) == nil {
				return values
			}
			return strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == '\n' || r == '\r' || r == '\t' })
		}
	}
	if app.CallbackURL != nil && strings.TrimSpace(*app.CallbackURL) != "" {
		return []string{*app.CallbackURL}
	}
	return nil
}

func authorizationProtocolError(err error, trustedRedirect, state string) *semantic.ProtocolError {
	if oauthError, ok := featureauth.OAuthProtocolErrorFrom(err); ok {
		switch oauthError.Code {
		case "invalid_client", "invalid_request", "invalid_scope", "invalid_grant", "invalid_target",
			"unsupported_response_type", "unsupported_grant_type", "unauthorized_client", "access_denied":
		default:
			return nil
		}
		if oauthError.Status != 400 && oauthError.Status != 403 {
			return nil
		}
		if trustedRedirect == "" {
			state = ""
		}
		return &semantic.ProtocolError{Code: oauthError.Code, SafeDescription: oauthError.SafeDescription,
			TrustedRedirectURI: trustedRedirect, State: state}
	}
	var notFound *kernelerrors.NotFoundError
	if stderrors.As(err, &notFound) {
		return &semantic.ProtocolError{Code: "invalid_client", SafeDescription: "invalid client"}
	}
	var forbidden *kernelerrors.ForbiddenError
	if stderrors.As(err, &forbidden) {
		return &semantic.ProtocolError{Code: "access_denied", SafeDescription: "access denied"}
	}
	return nil
}

func classifyAuthorizationOwnerError(ctx context.Context, err error) error {
	return classifySessionOwnerError(ctx, err)
}

func (s *AuthorizationLifecycle) admit(ctx context.Context, trusted runtimeapi.RuntimeWorkloadCall, installationID string) error {
	if trusted.OrgID == "" || trusted.CallerWorkloadID == "" || trusted.SecurityEpoch == 0 || trusted.Capability != semantic.CapabilityName {
		return authorizationFailure(semantic.FailureUnauthenticated)
	}
	if installationID != s.appInstallationID || trusted.CallerWorkloadID != s.authorizedWorkloadID {
		return authorizationFailure(semantic.FailureForbidden)
	}
	state, err := s.security(ctx, installationID, trusted.CallerWorkloadID)
	if err != nil {
		return classifyAuthorizationOwnerError(ctx, err)
	}
	if !state.StateFound {
		return authorizationFailure(semantic.FailureUnavailable)
	}
	if !state.BindingFound || !state.BindingActive || state.OrgID != trusted.OrgID || !slices.Contains(state.Capabilities, semantic.CapabilityName) {
		return authorizationFailure(semantic.FailureForbidden)
	}
	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 authorizationFailure(category semantic.FailureCategory) error {
	return &semantic.Failure{Category: category}
}

func federatedLoginFailure(category federatedloginsemantic.FailureCategory) error {
	return &federatedloginsemantic.Failure{Category: category}
}
