import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  HttpStatus,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
  Req,
  Res,
  StreamableFile,
  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, Response } 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 { CreateMeetingSummaryDto } from './dto/create-meeting-summary.dto';
import { ListMeetingSummariesDto } from './dto/list-meeting-summaries.dto';
import { UpdateMeetingSummaryDto } from './dto/update-meeting-summary.dto';
import { ChangeMeetingSummaryStatusDto } from './dto/change-meeting-summary-status.dto';
import { UploadIssueResolveDto } from './dto/upload-issue-resolve.dto';
import { MeetingSummaryService } from './meeting-summary.service';

// CASL subject used by every route in this controller.
const MEETING_SUMMARY_SUBJECT = 'MeetingSummary';

// Upload limit for a summary or issue reference PDF: 10 MB.
const PDF_MAX_BYTES = 10 * 1024 * 1024;
const documentUploadOptions = pdfUploadOptions(PDF_MAX_BYTES);

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

@ApiTags('meeting-summaries')
@ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard, PoliciesGuard)
@Controller('meeting-summaries')
export class MeetingSummaryController {
  constructor(private readonly service: MeetingSummaryService) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Create, MEETING_SUMMARY_SUBJECT),
  )
  @UseInterceptors(FileInterceptor('documentReference', documentUploadOptions))
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody({ type: CreateMeetingSummaryDto })
  @ApiOperation({ summary: 'Create (save a draft of) a meeting summary' })
  @ApiResponse({ status: 201, description: 'Meeting summary saved.' })
  @ApiResponse({ status: 400, description: 'Validation failed.' })
  @ApiResponse({
    status: 409,
    description: 'This meeting already has an active meeting summary.',
  })
  @ApiResponse({ status: 403, description: 'Insufficient permissions.' })
  create(
    @Req() request: AuthenticatedRequest,
    @Body() dto: CreateMeetingSummaryDto,
    @UploadedFile() file?: Express.Multer.File,
  ) {
    return this.service.create(dto, this.getCurrentUserId(request), file);
  }

  @Get()
  @HttpCode(HttpStatus.OK)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Read, MEETING_SUMMARY_SUBJECT),
  )
  @ApiOperation({ summary: 'List meeting summaries' })
  @ApiResponse({ status: 200, description: 'Paginated list of summaries.' })
  findAll(
    @Req() request: AuthenticatedRequest,
    @Query() query: ListMeetingSummariesDto,
  ) {
    return this.service.findAll(query, this.getCurrentUserId(request));
  }

  // Declared before ":id" so Express matches this literal path first.
  @Get('export')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Export, MEETING_SUMMARY_SUBJECT),
  )
  @ApiOperation({ summary: 'Export meeting summaries as Excel' })
  @ApiResponse({
    status: 200,
    description: 'Meeting summaries Excel file download.',
  })
  async exportSummaries(
    @Req() request: AuthenticatedRequest,
    @Res({ passthrough: true }) res: Response,
  ): Promise<StreamableFile> {
    const { buffer, filename } = await this.service.exportSummaries(
      this.getCurrentUserId(request),
    );

    res.set({
      'Content-Type':
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      'Content-Disposition': `attachment; filename="${filename}"`,
    });

    return new StreamableFile(buffer);
  }

  // Declared before ":id" so Express matches this literal path first.
  @Get('agencies')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Read, MEETING_SUMMARY_SUBJECT),
  )
  @ApiOperation({ summary: 'List government agencies for issue resolutions' })
  @ApiResponse({ status: 200, description: 'Government agencies.' })
  listAgencies() {
    return this.service.listAgencies();
  }

  @Get(':id')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Read, MEETING_SUMMARY_SUBJECT),
  )
  @ApiParam({ name: 'id', type: Number, description: 'Meeting summary ID.' })
  @ApiOperation({ summary: 'Get one meeting summary with merged issues' })
  @ApiResponse({ status: 200, description: 'Meeting summary detail.' })
  @ApiResponse({ status: 404, description: 'Meeting summary not found.' })
  findOne(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
  ) {
    return this.service.findOne(id, this.getCurrentUserId(request));
  }

  @Patch(':id/share')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, MEETING_SUMMARY_SUBJECT),
  )
  @ApiParam({ name: 'id', type: Number, description: 'Meeting summary ID.' })
  @ApiOperation({
    summary: 'Share a meeting summary with its PSWG working group',
  })
  @ApiResponse({
    status: 200,
    description: 'Meeting summary shared with PSWG.',
  })
  share(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
  ) {
    return this.service.share(id, this.getCurrentUserId(request));
  }

  @Patch(':id/unshare')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, MEETING_SUMMARY_SUBJECT),
  )
  @ApiParam({ name: 'id', type: Number, description: 'Meeting summary ID.' })
  @ApiOperation({
    summary: 'Unshare a meeting summary from its PSWG working group',
  })
  @ApiResponse({
    status: 200,
    description: 'Meeting summary unshared from PSWG.',
  })
  unshare(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
  ) {
    return this.service.unshare(id, this.getCurrentUserId(request));
  }

  @Patch(':id/status')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, MEETING_SUMMARY_SUBJECT),
  )
  @ApiParam({ name: 'id', type: Number, description: 'Meeting summary ID.' })
  @ApiBody({ type: ChangeMeetingSummaryStatusDto })
  @ApiOperation({ summary: 'Change a meeting summary status' })
  @ApiResponse({ status: 200, description: 'Meeting summary status updated.' })
  @ApiResponse({ status: 404, description: 'Meeting summary not found.' })
  changeStatus(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: ChangeMeetingSummaryStatusDto,
  ) {
    return this.service.changeStatus(
      id,
      dto.status,
      this.getCurrentUserId(request),
    );
  }

  @Patch(':id/issues/:issueId')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, MEETING_SUMMARY_SUBJECT),
  )
  @UseInterceptors(FileInterceptor('documentReference', documentUploadOptions))
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody({ type: UploadIssueResolveDto })
  @ApiParam({ name: 'id', type: Number, description: 'Meeting summary ID.' })
  @ApiParam({ name: 'issueId', type: Number, description: 'Issue ID.' })
  @ApiOperation({ summary: 'Create or update one issue resolution' })
  @ApiResponse({ status: 200, description: 'Issue resolution saved.' })
  upsertIssue(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
    @Param('issueId', ParseIntPipe) issueId: number,
    @Body() dto: UploadIssueResolveDto,
    @UploadedFile() file?: Express.Multer.File,
  ) {
    return this.service.upsertIssue(
      id,
      issueId,
      dto,
      this.getCurrentUserId(request),
      file,
    );
  }

  @Patch(':id')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Update, MEETING_SUMMARY_SUBJECT),
  )
  @UseInterceptors(FileInterceptor('documentReference', documentUploadOptions))
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody({ type: UpdateMeetingSummaryDto })
  @ApiParam({ name: 'id', type: Number, description: 'Meeting summary ID.' })
  @ApiOperation({ summary: 'Update a meeting summary' })
  @ApiResponse({ status: 200, description: 'Meeting summary updated.' })
  update(
    @Req() request: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: UpdateMeetingSummaryDto,
    @UploadedFile() file?: Express.Multer.File,
  ) {
    return this.service.update(id, dto, this.getCurrentUserId(request), file);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies((ability: AppAbility) =>
    ability.can(Action.Delete, MEETING_SUMMARY_SUBJECT),
  )
  @ApiParam({ name: 'id', type: Number, description: 'Meeting summary ID.' })
  @ApiOperation({ summary: 'Soft delete a meeting summary' })
  @ApiResponse({ status: 200, description: 'Meeting summary deleted.' })
  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;
  }
}
