import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  HttpStatus,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
  Req,
  UnauthorizedException,
  UploadedFile,
  UploadedFiles,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import {
  FileFieldsInterceptor,
  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 { MinistryProgressReportsService } from '@/modules/ministry-progress-reports/ministry-progress-reports.service';
import { pdfUploadOptions } from '@/modules/upload/pdf-upload.options';

import { CreateProgressReportDeadlineDto } from './dto/create-progress-report-deadline.dto';
import { CreateProgressReportDto } from './dto/create-progress-report.dto';
import { CreateProgressReportMeetingDto } from './dto/create-progress-report-meeting.dto';
import { QueryProgressReportsDto } from './dto/query-progress-reports.dto';
import { UpdateProgressReportDto } from './dto/update-progress-report.dto';
import { ProgressReportsService } from './progress-reports.service';

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

// Permission names use this subject, for example: "read ProgressReport".
const PROGRESS_REPORT_SUBJECT = 'ProgressReport';
const PROGRESS_REPORT_MINISTRY_SUBJECT = 'ProgressReportMinistry';

// This matches the 20 MB limit used by the progress-report form.
const MAX_PDF_SIZE = 20 * 1024 * 1024;
const MAX_MEETING_PDF_SIZE = 10 * 1024 * 1024;

// Both attachment uploads and meeting documents now go through the shared
// upload module: files arrive in memory and the service stores them.
const attachmentUploadOptions = pdfUploadOptions(MAX_PDF_SIZE);
const meetingDocumentUploadOptions = pdfUploadOptions(MAX_MEETING_PDF_SIZE);

const progressReportFileFields = [
  { name: 'attachment', maxCount: 1 },
  { name: 'draftSemesterReport', maxCount: 1 },
  { name: 'finalSemesterReport', maxCount: 1 },
];

type UploadedProgressReportFiles = {
  attachment?: Express.Multer.File[];
  draftSemesterReport?: Express.Multer.File[];
  finalSemesterReport?: Express.Multer.File[];
};

@ApiTags('progress-reports')
@ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard, PoliciesGuard)
@Controller('progress-reports')
export class ProgressReportsController {
  constructor(
    private readonly progressReportsService: ProgressReportsService,
    private readonly ministryProgressReportsService: MinistryProgressReportsService,
  ) {}

  // Create a report for the currently logged-in user.
  @Post()
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Create, PROGRESS_REPORT_SUBJECT),
  )
  @UseInterceptors(
    FileFieldsInterceptor(progressReportFileFields, attachmentUploadOptions),
  )
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody({ type: CreateProgressReportDto })
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({ summary: 'Create a progress report' })
  @ApiResponse({ status: 201, description: 'Progress report created.' })
  create(
    @Req() request: AuthenticatedRequest,
    @Body() dto: CreateProgressReportDto,
    @UploadedFiles() files?: UploadedProgressReportFiles,
  ) {
    return this.progressReportsService.create(
      dto,
      this.getUserId(request),
      this.getFirstUploadedFiles(files),
    );
  }

  @Post(':progressReportId/meetings')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Create, PROGRESS_REPORT_SUBJECT),
  )
  @UseInterceptors(FileInterceptor('document', meetingDocumentUploadOptions))
  @ApiConsumes('multipart/form-data')
  @ApiBody({ type: CreateProgressReportMeetingDto })
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({ summary: 'Create a progress report meeting' })
  @ApiParam({ name: 'progressReportId', type: Number })
  @ApiResponse({ status: 201, description: 'Meeting created.' })
  @ApiResponse({ status: 400, description: 'Validation failed.' })
  @ApiResponse({ status: 404, description: 'Related record not found.' })
  @ApiResponse({ status: 409, description: 'That meeting slot is taken.' })
  createMeeting(
    @Param('progressReportId', ParseIntPipe) progressReportId: number,
    @Req() request: AuthenticatedRequest,
    @Body() dto: CreateProgressReportMeetingDto,
    @UploadedFile() document?: Express.Multer.File,
  ) {
    return this.progressReportsService.createMeeting(
      progressReportId,
      dto,
      this.getUserId(request),
      document,
    );
  }

  @Get(':progressReportId/meetings')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Read, PROGRESS_REPORT_SUBJECT),
  )
  @ApiOperation({ summary: 'Get both progress report meeting slots' })
  @ApiParam({ name: 'progressReportId', type: Number })
  @ApiResponse({
    status: 200,
    description: 'The first and second meeting, each null when not created.',
  })
  @ApiResponse({ status: 404, description: 'Progress report not found.' })
  findMeetings(
    @Param('progressReportId', ParseIntPipe) progressReportId: number,
  ) {
    return this.progressReportsService.findMeetings(progressReportId);
  }

  // Users with "read ProgressReport" permission can list all active reports.
  @Get()
  @CheckPolicies(
    (ability: AppAbility) =>
      ability.can(Action.Read, PROGRESS_REPORT_SUBJECT) ||
      ability.can(Action.Read, PROGRESS_REPORT_MINISTRY_SUBJECT),
  )
  @ApiOperation({ summary: 'List progress reports' })
  async findAll(
    @Query() query: QueryProgressReportsDto,
    @Req() request: AuthenticatedRequest,
  ) {
    const userId = this.getUserId(request);

    if (await this.ministryProgressReportsService.isPswgUser(userId)) {
      return this.ministryProgressReportsService.findAllForPswg(
        { page: query.page, limit: query.limit },
        userId,
      );
    }

    return this.progressReportsService.findAll(query, userId);
  }

  @Get('assignments/:assignmentId')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Read, PROGRESS_REPORT_MINISTRY_SUBJECT),
  )
  @ApiOperation({
    summary: 'Get one progress report shared with the current PSWG',
  })
  @ApiParam({ name: 'assignmentId', type: Number })
  findOneForPswg(
    @Param('assignmentId', ParseIntPipe) assignmentId: number,
    @Req() request: AuthenticatedRequest,
  ) {
    return this.ministryProgressReportsService.findOneForPswg(
      assignmentId,
      this.getUserId(request),
    );
  }

  @Post('assignments/:assignmentId/review')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, PROGRESS_REPORT_MINISTRY_SUBJECT),
  )
  @ApiOperation({
    summary: 'Mark a shared Ministry report as reviewed by PSWG',
  })
  @ApiParam({ name: 'assignmentId', type: Number })
  reviewForPswg(
    @Param('assignmentId', ParseIntPipe) assignmentId: number,
    @Req() request: AuthenticatedRequest,
  ) {
    return this.ministryProgressReportsService.reviewForPswg(
      assignmentId,
      this.getUserId(request),
    );
  }

  // ParseIntPipe rejects an invalid ID before it reaches the service.
  @Get(':id')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Read, PROGRESS_REPORT_SUBJECT),
  )
  @ApiOperation({ summary: 'Get one progress report' })
  @ApiParam({ name: 'id', type: Number })
  findOne(
    @Param('id', ParseIntPipe) id: number,
    @Req() request: AuthenticatedRequest,
  ) {
    return this.progressReportsService.findOne(id, this.getUserId(request));
  }

  // PATCH allows the caller to update only the fields they provide.
  @Patch(':id')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, PROGRESS_REPORT_SUBJECT),
  )
  @UseInterceptors(
    FileFieldsInterceptor(progressReportFileFields, attachmentUploadOptions),
  )
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody({ type: UpdateProgressReportDto })
  @ApiOperation({ summary: 'Update one progress report' })
  @ApiParam({ name: 'id', type: Number })
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: UpdateProgressReportDto,
    @UploadedFiles() files?: UploadedProgressReportFiles,
  ) {
    return this.progressReportsService.update(
      id,
      dto,
      this.getFirstUploadedFiles(files),
    );
  }

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

  @Post(':progressReportId/deadlines')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, PROGRESS_REPORT_SUBJECT),
  )
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({ summary: 'Create the second or final deadline' })
  @ApiParam({ name: 'progressReportId', type: Number })
  @ApiBody({ type: CreateProgressReportDeadlineDto })
  @ApiResponse({ status: 201, description: 'Deadline created.' })
  @ApiResponse({ status: 400, description: 'Slot or date is invalid.' })
  @ApiResponse({ status: 404, description: 'Progress report not found.' })
  @ApiResponse({ status: 409, description: 'That deadline slot is taken.' })
  createDeadline(
    @Param('progressReportId', ParseIntPipe) progressReportId: number,
    @Body() dto: CreateProgressReportDeadlineDto,
  ) {
    return this.progressReportsService.createDeadline(progressReportId, dto);
  }

  @Get(':progressReportId/deadlines')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Read, PROGRESS_REPORT_SUBJECT),
  )
  @ApiOperation({ summary: 'Get all three progress report deadline slots' })
  @ApiParam({ name: 'progressReportId', type: Number })
  @ApiResponse({
    status: 200,
    description: 'First, second and final deadline, each null when not set.',
  })
  @ApiResponse({ status: 404, description: 'Progress report not found.' })
  findDeadlines(
    @Param('progressReportId', ParseIntPipe) progressReportId: number,
  ) {
    return this.progressReportsService.findDeadlines(progressReportId);
  }

  // The service performs a soft delete, so the database row is preserved.
  @Delete(':id')
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Delete, PROGRESS_REPORT_SUBJECT),
  )
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: 'Soft delete one progress report' })
  @ApiParam({ name: 'id', type: Number })
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.progressReportsService.remove(id);
  }

  // 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;
  }

  // FileFieldsInterceptor returns arrays, but each field allows only one PDF.
  private getFirstUploadedFiles(files?: UploadedProgressReportFiles) {
    return {
      attachment: files?.attachment?.[0],
      draftSemesterReport: files?.draftSemesterReport?.[0],
      finalSemesterReport: files?.finalSemesterReport?.[0],
    };
  }
}
