import {
  BadRequestException,
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  HttpStatus,
  NotFoundException,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
  Req,
  Res,
  UnauthorizedException,
  UploadedFile,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBearerAuth, ApiConsumes, ApiTags } from '@nestjs/swagger';
import type { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import { existsSync, mkdirSync, statSync } from 'node:fs';
import { basename, extname, resolve, sep } from 'node:path';
import { diskStorage } from 'multer';

import { JwtAuthGuard } from '@/modules/auth/jwt-auth.guard';

import { CreatePlenaryDto } from './dto/create-plenary.dto';
import { PlenaryQueryDto } from './dto/plenary-query.dto';
import { UpdatePlenaryDto } from './dto/update-plenary.dto';
import { PlenaryService } from './plenary.service';

const MAX_PDF_SIZE = 10 * 1024 * 1024;

const PLENARY_DOCUMENT_DIRECTORY = resolve(
  process.cwd(),
  'uploads',
  'plenaries',
);

mkdirSync(PLENARY_DOCUMENT_DIRECTORY, {
  recursive: true,
});

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

type PlenaryDocumentResult = {
  documentReference?: string | null;
  data?: {
    documentReference?: string | null;
  };
};

function createPdfUploadOptions() {
  return {
    storage: diskStorage({
      destination: (
        _request: Request,
        _file: Express.Multer.File,
        callback: (error: Error | null, destination: string) => void,
      ) => {
        callback(null, PLENARY_DOCUMENT_DIRECTORY);
      },

      filename: (
        _request: Request,
        file: Express.Multer.File,
        callback: (error: Error | null, filename: string) => void,
      ) => {
        const extension = extname(file.originalname).toLowerCase() || '.pdf';

        callback(null, `${Date.now()}-${randomUUID()}${extension}`);
      },
    }),

    limits: {
      fileSize: MAX_PDF_SIZE,
    },

    fileFilter: (
      _request: Request,
      file: Express.Multer.File,
      callback: (error: Error | null, acceptFile: boolean) => void,
    ) => {
      const extension = extname(file.originalname).toLowerCase();

      const isPdf = file.mimetype === 'application/pdf' || extension === '.pdf';

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

      callback(null, true);
    },
  };
}

@ApiTags('plenaries')
@ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard)
@Controller('plenaries')
export class PlenaryController {
  constructor(private readonly plenaryService: PlenaryService) {}

  @Get('lookups/ministries')
  getMinistries(
    @Query('relatedStakeholderId')
    relatedStakeholderId?: string,
  ) {
    return this.plenaryService.getMinistries(
      relatedStakeholderId ? Number(relatedStakeholderId) : undefined,
    );
  }

  @Get()
  findAll(
    @Query() query: PlenaryQueryDto,
    @Req() request: AuthenticatedRequest,
  ) {
    const userId =
      query.ministryOnly === true
        ? this.getAuthenticatedUserId(request)
        : undefined;

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

  /**
   * Create a Plenary and upload its PDF in one request.
   *
   * POST /api/v1/plenaries
   * Content-Type: multipart/form-data
   *
   * Fields:
   * - name: Text
   * - meetingDate: Text
   * - deadline: Text (optional)
   * - status: Text (optional)
   * - ministryIds: Text, e.g. [1,10]
   * - document: File (optional PDF)
   */
  @Post()
  @ApiConsumes('multipart/form-data')
  @UseInterceptors(FileInterceptor('document', createPdfUploadOptions()))
  create(
    @Body() dto: CreatePlenaryDto,
    @UploadedFile()
    document: Express.Multer.File | undefined,
    @Req() request: AuthenticatedRequest,
  ) {
    const documentReference = document
      ? `/uploads/plenaries/${document.filename}` +
        `?originalName=${encodeURIComponent(
          document.originalname.trim() || document.filename,
        )}`
      : dto.documentReference;

    return this.plenaryService.create(
      {
        ...dto,
        documentReference,
      },
      this.getAuthenticatedUserId(request),
    );
  }

  /**
   * View the uploaded Plenary PDF.
   */
  @Get(':id/document')
  async viewDocument(
    @Param('id', ParseIntPipe)
    plenaryId: number,
    @Req() request: AuthenticatedRequest,
    @Res() response: Response,
  ): Promise<void> {
    const result = (await this.plenaryService.findOne(
      plenaryId,
      this.getAuthenticatedUserId(request),
    )) as PlenaryDocumentResult;

    const documentReference =
      result.data?.documentReference ?? result.documentReference ?? null;

    if (!documentReference?.trim()) {
      throw new NotFoundException('Plenary document was not uploaded.');
    }

    const documentInfo = this.resolveStoredDocument(documentReference);

    if (
      !existsSync(documentInfo.absolutePath) ||
      !statSync(documentInfo.absolutePath).isFile()
    ) {
      throw new NotFoundException(
        `Plenary PDF file was not found: ${documentInfo.displayName}`,
      );
    }

    response.setHeader('Content-Type', 'application/pdf');

    response.setHeader(
      'Content-Disposition',
      `inline; filename*=UTF-8''${encodeURIComponent(
        documentInfo.displayName,
      )}`,
    );

    response.setHeader(
      'Cache-Control',
      'private, no-cache, no-store, must-revalidate',
    );

    response.sendFile(documentInfo.absolutePath);
  }

  @Get(':id')
  findOne(
    @Param('id', ParseIntPipe)
    plenaryId: number,
    @Req() request: AuthenticatedRequest,
  ) {
    return this.plenaryService.findOne(
      plenaryId,
      this.getAuthenticatedUserId(request),
    );
  }

  /**
   * Replace the PDF of an existing Plenary.
   *
   * PATCH /api/v1/plenaries/:id/document
   * form-data key: file
   */
  @Patch(':id/document')
  @HttpCode(HttpStatus.OK)
  @ApiConsumes('multipart/form-data')
  @UseInterceptors(FileInterceptor('file', createPdfUploadOptions()))
  updateDocument(
    @Param('id', ParseIntPipe)
    plenaryId: number,
    @UploadedFile()
    file: Express.Multer.File | undefined,
  ) {
    if (!file) {
      throw new BadRequestException('Please select a PDF document file.');
    }

    const originalName = file.originalname.trim() || file.filename;

    const documentReference =
      `/uploads/plenaries/${file.filename}` +
      `?originalName=${encodeURIComponent(originalName)}`;

    return this.plenaryService.updateDocument(plenaryId, documentReference);
  }

  @Patch(':id')
  update(
    @Param('id', ParseIntPipe)
    plenaryId: number,
    @Body() dto: UpdatePlenaryDto,
  ) {
    return this.plenaryService.update(plenaryId, dto);
  }

  @Patch(':id/submit')
  submitToCdc(
    @Param('id', ParseIntPipe)
    plenaryId: number,
  ) {
    return this.plenaryService.submitToCdc(plenaryId);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.OK)
  remove(
    @Param('id', ParseIntPipe)
    plenaryId: number,
  ) {
    return this.plenaryService.remove(plenaryId);
  }

  private resolveStoredDocument(reference: string): {
    absolutePath: string;
    displayName: string;
  } {
    const [storedPath, queryString = ''] = reference.trim().split('?');

    const storedFileName = basename(storedPath.replace(/\\/g, '/'));

    if (!storedFileName) {
      throw new NotFoundException('Invalid Plenary document path.');
    }

    const absolutePath = resolve(PLENARY_DOCUMENT_DIRECTORY, storedFileName);

    const uploadRootWithSeparator = PLENARY_DOCUMENT_DIRECTORY.endsWith(sep)
      ? PLENARY_DOCUMENT_DIRECTORY
      : `${PLENARY_DOCUMENT_DIRECTORY}${sep}`;

    if (
      absolutePath !== PLENARY_DOCUMENT_DIRECTORY &&
      !absolutePath.startsWith(uploadRootWithSeparator)
    ) {
      throw new NotFoundException('Invalid Plenary document path.');
    }

    const query = new URLSearchParams(queryString);

    const originalName = query.get('originalName')?.trim();

    return {
      absolutePath,
      displayName: this.safeDecode(originalName) || storedFileName,
    };
  }

  private safeDecode(value: string | null | undefined): string {
    if (!value) {
      return '';
    }

    try {
      return decodeURIComponent(value);
    } catch {
      return value;
    }
  }

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