package serverruntime

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	featureauth "github.com/nexuscrm/auth"
	"github.com/nexuscrm/backend/internal/features/streaming"
	jwtauth "github.com/nexuscrm/kernel/pkg/auth"
	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/flags"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/records"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
)

type apiHostAuthorizingSessionAdmitter struct {
	sessions                 *featureauth.AuthService
	apiTokens                *featureauth.ApiTokenService
	expectedAudienceResolver func() (string, error)
}

var _ hostruntime.AuthorizingSessionAdmitter = apiHostAuthorizingSessionAdmitter{}

func newAPIHostAuthorizingSessionAdmitter(
	sessions *featureauth.AuthService,
	apiTokens *featureauth.ApiTokenService,
) (apiHostAuthorizingSessionAdmitter, error) {
	return newAPIHostAuthorizingSessionAdmitterForAudienceResolver(sessions, apiTokens, nil)
}

// newAPIHostAuthorizingSessionAdmitterForAudienceResolver creates a strong
// admission path for one protected resource. The resolver is deliberately
// evaluated only while a bearer is admitted: local development can start with
// no public URL, while an MCP request with no canonical resource fails closed.
func newAPIHostAuthorizingSessionAdmitterForAudienceResolver(
	sessions *featureauth.AuthService,
	apiTokens *featureauth.ApiTokenService,
	expectedAudienceResolver func() (string, error),
) (apiHostAuthorizingSessionAdmitter, error) {
	if sessions == nil || !sessions.AuthorizingSessionAdmissionReady() {
		return apiHostAuthorizingSessionAdmitter{}, fmt.Errorf(
			"api-host authorizing adapter: strong session admission is required",
		)
	}
	if apiTokens == nil || !apiTokens.AuthorizingApiTokenAdmissionReady() {
		return apiHostAuthorizingSessionAdmitter{}, fmt.Errorf(
			"api-host authorizing adapter: strong API token admission is required",
		)
	}
	return apiHostAuthorizingSessionAdmitter{
		sessions:                 sessions,
		apiTokens:                apiTokens,
		expectedAudienceResolver: expectedAudienceResolver,
	}, nil
}

func (a apiHostAuthorizingSessionAdmitter) AdmitAuthorizingSession(
	ctx context.Context,
	request hostruntime.AuthorizingSessionRequest,
) (hostruntime.AuthorizingSessionAdmission, error) {
	if featureauth.LooksLikeApiToken(request.BearerToken) {
		if request.Mode == hostruntime.AuthorizingSessionControl ||
			request.ControlProof != "" {
			return hostruntime.AuthorizingSessionAdmission{},
				hostruntime.ErrAuthorizingCredentialRejected
		}
		validation, err := a.apiTokens.AdmitAuthorizingApiToken(
			ctx,
			request.BearerToken,
		)
		return apiHostAuthorizingSessionAdmission(
			validation,
			"",
			time.Time{},
			err,
		)
	}

	switch request.Mode {
	case hostruntime.AuthorizingSessionStrict:
		if request.ControlProof != "" {
			return hostruntime.AuthorizingSessionAdmission{},
				hostruntime.ErrAuthorizingCredentialRejected
		}
		var validation *featureauth.AuthorizingSessionValidation
		var err error
		if a.expectedAudienceResolver == nil {
			validation, err = a.sessions.AdmitAuthorizingSession(ctx, request.BearerToken)
		} else {
			expectedAudience, audienceErr := a.expectedAudienceResolver()
			if audienceErr != nil || strings.TrimSpace(expectedAudience) == "" {
				return hostruntime.AuthorizingSessionAdmission{}, fmt.Errorf("protected resource audience is unavailable")
			}
			validation, err = a.sessions.AdmitAuthorizingSessionForAudience(
				ctx,
				request.BearerToken,
				expectedAudience,
			)
		}
		return apiHostAuthorizingSessionAdmission(
			validation,
			"",
			time.Time{},
			err,
		)
	case hostruntime.AuthorizingSessionRenewal:
		if request.ControlProof != "" {
			return hostruntime.AuthorizingSessionAdmission{},
				hostruntime.ErrAuthorizingCredentialRejected
		}
		validation, replacement, err :=
			a.sessions.AdmitAuthorizingSessionWithRenewal(
				ctx,
				request.BearerToken,
			)
		return apiHostAuthorizingSessionAdmission(
			validation,
			replacement,
			time.Time{},
			err,
		)
	case hostruntime.AuthorizingSessionControl:
		validation, replacement, expiresAt, err :=
			a.sessions.AdmitAuthorizingSessionWithControl(
				ctx,
				request.BearerToken,
				request.ControlProof,
			)
		return apiHostAuthorizingSessionAdmission(
			validation,
			replacement,
			expiresAt,
			err,
		)
	case hostruntime.AuthorizingSessionBrowser:
		// The proof is the whole credential here, so a bearer alongside it is a caller
		// mixing lanes rather than a stronger request. api-host already refuses that
		// before admission; refusing it again costs nothing and means this adapter is
		// safe to call from anywhere, not only from behind that check.
		if request.BearerToken != "" || request.ControlProof == "" {
			return hostruntime.AuthorizingSessionAdmission{},
				hostruntime.ErrAuthorizingCredentialRejected
		}
		validation, expiresAt, err := a.sessions.AdmitAuthorizingBrowserSession(
			ctx,
			request.ControlProof,
		)
		// The replacement is empty by construction, not by omission: this lane may not
		// produce a bearer, and api-host fails the request if one appears.
		return apiHostAuthorizingSessionAdmission(
			validation,
			"",
			expiresAt,
			err,
		)
	default:
		return hostruntime.AuthorizingSessionAdmission{},
			hostruntime.ErrAuthorizingCredentialRejected
	}
}

func apiHostAuthorizingSessionAdmission(
	validation *featureauth.AuthorizingSessionValidation,
	replacement string,
	proofCookieExpiresAt time.Time,
	err error,
) (hostruntime.AuthorizingSessionAdmission, error) {
	if err != nil {
		var unauthorized *apperrors.UnauthorizedError
		if errors.As(err, &unauthorized) {
			return hostruntime.AuthorizingSessionAdmission{},
				hostruntime.ErrAuthorizingCredentialRejected
		}
		return hostruntime.AuthorizingSessionAdmission{}, err
	}
	if validation == nil || validation.Claims == nil {
		return hostruntime.AuthorizingSessionAdmission{}, fmt.Errorf(
			"api-host authorizing adapter: strong admission returned no claims",
		)
	}
	return hostruntime.AuthorizingSessionAdmission{
		Claims: &hostruntime.SessionClaims{
			SessionID: validation.Claims.RegisteredClaims.ID,
			User:      hostSessionFromAuth(validation.Claims.User),
		},
		Subject:              validation.Subject,
		PermissionFloor:      validation.PermissionFloor,
		RefreshedBearerToken: replacement,
		ProofCookieExpiresAt: proofCookieExpiresAt,
	}, nil
}

func (a apiHostAuthorizingSessionAdmitter) TouchSession(
	ctx context.Context,
	sessionID string,
) {
	if a.sessions != nil {
		a.sessions.TouchSession(ctx, sessionID)
	}
}

type apiHostSessionAuthenticator struct {
	svc       *featureauth.AuthService
	apiTokens *featureauth.ApiTokenService
}

var (
	_ hostruntime.SessionAuthenticator         = apiHostSessionAuthenticator{}
	_ hostruntime.SessionRenewingAuthenticator = apiHostSessionAuthenticator{}
	_ hostruntime.SessionControlAuthenticator  = apiHostSessionAuthenticator{}
)

func (a apiHostSessionAuthenticator) ValidateSession(ctx context.Context, bearerToken string) (*hostruntime.SessionClaims, error) {
	// Personal access tokens carry a distinct prefix and are validated against
	// the PAT store rather than verified as JWT sessions. Both paths return the
	// same claims shape, so the downstream authorization plane is identical.
	if featureauth.LooksLikeApiToken(bearerToken) {
		return a.validateApiToken(ctx, bearerToken)
	}
	if a.svc == nil {
		return nil, fmt.Errorf("api-host auth adapter: auth service is not configured")
	}
	claims, err := a.svc.ValidateSession(ctx, bearerToken)
	if err != nil {
		return nil, err
	}
	return &hostruntime.SessionClaims{
		SessionID: claims.RegisteredClaims.ID,
		User:      hostSessionFromAuth(claims.User),
	}, nil
}

func (a apiHostSessionAuthenticator) ValidateSessionWithRenewal(ctx context.Context, bearerToken string) (*hostruntime.SessionClaims, string, error) {
	// PATs never rotate per request, so the renewal path returns an empty
	// refreshed token and reuses the same validation as the non-renewal path.
	if featureauth.LooksLikeApiToken(bearerToken) {
		claims, err := a.validateApiToken(ctx, bearerToken)
		if err != nil {
			return nil, "", err
		}
		return claims, "", nil
	}
	if a.svc == nil {
		return nil, "", fmt.Errorf("api-host auth adapter: auth service is not configured")
	}
	claims, refreshedToken, err := a.svc.ValidateSessionWithRenewal(ctx, bearerToken)
	if err != nil {
		return nil, "", err
	}
	return &hostruntime.SessionClaims{
		SessionID: claims.RegisteredClaims.ID,
		User:      hostSessionFromAuth(claims.User),
	}, refreshedToken, nil
}

func (a apiHostSessionAuthenticator) ValidateSessionWithControl(ctx context.Context, bearerToken, controlProof string) (hostruntime.SessionControlValidation, error) {
	if a.svc == nil {
		return hostruntime.SessionControlValidation{}, fmt.Errorf("api-host auth adapter: auth service is not configured")
	}
	claims, refreshedToken, proofCookieExpiresAt, err := a.svc.ValidateSessionWithControl(ctx, bearerToken, controlProof)
	if err != nil {
		return hostruntime.SessionControlValidation{}, err
	}
	if claims == nil {
		return hostruntime.SessionControlValidation{}, fmt.Errorf("api-host auth adapter: auth service returned empty claims")
	}
	return hostruntime.SessionControlValidation{
		Claims: &hostruntime.SessionClaims{
			SessionID: claims.RegisteredClaims.ID,
			User:      hostSessionFromAuth(claims.User),
		},
		RefreshedBearerToken: refreshedToken,
		ProofCookieExpiresAt: proofCookieExpiresAt,
	}, nil
}

// validateApiToken authenticates a personal access token and projects it into
// the same SessionClaims shape a JWT session produces.
func (a apiHostSessionAuthenticator) validateApiToken(ctx context.Context, bearerToken string) (*hostruntime.SessionClaims, error) {
	if a.apiTokens == nil {
		return nil, fmt.Errorf("api-host auth adapter: api token service is not configured")
	}
	claims, err := a.apiTokens.Validate(ctx, bearerToken)
	if err != nil {
		return nil, err
	}
	return &hostruntime.SessionClaims{
		SessionID: claims.RegisteredClaims.ID,
		User:      hostSessionFromAuth(claims.User),
	}, nil
}

func (a apiHostSessionAuthenticator) TouchSession(ctx context.Context, sessionID string) {
	if a.svc != nil {
		a.svc.TouchSession(ctx, sessionID)
	}
}

func hostSessionFromAuth(user jwtauth.UserSession) hostruntime.UserSession {
	return hostruntime.UserSession{
		ID:                user.ID,
		Name:              user.Name,
		Email:             user.Email,
		ProfileID:         user.ProfileId,
		ProfileName:       user.ProfileName,
		RoleID:            user.RoleId,
		RoleName:          user.RoleName,
		ConnectedAppID:    user.ConnectedAppId,
		Scope:             user.Scope,
		GrantType:         user.GrantType,
		SessionClass:      user.SessionClass,
		SystemPermissions: user.SystemPermissions,
	}
}

type apiHostPermissionRequestContext struct{}

func (apiHostPermissionRequestContext) Install(ctx context.Context) context.Context {
	lineage := hostruntime.RequestLineageFromContext(ctx)
	if lineage.RequestID == "" {
		lineage.RequestID, _ = ctx.Value(constants.ContextKeyRequestID).(string)
	}
	if lineage.Traceparent == "" {
		lineage.Traceparent, _ = ctx.Value(constants.ContextKeyTraceparent).(string)
	}
	ctx = hostruntime.ContextWithRequestLineage(ctx, lineage)
	if lineage.RequestID != "" {
		ctx = context.WithValue(ctx, constants.ContextKeyRequestID, lineage.RequestID)
	}
	if lineage.Traceparent != "" {
		ctx = context.WithValue(ctx, constants.ContextKeyTraceparent, lineage.Traceparent)
	}
	diag := permissions.NewDiagnostics()
	ctx = permissions.WithDiagnostics(ctx, diag)
	ctx = permissions.WithAccessInputCache(ctx)
	return records.WithRecordReadCache(ctx)
}

func (apiHostPermissionRequestContext) Snapshot(ctx context.Context) hostruntime.PermissionDiagnosticsSnapshot {
	s := permissions.SnapshotDiagnostics(ctx)
	return hostruntime.PermissionDiagnosticsSnapshot{
		SystemCalls:             s.SystemCalls,
		SystemDuration:          s.SystemDuration,
		CustomCalls:             s.CustomCalls,
		CustomDuration:          s.CustomDuration,
		RecordTypeCalls:         s.RecordTypeCalls,
		RecordTypeRows:          s.RecordTypeRows,
		RecordTypeDur:           s.RecordTypeDur,
		ObjectDBFallbacks:       s.ObjectDBFallbacks,
		ObjectDBDuration:        s.ObjectDBDuration,
		ObjectCacheHits:         s.ObjectCacheHits,
		FieldDBFallbacks:        s.FieldDBFallbacks,
		FieldDBDuration:         s.FieldDBDuration,
		FieldCacheHits:          s.FieldCacheHits,
		ObjectBulkCalls:         s.ObjectBulkCalls,
		ObjectBulkRows:          s.ObjectBulkRows,
		ObjectBulkDuration:      s.ObjectBulkDuration,
		FieldBulkCalls:          s.FieldBulkCalls,
		FieldBulkRows:           s.FieldBulkRows,
		FieldBulkDuration:       s.FieldBulkDuration,
		FieldBulkRejected:       s.FieldBulkRejected,
		EffectiveAccessCalls:    s.EffectiveAccessCalls,
		EffectiveAccessFields:   s.EffectiveAccessFields,
		EffectiveAccessDuration: s.EffectiveAccessDuration,
	}
}

type apiHostFeatureGate struct{}

func (apiHostFeatureGate) Disabled(feature string) bool {
	return flags.IsDisabled(feature)
}

type apiHostWebSocketSessions struct {
	manager *streaming.SessionManager
}

func (s apiHostWebSocketSessions) CreateSession() string {
	return s.manager.CreateSession()
}

func (s apiHostWebSocketSessions) DestroySession(clientID string) {
	s.manager.DestroySession(clientID)
}

func (s apiHostWebSocketSessions) Subscribe(clientID, channel string) bool {
	return s.manager.Subscribe(clientID, channel)
}

func (s apiHostWebSocketSessions) Unsubscribe(clientID, channel string) bool {
	return s.manager.Unsubscribe(clientID, channel)
}

func (s apiHostWebSocketSessions) GetSubscribedChannels(clientID string) []string {
	return s.manager.GetSubscribedChannels(clientID)
}

// ValidateBrowserSession satisfies hostruntime.BrowserSessionAuthenticator, the
// cookie-only lane. It is the adapter's narrowest method by design: no bearer goes
// in, and hostruntime.BrowserSessionValidation has no field one could come back out
// through, so this cannot hand the page a readable credential.
//
// The durable expiry is reported as the proof cookie's expiry so the transport can
// reissue the same cookie through a renewed session. Nothing mounts the handler that
// calls this yet.
func (a apiHostSessionAuthenticator) ValidateBrowserSession(ctx context.Context, controlProof string) (hostruntime.BrowserSessionValidation, error) {
	if a.svc == nil {
		return hostruntime.BrowserSessionValidation{}, fmt.Errorf("api-host auth adapter: auth service is not configured")
	}
	validation, err := a.svc.ValidateBrowserSession(ctx, controlProof)
	if err != nil {
		return hostruntime.BrowserSessionValidation{}, err
	}
	if validation == nil {
		return hostruntime.BrowserSessionValidation{}, fmt.Errorf("api-host auth adapter: auth service returned no browser session")
	}
	return hostruntime.BrowserSessionValidation{
		Claims: &hostruntime.SessionClaims{
			SessionID: validation.SessionID,
			User:      hostSessionFromAuth(validation.User),
		},
		ProofCookieExpiresAt: validation.ExpiresAt,
	}, nil
}
