/**
 * FederationScopeService — M3 server-side scope enforcement pipeline.
 *
 * Pure trust-boundary service: it validates the grant scope, asks an injected
 * native RBAC evaluator what the subject user can read locally, intersects that
 * answer with the federation scope filters, and returns a query filter for the
 * verb controllers. The service performs no DB calls directly.
 */

import { Injectable } from '@nestjs/common';
import {
  FEDERATION_RESOURCE_VALUES,
  type FederationResource,
  FederationScopeError,
  parseFederationScope,
} from '../scope-schema.js';
import type { FederationContext } from './federation-context.js';

const federationResourceSet: ReadonlySet<string> = new Set<string>(FEDERATION_RESOURCE_VALUES);

export type FederationScopeDenyStage =
  | 'scope_parse'
  | 'resource_allowlist'
  | 'resource_exclusion'
  | 'native_rbac'
  | 'row_cap';

export type FederationScopeDenyCode =
  | 'invalid_scope'
  | 'invalid_resource'
  | 'resource_not_granted'
  | 'resource_excluded'
  | 'native_rbac_denied'
  | 'invalid_limit';

export type FederationScopeDenyStatus = 400 | 403;

export interface FederationScopeDenyDetails {
  readonly [key: string]: string | number | boolean | readonly string[];
}

export interface FederationScopeDenyReason {
  readonly code: FederationScopeDenyCode;
  readonly stage: FederationScopeDenyStage;
  readonly statusCode: FederationScopeDenyStatus;
  readonly message: string;
  readonly grantId: string;
  readonly peerId: string;
  readonly subjectUserId: string;
  readonly resource: string;
  readonly details?: FederationScopeDenyDetails;
}

export interface FederationNativeRbacRequest {
  readonly grantId: string;
  readonly peerId: string;
  readonly subjectUserId: string;
  readonly resource: FederationResource;
}

export interface FederationNativeRbacAccess {
  /** Whether this user may read personal rows for this resource. */
  readonly includePersonal: boolean;

  /** Team IDs this user may read for this resource under native RBAC. */
  readonly teamIds: readonly string[];
}

export interface FederationNativeRbacAllowedResult {
  readonly allowed: true;
  readonly access: FederationNativeRbacAccess;
}

export interface FederationNativeRbacDeniedResult {
  readonly allowed: false;
  readonly reason?: string;
  readonly details?: FederationScopeDenyDetails;
}

export type FederationNativeRbacResult =
  | FederationNativeRbacAllowedResult
  | FederationNativeRbacDeniedResult;

export interface FederationNativeRbacEvaluator {
  evaluateReadAccess(request: FederationNativeRbacRequest): Promise<FederationNativeRbacResult>;
}

export interface FederationScopeEvaluationInput {
  readonly context: FederationContext;
  readonly resource: string;
  readonly requestedLimit?: number;
  readonly nativeRbac: FederationNativeRbacEvaluator;
}

export interface FederationScopeQueryFilter {
  readonly resource: FederationResource;
  readonly subjectUserId: string;
  readonly includePersonal: boolean;
  readonly teamIds: readonly string[];
  readonly limit: number;
  readonly maxRowsPerQuery: number;
}

export interface FederationScopeAllowedResult {
  readonly allowed: true;
  readonly filter: FederationScopeQueryFilter;
}

export interface FederationScopeDeniedResult {
  readonly allowed: false;
  readonly deny: FederationScopeDenyReason;
}

export type FederationScopeEvaluationResult =
  | FederationScopeAllowedResult
  | FederationScopeDeniedResult;

function isFederationResource(resource: string): resource is FederationResource {
  return federationResourceSet.has(resource);
}

function uniqueStrings(values: readonly string[]): readonly string[] {
  return Array.from(new Set<string>(values));
}

function intersectTeamIds(
  nativeTeamIds: readonly string[],
  scopedTeamIds: readonly string[] | undefined,
): readonly string[] {
  const uniqueNativeTeamIds = uniqueStrings(nativeTeamIds);

  if (scopedTeamIds === undefined) {
    return uniqueNativeTeamIds;
  }

  const nativeSet = new Set<string>(uniqueNativeTeamIds);
  return uniqueStrings(scopedTeamIds).filter((teamId: string): boolean => nativeSet.has(teamId));
}

function makeDenyReason(params: {
  readonly code: FederationScopeDenyCode;
  readonly stage: FederationScopeDenyStage;
  readonly statusCode?: FederationScopeDenyStatus;
  readonly message: string;
  readonly context: FederationContext;
  readonly resource: string;
  readonly details?: FederationScopeDenyDetails;
}): FederationScopeDeniedResult {
  return {
    allowed: false,
    deny: {
      code: params.code,
      stage: params.stage,
      statusCode: params.statusCode ?? 403,
      message: params.message,
      grantId: params.context.grantId,
      peerId: params.context.peerId,
      subjectUserId: params.context.subjectUserId,
      resource: params.resource,
      ...(params.details !== undefined ? { details: params.details } : {}),
    },
  };
}

@Injectable()
export class FederationScopeService {
  async evaluateAccess(
    input: FederationScopeEvaluationInput,
  ): Promise<FederationScopeEvaluationResult> {
    const { context, resource, requestedLimit, nativeRbac } = input;

    let scope: ReturnType<typeof parseFederationScope>;
    try {
      scope = parseFederationScope(context.scope);
    } catch (error: unknown) {
      const message =
        error instanceof FederationScopeError
          ? 'Federation grant scope is invalid'
          : 'Federation grant scope could not be parsed';
      const details = error instanceof Error ? { reason: error.message } : undefined;
      return makeDenyReason({
        code: 'invalid_scope',
        stage: 'scope_parse',
        statusCode: 400,
        message,
        context,
        resource,
        ...(details !== undefined ? { details } : {}),
      });
    }

    if (!isFederationResource(resource)) {
      return makeDenyReason({
        code: 'invalid_resource',
        stage: 'resource_allowlist',
        message: 'Requested federation resource is not supported',
        context,
        resource,
        details: { supportedResources: FEDERATION_RESOURCE_VALUES },
      });
    }

    if (scope.excluded_resources.includes(resource)) {
      return makeDenyReason({
        code: 'resource_excluded',
        stage: 'resource_exclusion',
        message: 'Requested federation resource is explicitly excluded by grant scope',
        context,
        resource,
      });
    }

    if (!scope.resources.includes(resource)) {
      return makeDenyReason({
        code: 'resource_not_granted',
        stage: 'resource_allowlist',
        message: 'Requested federation resource is not granted by scope',
        context,
        resource,
        details: { grantedResources: scope.resources },
      });
    }

    if (requestedLimit !== undefined && (!Number.isInteger(requestedLimit) || requestedLimit < 1)) {
      return makeDenyReason({
        code: 'invalid_limit',
        stage: 'row_cap',
        statusCode: 400,
        message: 'Requested row limit must be a positive integer',
        context,
        resource,
        details: { requestedLimit },
      });
    }

    const nativeResult = await nativeRbac.evaluateReadAccess({
      grantId: context.grantId,
      peerId: context.peerId,
      subjectUserId: context.subjectUserId,
      resource,
    });

    if (!nativeResult.allowed) {
      return makeDenyReason({
        code: 'native_rbac_denied',
        stage: 'native_rbac',
        message: nativeResult.reason ?? 'Subject user is not allowed to read this resource',
        context,
        resource,
        ...(nativeResult.details !== undefined ? { details: nativeResult.details } : {}),
      });
    }

    const scopeFilter = scope.filters?.[resource];
    const includePersonal =
      Boolean(scopeFilter?.include_personal ?? true) && nativeResult.access.includePersonal;
    const teamIds = intersectTeamIds(nativeResult.access.teamIds, scopeFilter?.include_teams);
    const limit = Math.min(requestedLimit ?? scope.max_rows_per_query, scope.max_rows_per_query);

    return {
      allowed: true,
      filter: {
        resource,
        subjectUserId: context.subjectUserId,
        includePersonal,
        teamIds,
        limit,
        maxRowsPerQuery: scope.max_rows_per_query,
      },
    };
  }
}
