import {
  BadRequestException,
  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 { existsSync, mkdirSync } from 'fs';
import { diskStorage } from 'multer';
import { extname } from 'path';

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 { 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';

// Where uploaded PDFs (summary + per-issue references) are stored.
const MEETING_SUMMARY_UPLOAD_DIR = './uploads/meeting-summaries';

// Upload limit for a summary or issue reference PDF: 5 MB.
const PDF_MAX_BYTES = 5 * 1024 * 1024;

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

// Store uploaded PDFs on disk under the meeting-summaries folder. Mirrors the
// helper in meeting-requests.controller.ts but writes to its own directory.
function pdfStorage() {
  return diskStorage({
    destination: (_req, _file, callback) => {
      if (!existsSync(MEETING_SUMMARY_UPLOAD_DIR)) {
        mkdirSync(MEETING_SUMMARY_UPLOAD_DIR, { recursive: true });
      }

      callback(null, MEETING_SUMMARY_UPLOAD_DIR);
    },
    filename: (_req, file, callback) => {
      const fileExtension = extname(file.originalname).toLowerCase();

      const safeOriginalName = file.originalname
        .replace(fileExtension, '')
        .replace(/[^a-zA-Z0-9-_]/g, '_');

      callback(null, `${Date.now()}-${safeOriginalName}${fileExtension}`);
    },
  });
}

function pdfFileFilter(
  _req: unknown,
  file: Express.Multer.File,
  callback: (error: Error | null, acceptFile: boolean) => void,
) {
  const isPdf =
    file.mimetype === 'application/pdf' ||
    extname(file.originalname).toLowerCase() === '.pdf';

  if (!isPdf) {
    return callback(
      new BadRequestException('Only PDF files are allowed'),
      false,
    );
  }

  return callback(null, true);
}

const documentUploadOptions = {
  storage: pdfStorage(),
  fileFilter: pdfFileFilter,
  limits: { fileSize: PDF_MAX_BYTES },
};

@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(@Param('id', ParseIntPipe) id: number) {
    return this.service.unshare(id);
  }

  @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(@Param('id', ParseIntPipe) id: number) {
    return this.service.remove(id);
  }

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