/**
 * Federation list verb (FED-M3-05).
 *
 * POST /api/federation/v1/list/:resource
 *
 * Pipeline: FederationAuthGuard attaches the active grant context, then
 * FederationScopeService enforces grant scope + native RBAC intersection, then
 * the read-only query layer returns capped rows tagged with `_source`. Read
 * audit-log writes are deferred to M4; this controller does not persist request
 * or response bodies.
 */

import {
  Body,
  Controller,
  HttpException,
  Inject,
  Param,
  Post,
  Req,
  UseGuards,
} from '@nestjs/common';
import type { FastifyRequest } from 'fastify';
import {
  FederationInvalidRequestError,
  FederationScopeViolationError,
  FederationUnauthorizedError,
  SOURCE_LOCAL,
  tagWithSource,
  type FederationListResponse,
  type SourceTag,
} from '@mosaicstack/types';
import { FederationAuthGuard } from '../federation-auth.guard.js';
import '../federation-context.js';
import { FederationScopeService } from '../scope.service.js';
import { FederationListQueryService } from './list-query.service.js';

interface FederationListRequestBody {
  readonly limit?: unknown;
  readonly cursor?: unknown;
}

type FederatedRow = Record<string, unknown> & SourceTag;

function parseLimit(body: FederationListRequestBody | undefined): number | undefined {
  if (body?.limit === undefined) {
    return undefined;
  }

  const parsed =
    typeof body.limit === 'number'
      ? body.limit
      : typeof body.limit === 'string' && body.limit.trim().length > 0
        ? Number(body.limit)
        : Number.NaN;

  if (!Number.isSafeInteger(parsed) || parsed < 1) {
    throw new HttpException(
      new FederationInvalidRequestError(
        'Federation list limit must be a positive integer',
      ).toEnvelope(),
      400,
    );
  }

  return parsed;
}

function parseCursor(body: FederationListRequestBody | undefined): string | undefined {
  if (body?.cursor === undefined) {
    return undefined;
  }
  if (typeof body.cursor === 'string') {
    return body.cursor;
  }
  throw new HttpException(
    new FederationInvalidRequestError('Federation list cursor must be a string').toEnvelope(),
    400,
  );
}

@Controller('api/federation/v1/list')
@UseGuards(FederationAuthGuard)
export class ListController {
  constructor(
    @Inject(FederationScopeService) private readonly scope: FederationScopeService,
    @Inject(FederationListQueryService) private readonly query: FederationListQueryService,
  ) {}

  @Post(':resource')
  async list(
    @Param('resource') resource: string,
    @Req() request: FastifyRequest,
    @Body() body?: FederationListRequestBody,
  ): Promise<FederationListResponse<FederatedRow>> {
    if (!request.federationContext) {
      throw new HttpException(
        new FederationUnauthorizedError('Federation context missing').toEnvelope(),
        401,
      );
    }

    const requestedLimit = parseLimit(body);
    const cursor = parseCursor(body);
    const scopeResult = await this.scope.evaluateAccess({
      context: request.federationContext,
      resource,
      requestedLimit,
      nativeRbac: this.query,
    });

    if (!scopeResult.allowed) {
      const ErrorClass =
        scopeResult.deny.statusCode === 400
          ? FederationInvalidRequestError
          : FederationScopeViolationError;
      throw new HttpException(
        new ErrorClass(scopeResult.deny.message, scopeResult.deny).toEnvelope(),
        scopeResult.deny.statusCode,
      );
    }

    let result: Awaited<ReturnType<FederationListQueryService['list']>>;
    try {
      result = await this.query.list({ filter: scopeResult.filter, cursor });
    } catch (error: unknown) {
      if (error instanceof Error && error.message === 'Invalid federation list cursor') {
        throw new HttpException(
          new FederationInvalidRequestError('Federation list cursor is invalid').toEnvelope(),
          400,
        );
      }
      throw error;
    }

    const response: FederationListResponse<FederatedRow> = {
      items: tagWithSource(result.items, SOURCE_LOCAL),
    };
    if (result.nextCursor !== undefined) {
      response.nextCursor = result.nextCursor;
    }
    if (result.truncated) {
      response._truncated = true;
    }
    return response;
  }
}
