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

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

const MEETING_REQUEST_UPLOAD_DIR = './uploads/meeting-requests';

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

function pdfStorage() {
  return diskStorage({
    destination: (_req, _file, callback) => {
      if (!existsSync(MEETING_REQUEST_UPLOAD_DIR)) {
        mkdirSync(MEETING_REQUEST_UPLOAD_DIR, {
          recursive: true,
        });
      }

      callback(null, MEETING_REQUEST_UPLOAD_DIR);
    },

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

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

      const fileName = `${Date.now()}-${safeOriginalName}${fileExtension}`;

      callback(null, fileName);
    },
  });
}

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

@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', {
      storage: pdfStorage(),
      fileFilter: pdfFileFilter,
      limits: {
        fileSize: 10 * 1024 * 1024,
      },
    }),
  )
  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', {
      storage: pdfStorage(),
      fileFilter: pdfFileFilter,
      limits: {
        fileSize: 10 * 1024 * 1024,
      },
    }),
  )
  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);
  }

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