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

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 { WorkingGroupIssuesService } from '@/modules/working-group-issues/working-group-issues.service';
import { CreateIssueDto } from './dto/create-issue.dto';
import { QueryIssuesDto } from './dto/query-issues.dto';
import { UpdateIssueDto } from './dto/update-issue.dto';
import {
  IssuesService,
  type IssueMatrixSummary,
  type PaginatedIssueMatrix,
} from './issues.service';

const CDC_SUBJECT = 'CdcIssueMatrix';
const CEFP_SUBJECT = 'CefpIssueMatrix';

// CDC and CEFP share this module, so a caller needs the permission of either
// one. Which matrix they actually get is decided from their role.
const canOnEitherMatrix = (action: Action) => (ability: AppAbility) =>
  ability.can(action, CDC_SUBJECT) || ability.can(action, CEFP_SUBJECT);
const ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
const attachmentUploadOptions = {
  storage: memoryStorage(),
  limits: { fileSize: ATTACHMENT_MAX_BYTES },
};

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

@ApiTags('issues')
@ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard, PoliciesGuard)
@Controller('issues')
export class IssuesController {
  constructor(
    private readonly issuesService: IssuesService,
    private readonly workingGroupIssuesService: WorkingGroupIssuesService,
  ) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @CheckPolicies(canOnEitherMatrix(Action.Create))
  @UseInterceptors(FileInterceptor('attachmentFile', attachmentUploadOptions))
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody({ type: CreateIssueDto })
  @ApiOperation({ summary: 'Create an issue for a selected working group' })
  createIssue(
    @Req() request: AuthenticatedRequest,
    @Body() dto: CreateIssueDto,
    @UploadedFile() attachmentFile?: Express.Multer.File,
  ) {
    return this.workingGroupIssuesService.createForWorkingGroup(
      this.getAuthenticatedUserId(request),
      dto.workingGroupId,
      dto,
      attachmentFile,
    );
  }

  @Delete(':issueId')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies(canOnEitherMatrix(Action.Delete))
  async deleteIssue(
    @Param('issueId', ParseIntPipe) issueId: number,
    @Req() request: AuthenticatedRequest,
  ) {
    const target = await this.resolveTarget(request);

    return this.workingGroupIssuesService.removeIssueCreatedByRole(
      issueId,
      target.roleName,
    );
  }

  @Get()
  @HttpCode(HttpStatus.OK)
  @CheckPolicies(canOnEitherMatrix(Action.Read))
  @ApiOperation({
    summary: 'List CDC-created and ministry-submitted issues',
  })
  @ApiResponse({
    status: 200,
    description: 'Paginated CDC issue matrix',
  })
  async findAll(
    @Query() query: QueryIssuesDto,
    @Req() request: AuthenticatedRequest,
  ): Promise<PaginatedIssueMatrix> {
    return this.issuesService.findAll(query, await this.resolveTarget(request));
  }

  @Get('summary')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies(canOnEitherMatrix(Action.Read))
  @ApiOperation({
    summary: 'Get CDC issue matrix summary counts',
  })
  @ApiResponse({
    status: 200,
    description: 'CDC issue matrix summary',
  })
  async getSummary(
    @Req() request: AuthenticatedRequest,
  ): Promise<IssueMatrixSummary> {
    return this.issuesService.getSummary(await this.resolveTarget(request));
  }

  /**
   * Change the ministries and/or the status of one row, straight from the table.
   * Declared after the more specific routes above so they keep matching first.
   */
  @Get(':issueId')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies(canOnEitherMatrix(Action.Read))
  async getIssue(
    @Param('issueId', ParseIntPipe) issueId: number,
    @Req() request: AuthenticatedRequest,
  ) {
    return this.issuesService.findIssueForDetail(
      issueId,
      await this.resolveTarget(request),
    );
  }

  /**
   * One update route for both editors: the form sends the whole issue as
   * multipart, the table sends only the field it changed as JSON.
   */
  @Patch(':issueId')
  @HttpCode(HttpStatus.OK)
  @CheckPolicies(canOnEitherMatrix(Action.Update))
  @UseInterceptors(FileInterceptor('attachmentFile', attachmentUploadOptions))
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody({ type: UpdateIssueDto })
  @ApiOperation({ summary: 'Update an issue' })
  async updateIssue(
    @Param('issueId', ParseIntPipe) issueId: number,
    @Body() dto: UpdateIssueDto,
    @Req() request: AuthenticatedRequest,
    @UploadedFile() attachmentFile?: Express.Multer.File,
  ) {
    return this.issuesService.updateIssue(
      issueId,
      dto,
      await this.resolveTarget(request),
      attachmentFile,
      this.getAuthenticatedUserId(request),
    );
  }

  /** Which matrix the caller sees, worked out from the roles they hold. */
  private resolveTarget(request: AuthenticatedRequest) {
    return this.issuesService.resolveTarget(
      this.getAuthenticatedUserId(request),
    );
  }

  private getAuthenticatedUserId(request: AuthenticatedRequest): number {
    const userId = request.user?.id;

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

    return Number(userId);
  }
}
