package auth

import (
	"context"
	"time"

	kernelauth "github.com/nexuscrm/kernel/pkg/auth"
	"github.com/nexuscrm/kernel/pkg/errors"
)

// AdmitAuthorizingBrowserSession admits a first-party browser request whose only
// credential is its session-control proof.
//
// It composes two owners rather than deriving anything itself:
//
//   - ValidateBrowserSession resolves the proof to a live durable session, including the
//     verifier lookup, the fail-closed ambiguity check and the identity projection;
//   - loadValidatedAuthorizingSessionState resolves the authoritative Subject and
//     Permission State floor from that session id, *and* applies the fail-closed state
//     invariants — the same function the bearer path uses, not the same repository call.
//     That distinction is the finding this was rewritten for: calling the repository
//     directly skipped the nil-state guard, the revoked/expired re-check, the
//     authorization flags and the subject-consistency check, so a session revoked between
//     the two reads was still admitted and a missing session panicked.
//
// The floor is what makes this a real admission rather than a shortcut. A browser request
// must be authorized against the identical snapshot a bearer request would be, so the
// answer cannot depend on which credential the page happened to hold.
//
// No replacement credential is produced and none can be: there is no bearer in this
// exchange to replace, and api-host's browser mode fails the request if an admission
// returns one. The returned expiry is the durable session's, so the transport can reissue
// the same proof cookie through it.
func (s *AuthService) AdmitAuthorizingBrowserSession(
	ctx context.Context,
	controlProof string,
) (*AuthorizingSessionValidation, time.Time, error) {
	if s == nil {
		return nil, time.Time{}, errors.NewUnauthorizedError("Invalid session")
	}

	browserSession, err := s.ValidateBrowserSession(ctx, controlProof)
	if err != nil {
		return nil, time.Time{}, err
	}
	if browserSession == nil {
		return nil, time.Time{}, errors.NewUnauthorizedError("Invalid session")
	}

	state, err := s.loadValidatedAuthorizingSessionState(ctx, browserSession.SessionID)
	if err != nil {
		return nil, time.Time{}, err
	}
	// The projection and the authoritative state are two reads, so they can disagree. An
	// admission built from halves of two different truths is an admission of neither, and
	// the halves that matter are the ones authorization is computed from: identity, role,
	// and the durable grants.
	if browserSession.User.ID != state.Subject.UserID() ||
		browserSession.User.ProfileId != state.Subject.ProfileID() ||
		!authorizingRoleIDMatches(browserSession.User.RoleId, state.UserRoleID) {
		return nil, time.Time{}, errors.NewUnauthorizedError("Invalid session")
	}
	// Scope, connected app and grant type are reconciled through the same owner the bearer
	// path uses rather than compared here. They are standing authority — an empty scope
	// currently means full access — so a projection that disagreed with the durable row
	// would authorize the browser differently from every other credential on the same
	// session. reconcileDurableSessionGrant is what decides whether a difference is a
	// legitimate narrowing or a conflict that must fail closed.
	reconciled := browserSession.User
	if reconciled.Scope, err = reconcileDurableSessionGrant(
		reconciled.Scope,
		state.Session.Scope,
	); err != nil {
		return nil, time.Time{}, err
	}
	if reconciled.ConnectedAppId, err = reconcileDurableSessionGrant(
		reconciled.ConnectedAppId,
		state.Session.ConnectedAppID,
	); err != nil {
		return nil, time.Time{}, err
	}
	if reconciled.GrantType, err = reconcileDurableSessionGrant(
		reconciled.GrantType,
		state.Session.GrantType,
	); err != nil {
		return nil, time.Time{}, err
	}

	// After the drift checks above, so a projection that disagreed with the authoritative state
	// is refused rather than counted.
	s.observeCredentialScope(CredentialScopeClassBrowser, reconciled.Scope)

	// Claims is the in-process carrier the admission contract is expressed in, not a
	// token: nothing here is signed, and nothing downstream may sign it, because the
	// browser lane forbids producing a bearer. The session id and the projected user are
	// the only fields any consumer reads.
	claims := &kernelauth.Claims{User: reconciled}
	claims.RegisteredClaims.ID = browserSession.SessionID

	return &AuthorizingSessionValidation{
		Claims:          claims,
		Subject:         state.Subject,
		PermissionFloor: state.PermissionFloor,
	}, browserSession.ExpiresAt, nil
}
