package auth

import (
	"context"
	stderrors "errors"
	"strings"
	"time"

	kernelauth "github.com/nexuscrm/kernel/pkg/auth"
	"github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
)

// AuthorizingSessionValidation carries the source-compatible claims projection
// together with the current authoritative subject and strong Permission State
// floor required by the additive authorization path.
type AuthorizingSessionValidation struct {
	Claims          *kernelauth.Claims
	Subject         permissionstatev1.Subject
	PermissionFloor permissionstatev1.Version
}

// RuntimeAuthorizingSessionValidation is the bearer-free current authority
// projection used after a Runtime delegation has supplied only verified
// session and principal selectors.
type RuntimeAuthorizingSessionValidation struct {
	User            models.UserSession
	AuthEpoch       uint64
	Subject         permissionstatev1.Subject
	PermissionFloor permissionstatev1.Version
}

// RuntimeIdentitySessionValidation is the one strong bearer admission result
// used by Runtime Identity when issuing a delegation. It carries SessionID and
// AuthEpoch together with the already-validated Runtime projection so the
// issuer does not repeat the authoritative session read.
type RuntimeIdentitySessionValidation struct {
	SessionID string
	RuntimeAuthorizingSessionValidation
}

// AuthorizingSessionAdmissionReady reports whether this service owns the
// additive strong repository capability. Production authorizing compositions
// use it to reject construction around the legacy split session readers.
func (s *AuthService) AuthorizingSessionAdmissionReady() bool {
	return s != nil && s.authorizingSessions != nil
}

// AdmitAuthorizingSession validates one non-renewing bearer through the strong
// authorizing-session repository capability.
func (s *AuthService) AdmitAuthorizingSession(
	ctx context.Context,
	tokenString string,
) (*AuthorizingSessionValidation, error) {
	validation, session, _, err := s.validateAuthorizingSessionForPolicy(
		ctx,
		tokenString,
		sessionValidationPolicy{},
	)
	if err != nil {
		return nil, err
	}
	s.reportAdmittedScope(validation, session)
	return validation, nil
}

// AdmitRuntimeIdentitySession returns every session fact Runtime Identity
// needs from one strong bearer admission. Runtime SecurityAdmission still
// revalidates the session for every data RPC; this removes only the duplicate
// read inside delegation issuance.
func (s *AuthService) AdmitRuntimeIdentitySession(
	ctx context.Context,
	tokenString string,
) (*RuntimeIdentitySessionValidation, error) {
	return s.admitRuntimeIdentitySessionForPolicy(ctx, tokenString, sessionValidationPolicy{})
}

// AdmitRuntimeIdentitySessionForAudience preserves the one-strong-read Runtime
// projection while requiring an exact resource audience selected by the trusted
// resource admission owner. Empty audience never falls back to ordinary host
// admission. This method does not itself admit a source scope or resource profile.
func (s *AuthService) AdmitRuntimeIdentitySessionForAudience(
	ctx context.Context, tokenString, audience string,
) (*RuntimeIdentitySessionValidation, error) {
	if strings.TrimSpace(audience) == "" {
		return nil, errors.NewUnauthorizedError("Invalid resource audience")
	}
	return s.admitRuntimeIdentitySessionForPolicy(ctx, tokenString, sessionValidationPolicy{expectedAudience: audience})
}

func (s *AuthService) admitRuntimeIdentitySessionForPolicy(
	ctx context.Context, tokenString string, policy sessionValidationPolicy,
) (*RuntimeIdentitySessionValidation, error) {
	validation, session, _, err := s.validateAuthorizingSessionForPolicy(
		ctx,
		tokenString,
		policy,
	)
	if err != nil {
		return nil, err
	}
	if validation == nil || validation.Claims == nil || session == nil ||
		session.AuthEpoch == 0 {
		return nil, newSecurityAuthorityUnavailable(
			stderrors.New("authorizing session projection is incomplete"),
		)
	}
	s.reportAdmittedScope(validation, session)
	return &RuntimeIdentitySessionValidation{
		SessionID: validation.Claims.RegisteredClaims.ID,
		RuntimeAuthorizingSessionValidation: RuntimeAuthorizingSessionValidation{
			User:      runtimeUserSessionFromClaims(validation.Claims.User),
			AuthEpoch: session.AuthEpoch, Subject: validation.Subject,
			PermissionFloor: validation.PermissionFloor,
		},
	}, nil
}

// AdmitAuthorizingSessionForAudience performs the same strong, durable
// admission as AdmitAuthorizingSession while requiring the JWT's sole audience
// to be one exact protected resource. It is for resource servers such as MCP;
// ordinary host routes continue to use the deployment-origin admission path.
func (s *AuthService) AdmitAuthorizingSessionForAudience(
	ctx context.Context,
	tokenString string,
	audience string,
) (*AuthorizingSessionValidation, error) {
	validation, session, _, err := s.validateAuthorizingSessionForPolicy(
		ctx,
		tokenString,
		sessionValidationPolicy{expectedAudience: audience},
	)
	if err != nil {
		return nil, err
	}
	s.reportAdmittedScope(validation, session)
	return validation, nil
}

// AdmitAuthorizingSessionWithRenewal admits an HTTP bearer and may return the
// durable replacement selected by the existing bounded renewal protocol.
func (s *AuthService) AdmitAuthorizingSessionWithRenewal(
	ctx context.Context,
	tokenString string,
) (*AuthorizingSessionValidation, string, error) {
	return s.AdmitAuthorizingSessionWithRenewalPreparation(ctx, tokenString, nil)
}

// AdmitAuthorizingSessionWithRenewalPreparation runs prepare after strong
// admission and before this call can persist a replacement bearer. A failed
// preparation returns without renewing. Existing overlap and CAS recovery
// remain owned by the normal renewal protocol.
func (s *AuthService) AdmitAuthorizingSessionWithRenewalPreparation(
	ctx context.Context,
	tokenString string,
	prepare func(*AuthorizingSessionValidation) error,
) (*AuthorizingSessionValidation, string, error) {
	validation, session, _, err := s.validateAuthorizingSessionForPolicy(
		ctx,
		tokenString,
		sessionValidationPolicy{allowRenewedBearerOverlap: true},
	)
	if err != nil {
		return nil, "", err
	}
	if prepare != nil {
		if err := prepare(validation); err != nil {
			return nil, "", err
		}
	}
	if session.Token != tokenString {
		s.reportAdmittedScope(validation, session)
		return validation, session.Token, nil
	}
	recoveredValidation, refreshedToken, err := s.renewAuthorizingInteractiveSession(
		ctx,
		validation.Claims,
		session,
		tokenString,
	)
	if err != nil {
		return nil, "", err
	}
	if recoveredValidation != nil {
		validation = recoveredValidation
	}
	s.reportAdmittedScope(validation, session)
	return validation, refreshedToken, nil
}

// AdmitAuthorizingSessionWithControl admits a browser bearer through the same
// strong read and applies the existing independent control-proof protocol.
func (s *AuthService) AdmitAuthorizingSessionWithControl(
	ctx context.Context,
	tokenString string,
	controlProof string,
) (*AuthorizingSessionValidation, string, time.Time, error) {
	return s.AdmitAuthorizingSessionWithControlPreparation(ctx, tokenString, controlProof, nil)
}

// AdmitAuthorizingSessionWithControlPreparation runs prepare only after the
// bearer passes strong admission and control-proof eligibility is resolved,
// before this call can persist
// a renewed pair. Preparation failure leaves the current pair unchanged.
func (s *AuthService) AdmitAuthorizingSessionWithControlPreparation(
	ctx context.Context,
	tokenString string,
	controlProof string,
	prepare func(*AuthorizingSessionValidation) error,
) (*AuthorizingSessionValidation, string, time.Time, error) {
	if s == nil {
		return nil, "", time.Time{}, errors.NewUnauthorizedError("Invalid session")
	}
	policy := sessionValidationPolicy{}
	if s.sessionControlRepo != nil {
		policy.controlProof = controlProof
	}
	validation, session, controlProofHash, err := s.validateAuthorizingSessionForPolicy(
		ctx,
		tokenString,
		policy,
	)
	if err != nil {
		return nil, "", time.Time{}, err
	}
	if prepare != nil {
		if err := prepare(validation); err != nil {
			return nil, "", time.Time{}, err
		}
	}
	if s.sessionControlRepo == nil || controlProofHash == "" {
		s.reportAdmittedScope(validation, session)
		return validation, "", time.Time{}, nil
	}
	if session.Token != tokenString {
		s.reportAdmittedScope(validation, session)
		return validation, session.Token, session.ExpiresAt, nil
	}
	recoveredValidation, refreshedToken, err :=
		s.renewAuthorizingInteractiveSessionWithControl(
			ctx,
			validation.Claims,
			session,
			tokenString,
			controlProof,
			controlProofHash,
		)
	if err != nil {
		return nil, "", time.Time{}, err
	}
	if recoveredValidation != nil {
		validation = recoveredValidation
	}
	s.reportAdmittedScope(validation, session)
	if refreshedToken == "" {
		return validation, "", time.Time{}, nil
	}
	return validation, refreshedToken, session.ExpiresAt, nil
}

// AdmitRuntimeAuthorizingSession performs one bearer-free strong admission
// using verified delegation selectors. It never falls back to the legacy
// session, user, role, or Connected App readers.
func (s *AuthService) AdmitRuntimeAuthorizingSession(
	ctx context.Context,
	sessionID string,
	principalID string,
) (*RuntimeAuthorizingSessionValidation, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	sessionID = strings.TrimSpace(sessionID)
	principalID = strings.TrimSpace(principalID)
	if sessionID == "" || principalID == "" {
		return nil, errors.NewUnauthorizedError("Invalid session")
	}
	if !s.AuthorizingSessionAdmissionReady() {
		return nil, newSecurityAuthorityUnavailable(
			stderrors.New("authorizing session repository is not configured"),
		)
	}
	// The state invariants come from the one owner, not from a copy. Every check it makes is
	// a way to admit a request that must not be admitted — a revoked session, a deauthorized
	// user, an incoherent subject — and a second implementation of them drifts silently,
	// because nothing fails when only one of the two is updated.
	state, err := s.loadValidatedAuthorizingSessionState(ctx, sessionID)
	if err != nil {
		return nil, err
	}
	session := state.Session

	// What stays here is what only this path can check: the caller names the principal it
	// expects, and a delegated runtime admission is only meaningful if the session actually
	// belongs to that principal. These are caller-influenced, so they are denials — unlike the
	// state's internal consistency, which the caller cannot affect and which the shared owner
	// therefore reports as the authority answering incoherently.
	//
	// AuthEpoch is required non-zero for the same reason it always was: a delegated admission
	// carries the epoch forward so a revocation can invalidate work already in flight, and an
	// epoch of zero would carry nothing.
	if session.UserID != principalID ||
		state.Subject.UserID() != principalID ||
		session.AuthEpoch == 0 {
		return nil, errors.NewUnauthorizedError("Invalid session")
	}
	runtimeUser, err := runtimeUserSessionFromAuthorizingState(state)
	if err != nil {
		return nil, err
	}

	// Reported after this path's own checks, not inside the shared state validator. The
	// validator cannot know what its callers will still refuse — a wrong principal here, a
	// stale bearer in the policy path — so reporting there counted rejections and the number
	// stopped meaning "credentials a deployment relies on", which is the only thing it is for.
	// Delegated and bearer-free: the caller proved a session id and a principal, not a browser
	// credential, so this is programmatic regardless of what the row carries.
	s.observeCredentialScope(
		CredentialScopeClassProgrammatic,
		durableSessionString(session.Scope),
	)
	return &RuntimeAuthorizingSessionValidation{
		User:            runtimeUser,
		AuthEpoch:       session.AuthEpoch,
		Subject:         state.Subject,
		PermissionFloor: state.PermissionFloor,
	}, nil
}

// runtimeUserSessionFromAuthorizingState restores the session-bound identity
// projection from the current signed bearer selected by the same strong read.
// Runtime never receives that bearer: Auth owns its validation and returns only
// the trusted projection. Current user/profile/role IDs remain owned by the
// durable Subject read, while names remain byte-identical to the login session
// as required by formula-session-context.md.
func runtimeUserSessionFromAuthorizingState(
	state *ports.AuthorizingSessionState,
) (models.UserSession, error) {
	if state == nil {
		return models.UserSession{}, errors.NewUnauthorizedError("Invalid session")
	}
	claims, err := kernelauth.ValidateToken(state.Session.Token)
	if err != nil || claims == nil {
		return models.UserSession{}, errors.NewUnauthorizedError("Invalid session")
	}
	if claims.RegisteredClaims.ID != state.Session.ID ||
		claims.Subject != state.Subject.UserID() ||
		claims.User.ID != state.Subject.UserID() ||
		claims.User.ProfileId != state.Subject.ProfileID() ||
		!authorizingRoleIDMatches(claims.User.RoleId, state.UserRoleID) {
		return models.UserSession{}, errors.NewUnauthorizedError("Invalid session")
	}

	email := claims.User.Email
	// SessionClass remains absent: unlike formula display identity it is a
	// bearer-owned handler authorization input, and a delegated Runtime call
	// must not inherit it.
	return models.UserSession{
		ID:             state.Subject.UserID(),
		Name:           claims.User.Name,
		Email:          &email,
		ProfileID:      state.Subject.ProfileID(),
		ProfileName:    claims.User.ProfileName,
		RoleID:         authorizingOptionalString(state.UserRoleID),
		RoleName:       cloneOptionalString(claims.User.RoleName),
		ConnectedAppId: durableSessionString(state.Session.ConnectedAppID),
		Scope:          durableSessionString(state.Session.Scope),
		GrantType:      durableSessionString(state.Session.GrantType),
	}, nil
}

func runtimeUserSessionFromClaims(user kernelauth.UserSession) models.UserSession {
	email := user.Email
	// SessionClass remains absent: unlike formula display identity it is a
	// bearer-owned handler authorization input, and a delegated Runtime call
	// must not inherit it.
	return models.UserSession{
		ID: user.ID, Name: user.Name, Email: &email,
		ProfileID: user.ProfileId, ProfileName: user.ProfileName,
		RoleID: cloneOptionalString(user.RoleId), RoleName: cloneOptionalString(user.RoleName),
		ConnectedAppId: user.ConnectedAppId, Scope: user.Scope, GrantType: user.GrantType,
	}
}

func cloneOptionalString(value *string) *string {
	if value == nil {
		return nil
	}
	result := *value
	return &result
}

func (s *AuthService) validateAuthorizingSessionForPolicy(
	ctx context.Context,
	tokenString string,
	policy sessionValidationPolicy,
) (*AuthorizingSessionValidation, *models.SystemSession, string, error) {
	var claims *kernelauth.Claims
	var err error
	if audience := strings.TrimSpace(policy.expectedAudience); audience != "" {
		claims, err = kernelauth.ValidateTokenForAudience(tokenString, audience)
	} else {
		claims, err = kernelauth.ValidateToken(tokenString)
	}
	if err != nil {
		return nil, nil, "", errors.NewUnauthorizedError("Invalid session")
	}
	if err := ctx.Err(); err != nil {
		return nil, nil, "", err
	}
	if s == nil || s.authorizingSessions == nil {
		return nil, nil, "", newSecurityAuthorityUnavailable(
			stderrors.New("authorizing session repository is not configured"),
		)
	}
	state, err := s.loadValidatedAuthorizingSessionState(ctx, claims.RegisteredClaims.ID)
	if err != nil {
		return nil, nil, "", err
	}
	session := &state.Session

	presentedIsCurrent := session.Token == tokenString
	allowOverlap := policy.allowRenewedBearerOverlap &&
		session.ControlProofHash == nil &&
		allowsRenewedBearerOverlap(claims, session, time.Now())
	controlProofHash, controlProofMatches := matchingSessionControlProofHash(
		policy.controlProof,
		session.ControlProofHash,
	)
	controlCandidate := controlProofMatches &&
		claims.RegisteredClaims.ID == session.ID &&
		claims.User.ID == session.UserID
	if !presentedIsCurrent && !allowOverlap && !controlCandidate {
		return nil, nil, "", errors.NewUnauthorizedError("Invalid session")
	}

	// Profile and role identity are session-bound in the JWT. Reject durable
	// drift instead of combining current IDs with historical or empty names.
	if claims.User.ID != state.Subject.UserID() ||
		claims.Subject != state.Subject.UserID() ||
		claims.User.ProfileId != state.Subject.ProfileID() ||
		!authorizingRoleIDMatches(claims.User.RoleId, state.UserRoleID) {
		return nil, nil, "", errors.NewUnauthorizedError("Invalid session")
	}
	claims.User.Scope, err = reconcileDurableSessionGrant(
		claims.User.Scope,
		session.Scope,
	)
	if err != nil {
		return nil, nil, "", err
	}
	claims.User.ConnectedAppId, err = reconcileDurableSessionGrant(
		claims.User.ConnectedAppId,
		session.ConnectedAppID,
	)
	if err != nil {
		return nil, nil, "", err
	}
	claims.User.GrantType, err = reconcileDurableSessionGrant(
		claims.User.GrantType,
		session.GrantType,
	)
	if err != nil {
		return nil, nil, "", err
	}
	if controlCandidate && !IsInteractiveBrowserSession(
		claims.User.SessionClass,
		claims.User.Scope,
		claims.User.ConnectedAppId,
		claims.User.GrantType,
	) {
		controlCandidate = false
	}
	if !presentedIsCurrent && !allowOverlap && !controlCandidate {
		return nil, nil, "", errors.NewUnauthorizedError("Invalid session")
	}
	if !controlCandidate {
		controlProofHash = ""
	}

	return &AuthorizingSessionValidation{
		Claims:          claims,
		Subject:         state.Subject,
		PermissionFloor: state.PermissionFloor,
	}, session, controlProofHash, nil
}

// reportAdmittedScope records one admission for the empty-scope inventory.
//
// It is called from the *public* admission methods and nowhere else. Two review rounds narrowed
// that: reporting from the shared state validator counted what its callers went on to refuse, and
// reporting from the policy helper counted what *its* callers still refused during renewal or a
// failed compare-and-swap recovery. A public method is by definition the last point that can say
// no, so it is the only place the answer is settled.
//
// The scope comes from the admitted claims rather than the durable row, because that is the
// authority the request actually carries after reconciliation.
func (s *AuthService) reportAdmittedScope(
	validation *AuthorizingSessionValidation,
	session *models.SystemSession,
) {
	if validation == nil || validation.Claims == nil || session == nil {
		return
	}
	s.observeCredentialScope(
		bearerAdmissionCredentialClass(*session),
		validation.Claims.User.Scope,
	)
}

// loadValidatedAuthorizingSessionState is the one place the authoritative session state is
// read and checked.
//
// It exists because a second credential lane needs exactly these invariants, and a lane
// that re-derived them would drift: every check below is a way to admit a request that must
// not be admitted, and the failure would be silent. Any admission path that resolves a
// session id must come through here rather than calling the repository directly.
//
// The error classes are not interchangeable. A store that cannot answer is an outage —
// FailureAuthorityUnavailable — and reporting it as an invalid session would sign users out
// during an incident. An inconsistent subject is also an outage rather than a denial: the
// state is not something a caller can influence, so it means the authority is not
// answering coherently. A revoked, expired or unauthorized session is a genuine denial.
func (s *AuthService) loadValidatedAuthorizingSessionState(
	ctx context.Context,
	sessionID string,
) (*ports.AuthorizingSessionState, error) {
	if s == nil || s.authorizingSessions == nil {
		return nil, newSecurityAuthorityUnavailable(
			stderrors.New("authorizing session repository is not configured"),
		)
	}
	state, err := s.authorizingSessions.LoadAuthorizingSessionState(ctx, sessionID)
	if err != nil {
		if ctxErr := ctx.Err(); ctxErr != nil {
			return nil, ctxErr
		}
		return nil, newSecurityAuthorityUnavailable(err)
	}
	// (nil, nil) is what persistence returns for a session that is no longer there, so this
	// is both the missing-session denial and the guard that keeps every dereference below
	// from panicking.
	if state == nil {
		return nil, errors.NewUnauthorizedError("Invalid session")
	}
	if err := state.PermissionFloor.Validate(); err != nil {
		return nil, newSecurityAuthorityUnavailable(err)
	}
	if !state.UserAuthorized || !state.ConnectedAppAuthorized {
		return nil, errors.NewUnauthorizedError("Invalid session")
	}
	if err := state.Subject.Validate(); err != nil ||
		state.Session.ID != sessionID ||
		state.Session.UserID != state.Subject.UserID() {
		return nil, newSecurityAuthorityUnavailable(
			stderrors.New("authorizing session subject is inconsistent"),
		)
	}
	// Re-checked here rather than trusted from whatever resolved the credential. The two
	// reads are not atomic, so a session revoked in between would otherwise still be
	// admitted — which is the whole point of revocation.
	session := &state.Session
	if session.IsRevoked ||
		session.ExpiresAt.IsZero() ||
		!time.Now().Before(session.ExpiresAt) ||
		strings.TrimSpace(session.Token) == "" {
		return nil, errors.NewUnauthorizedError("Invalid session")
	}

	return state, nil
}

func authorizingOptionalString(value string) *string {
	if value == "" {
		return nil
	}
	result := value
	return &result
}

func authorizingRoleIDMatches(claimed *string, current string) bool {
	if claimed == nil {
		return current == ""
	}
	return *claimed == current
}

type securityAuthorityUnavailableError struct {
	cause error
}

func newSecurityAuthorityUnavailable(cause error) error {
	return &securityAuthorityUnavailableError{cause: cause}
}

func (err *securityAuthorityUnavailableError) Error() string {
	return "security authority unavailable"
}

func (err *securityAuthorityUnavailableError) Unwrap() error {
	return &permissionstatev1.Failure{
		Category: permissionstatev1.FailureAuthorityUnavailable,
	}
}

func (err *securityAuthorityUnavailableError) Cause() error {
	if err == nil {
		return nil
	}
	return err.cause
}
