import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
  Req,
  UnauthorizedException,
  UploadedFile,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import type { Request } from 'express';
import { FileInterceptor } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';

import { Action } from '@/casl/action.enum';
import { AppAbility } from '@/casl/casl-ability.factory';
import { CheckPolicies } from '@/casl/check-policies.decorator';
import { PoliciesGuard } from '@/casl/policies.guard';
import { JwtAuthGuard } from '@/modules/auth/jwt-auth.guard';
import { pdfUploadOptions } from '@/modules/upload/pdf-upload.options';

import { AddMeetingRequestIssueDto } from './dto/add-meeting-request-issue.dto';
import { CreateMeetingRequestDto } from './dto/create-meeting-request.dto';
import { ListMeetingRequestsDto } from './dto/list-meeting-requests.dto';
import { UpdateMeetingRequestDto } from './dto/update-meeting-request.dto';
import { MeetingRequestsService } from './meeting-requests.service';

// CASL subject for meeting-request permissions ("<action> MeetingRequest").
const MEETING_REQUEST_SUBJECT = 'MeetingRequest';
// Adding an issue creates a working-group issue, so it uses that subject.
const WORKING_GROUP_ISSUE_SUBJECT = 'WorkingGroupIssue';
const MEETING_REQUEST_PDF_MAX_BYTES = 10 * 1024 * 1024;
const meetingRequestUploadOptions = pdfUploadOptions(
  MEETING_REQUEST_PDF_MAX_BYTES,
);

// Same limit and in-memory handling the other issue create routes use.
const ISSUE_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
const issueAttachmentUploadOptions = {
  storage: memoryStorage(),
  limits: { fileSize: ISSUE_ATTACHMENT_MAX_BYTES },
};

type AuthenticatedRequest = Request & {
  user?: {
    id?: number;
  };
};

@UseGuards(JwtAuthGuard, PoliciesGuard)
@Controller('meeting-requests')
export class MeetingRequestsController {
  constructor(private readonly service: MeetingRequestsService) {}

  @Post()
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Create, MEETING_REQUEST_SUBJECT),
  )
  @UseInterceptors(
    FileInterceptor('meetingRequestLetter', meetingRequestUploadOptions),
  )
  create(
    @Req() request: AuthenticatedRequest,
    @Body() dto: CreateMeetingRequestDto,
    @UploadedFile() file?: Express.Multer.File,
  ) {
    return this.service.create(dto, this.getCurrentUserId(request), file);
  }

  @Get()
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Read, MEETING_REQUEST_SUBJECT),
  )
  findAll(
    @Req() request: AuthenticatedRequest,
    @Query() query: ListMeetingRequestsDto,
  ) {
    return this.service.findAll(query, this.getCurrentUserId(request));
  }

  @Get(':id')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Read, MEETING_REQUEST_SUBJECT),
  )
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.service.findOne(id);
  }

  @Patch(':id/send-request')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, MEETING_REQUEST_SUBJECT),
  )
  sendRequest(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
  ) {
    return this.service.sendRequest(id, this.getCurrentUserId(request));
  }

  @Patch(':id/start-review')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, MEETING_REQUEST_SUBJECT),
  )
  startReview(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
  ) {
    return this.service.startReview(id, this.getCurrentUserId(request));
  }

  @Patch(':id')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, MEETING_REQUEST_SUBJECT),
  )
  @UseInterceptors(
    FileInterceptor('meetingRequestLetter', meetingRequestUploadOptions),
  )
  update(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: UpdateMeetingRequestDto,
    @UploadedFile() file?: Express.Multer.File,
  ) {
    return this.service.update(id, dto, this.getCurrentUserId(request), file);
  }

  // A Ministry adds one more issue to a meeting request addressed to it. The
  // permission subject is "WorkingGroupIssue" because an issue is created, and
  // the Ministry role already has "create WorkingGroupIssue".
  @Post(':id/issues')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Create, WORKING_GROUP_ISSUE_SUBJECT),
  )
  @UseInterceptors(
    FileInterceptor('attachmentFile', issueAttachmentUploadOptions),
  )
  addIssue(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: AddMeetingRequestIssueDto,
    @UploadedFile() attachmentFile?: Express.Multer.File,
  ) {
    return this.service.addMinistryIssue(
      id,
      this.getCurrentUserId(request),
      dto,
      attachmentFile,
    );
  }

  @Delete(':id')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Delete, MEETING_REQUEST_SUBJECT),
  )
  remove(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
  ) {
    return this.service.remove(id, this.getCurrentUserId(request));
  }

  private getCurrentUserId(request: AuthenticatedRequest): number {
    const userId = Number(request.user?.id);

    if (!Number.isInteger(userId) || userId <= 0) {
      throw new UnauthorizedException('Invalid authenticated user.');
    }

    return userId;
  }
}
