import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  HttpStatus,
  Param,
  ParseIntPipe,
  Patch,
  Req,
  UnauthorizedException,
  UploadedFile,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import {
  ApiBearerAuth,
  ApiBody,
  ApiConsumes,
  ApiOperation,
  ApiParam,
  ApiResponse,
  ApiTags,
} from '@nestjs/swagger';
import type { Request } from 'express';

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 { UpdateProgressReportMeetingDto } from './dto/update-progress-report-meeting.dto';
import { ProgressReportsService } from './progress-reports.service';

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

// One meeting is its own resource, so it does not need the report ID in the
// URL. Permissions still use the "ProgressReport" subject.
const PROGRESS_REPORT_SUBJECT = 'ProgressReport';

const MAX_MEETING_PDF_SIZE = 10 * 1024 * 1024;
const meetingDocumentUploadOptions = pdfUploadOptions(MAX_MEETING_PDF_SIZE);

@ApiTags('progress-report-meetings')
@ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard, PoliciesGuard)
@Controller('progress-report-meetings')
export class ProgressReportMeetingsController {
  constructor(
    private readonly progressReportsService: ProgressReportsService,
  ) {}

  @Get(':meetingId')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Read, PROGRESS_REPORT_SUBJECT),
  )
  @ApiOperation({ summary: 'Get one progress report meeting' })
  @ApiParam({ name: 'meetingId', type: Number })
  @ApiResponse({ status: 200, description: 'Meeting detail.' })
  @ApiResponse({ status: 404, description: 'Meeting not found.' })
  findOne(@Param('meetingId', ParseIntPipe) meetingId: number) {
    return this.progressReportsService.findMeeting(meetingId);
  }

  @Patch(':meetingId')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, PROGRESS_REPORT_SUBJECT),
  )
  @UseInterceptors(FileInterceptor('document', meetingDocumentUploadOptions))
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody({ type: UpdateProgressReportMeetingDto })
  @ApiOperation({ summary: 'Save one progress report meeting draft' })
  @ApiParam({ name: 'meetingId', type: Number })
  @ApiResponse({ status: 200, description: 'Meeting updated.' })
  @ApiResponse({ status: 400, description: 'Validation failed.' })
  @ApiResponse({ status: 404, description: 'Meeting not found.' })
  @ApiResponse({
    status: 409,
    description: 'The meeting is no longer a draft.',
  })
  update(
    @Param('meetingId', ParseIntPipe) meetingId: number,
    @Body() dto: UpdateProgressReportMeetingDto,
    @UploadedFile() document?: Express.Multer.File,
  ) {
    return this.progressReportsService.updateMeeting(meetingId, dto, document);
  }

  // Sending takes no body: the status change is decided by the server.
  @Patch(':meetingId/send')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, PROGRESS_REPORT_SUBJECT),
  )
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: 'Send one progress report meeting' })
  @ApiParam({ name: 'meetingId', type: Number })
  @ApiResponse({ status: 200, description: 'Meeting sent.' })
  @ApiResponse({ status: 400, description: 'The meeting is incomplete.' })
  @ApiResponse({ status: 404, description: 'Meeting not found.' })
  @ApiResponse({ status: 409, description: 'The meeting was already sent.' })
  send(
    @Param('meetingId', ParseIntPipe) meetingId: number,
    @Req() request: AuthenticatedRequest,
  ) {
    return this.progressReportsService.sendMeeting(
      meetingId,
      this.getUserId(request),
    );
  }

  @Delete(':meetingId')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Delete, PROGRESS_REPORT_SUBJECT),
  )
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: 'Soft delete one progress report meeting' })
  @ApiParam({ name: 'meetingId', type: Number })
  @ApiResponse({ status: 200, description: 'Meeting deleted.' })
  @ApiResponse({ status: 404, description: 'Meeting not found.' })
  @ApiResponse({
    status: 409,
    description: 'The meeting is no longer a draft.',
  })
  remove(@Param('meetingId', ParseIntPipe) meetingId: number) {
    return this.progressReportsService.removeMeeting(meetingId);
  }

  // Read and validate the user ID that JwtAuthGuard added to the request.
  private getUserId(request: AuthenticatedRequest): number {
    const userId = Number(request.user?.id);

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

    return userId;
  }
}
