import {
  BadRequestException,
  Body,
  Controller,
  Get,
  Param,
  ParseIntPipe,
  Post,
  Query,
  Req,
  Res,
  ServiceUnavailableException,
  UploadedFile,
  UploadedFiles,
  UnauthorizedException,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { plainToInstance } from 'class-transformer';
import { Request, Response } from 'express';
import { JwtAuthGuard } from '@api/modules/auth/user/jwt.guard';
import {
  CreateHistoryDto,
  GenerateInvoiceDto,
  GetAllInsurersResponse,
  GetHistoriesDto,
  GetHistoriesResponse,
  GetHistoryResponse,
  GetInvoiceByInsurerResponse,
} from './invoice.dto';
import { InvoiceService } from './invoice.service';

const INVOICE_PDF_VIEW_AUDIENCE = 'invoice-pdf-view';
const INVOICE_PDF_VIEW_ISSUER = 'insuniverse-api';

interface InvoicePdfViewToken {
  purpose: typeof INVOICE_PDF_VIEW_AUDIENCE;
  ivhId: number;
  memId: number;
  insurerName: string;
}

@ApiTags('청구서')
//* 가드가 없어 비로그인 상태에서 ivhId만 바꾸면 남의 청구서/PDF가 그대로 열렸다.
//* 회원과 관리자가 서로 다른 경로·가드로 조회하므로 라우트별로 명시한다.
@Controller('invoices')
export class InvoiceController {
  constructor(
    private invoiceService: InvoiceService,
    private jwtService: JwtService,
  ) {}

  @ApiOperation({ summary: '전체보험사조회' })
  @UseGuards(JwtAuthGuard)
  @Get('insurers')
  async getAllInsurer() {
    const result = await this.invoiceService.getAllInsurer();
    return plainToInstance(GetAllInsurersResponse, result);
  }

  @ApiOperation({ summary: '보험사별 좌표' })
  @UseGuards(JwtAuthGuard)
  @Get('insurer/:insCode')
  async getInvoiceByInsurer(@Param('insCode') insCode: string) {
    const result = await this.invoiceService.getInvoiceByInsurer(insCode);
    return plainToInstance(GetInvoiceByInsurerResponse, result);
  }

  @ApiOperation({ summary: '청구서발급내역목록조회' })
  @UseGuards(JwtAuthGuard)
  @Get('histories')
  async getHistories(@Query() dto: GetHistoriesDto, @Req() req: Request) {
    //* 조회 대상은 토큰으로만 정한다(클라 전송 memId 무시).
    const memId = Number(req['user']?.memId ?? -1);
    const result = await this.invoiceService.getHistories({ ...dto, memId });
    return plainToInstance(GetHistoriesResponse, result);
  }

  @ApiOperation({ summary: '청구서발급내역상세' })
  @UseGuards(JwtAuthGuard)
  @Get('histories/:ivhId')
  async getHistory(
    @Param('ivhId', ParseIntPipe) ivhId: number,
    @Req() req: Request,
  ) {
    const result = await this.invoiceService.getHistory(
      ivhId,
      Number(req['user']?.memId ?? -1),
    );
    return plainToInstance(GetHistoryResponse, result);
  }

  @ApiOperation({ summary: '청구서 PDF 다운로드' })
  @UseGuards(JwtAuthGuard)
  @Get('histories/:ivhId/pdf')
  async getHistoryPdf(
    @Param('ivhId', ParseIntPipe) ivhId: number,
    @Req() req: Request,
  ) {
    const { pdfBuffers, pdfNames, attachmentBuffers, attachmentNames } =
      await this.invoiceService.getHistoryPdf(
        ivhId,
        Number(req['user']?.memId ?? -1),
      );

    // Base64로 인코딩하여 JSON 응답 (원본 파일명 포함)
    return {
      invoices: pdfBuffers.map(buf => buf.toString('base64')),
      invoiceNames: pdfNames,
      attachments: attachmentBuffers.map(buf => buf.toString('base64')),
      attachmentNames: attachmentNames,
    };
  }

  @ApiOperation({ summary: '청구서 + 첨부파일 병합 PDF 조회' })
  @UseGuards(JwtAuthGuard)
  @Get('histories/:ivhId/merged-pdf')
  async getMergedPdf(
    @Param('ivhId', ParseIntPipe) ivhId: number,
    @Query('insurerName') insurerName: string,
    @Req() req: Request,
    @Res() res: Response,
  ) {
    const { pdfBuffer, filename } = await this.invoiceService.getMergedPdf(
      ivhId,
      Number(req['user']?.memId ?? -1),
      insurerName,
    );

    res.set({
      'Content-Type': 'application/pdf',
      'Content-Disposition': `inline; filename=${encodeURIComponent(filename)}`,
    });
    res.send(pdfBuffer);
  }

  @ApiOperation({ summary: '기존 PDF 뷰어용 단기 조회 링크 발급' })
  @UseGuards(JwtAuthGuard)
  @Post('histories/:ivhId/merged-pdf-view')
  async createMergedPdfViewLink(
    @Param('ivhId', ParseIntPipe) ivhId: number,
    @Body('insurerName') rawInsurerName: string,
    @Req() req: Request,
  ) {
    const insurerName =
      typeof rawInsurerName === 'string' ? rawInsurerName.trim() : '';
    const memId = Number(req['user']?.memId ?? -1);

    if (!insurerName || insurerName.length > 100) {
      throw new BadRequestException('보험사명이 올바르지 않습니다.');
    }
    if (!Number.isInteger(memId) || memId <= 0) {
      throw new UnauthorizedException();
    }

    const secret = process.env.ADMIN_VIEW_SECRET || process.env.AES_SECRET;
    if (!secret) {
      throw new ServiceUnavailableException(
        '청구서 링크를 발급할 수 없습니다.',
      );
    }

    //* 링크 발급 시점에도 소유권을 확인하고, 조회 시점에 다시 확인한다.
    await this.invoiceService.getHistory(ivhId, memId);

    const access = this.jwtService.sign(
      {
        purpose: INVOICE_PDF_VIEW_AUDIENCE,
        ivhId,
        memId,
        insurerName,
      } satisfies InvoicePdfViewToken,
      {
        secret,
        audience: INVOICE_PDF_VIEW_AUDIENCE,
        issuer: INVOICE_PDF_VIEW_ISSUER,
        expiresIn: '1m',
      },
    );

    return {
      path: `/invoices/histories/${ivhId}/merged-pdf-view?access=${encodeURIComponent(access)}`,
    };
  }

  @ApiOperation({ summary: '단기 조회 링크로 병합 PDF 열기' })
  @Get('histories/:ivhId/merged-pdf-view')
  async viewMergedPdf(
    @Param('ivhId', ParseIntPipe) ivhId: number,
    @Query('access') access: string,
    @Res() res: Response,
  ) {
    let payload: InvoicePdfViewToken;
    const secret = process.env.ADMIN_VIEW_SECRET || process.env.AES_SECRET;

    try {
      if (!secret) throw new Error('PDF view secret is missing');
      payload = this.jwtService.verify<InvoicePdfViewToken>(access, {
        secret,
        audience: INVOICE_PDF_VIEW_AUDIENCE,
        issuer: INVOICE_PDF_VIEW_ISSUER,
      });
    } catch {
      throw new UnauthorizedException('유효하지 않거나 만료된 청구서 링크입니다.');
    }

    if (
      payload.purpose !== INVOICE_PDF_VIEW_AUDIENCE ||
      Number(payload.ivhId) !== ivhId ||
      !Number.isInteger(Number(payload.memId)) ||
      Number(payload.memId) <= 0 ||
      typeof payload.insurerName !== 'string' ||
      !payload.insurerName.trim() ||
      payload.insurerName.length > 100
    ) {
      throw new UnauthorizedException('유효하지 않거나 만료된 청구서 링크입니다.');
    }

    const { pdfBuffer, filename } = await this.invoiceService.getMergedPdf(
      ivhId,
      Number(payload.memId),
      payload.insurerName,
    );

    res.set({
      'Content-Type': 'application/pdf',
      'Content-Disposition': `inline; filename=${encodeURIComponent(filename)}`,
      'Cache-Control': 'private, no-store',
      'Referrer-Policy': 'no-referrer',
      'X-Content-Type-Options': 'nosniff',
    });
    res.send(pdfBuffer);
  }

  @ApiOperation({ summary: '청구서생성' })
  @UseGuards(JwtAuthGuard)
  @Post()
  async generate(@Body() dto: GenerateInvoiceDto, @Res() res: Response) {
    const { doc, filename } = await this.invoiceService.generate(dto);
    res.set({
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename=${encodeURIComponent(filename)}`,
    });
    res.send(Buffer.from(doc));
  }

  @ApiOperation({ summary: '청구서발급내역생성' })
  @UseGuards(JwtAuthGuard)
  @Post('histories')
  @UseInterceptors(FilesInterceptor('files', 10))
  async createHistory(
    @Body() body: any,
    @Req() req: Request,
    @UploadedFiles() files?: Express.Multer.File[],
  ) {
    // FormData에서 데이터 파싱
    // requestedInvoices는 이미 배열로 들어옴
    const requestedInvoices = Array.isArray(body.requestedInvoices)
      ? body.requestedInvoices
      : [body.requestedInvoices].filter(Boolean);

    // insurerNames 파싱 (보험사명 배열)
    const insurerNames = body.insurerNames
      ? Array.isArray(body.insurerNames)
        ? body.insurerNames
        : [body.insurerNames].filter(Boolean)
      : undefined;

    const dto: CreateHistoryDto = {
      //* 소유자는 토큰으로만 정한다. 클라 전송 memId를 쓰면 남의 계정으로 청구서가 생성된다.
      memId: Number(req['user']?.memId ?? -1),
      ordId: parseInt(body.ordId, 10),
      insured: {
        name: body['insured.name'],
        phone: body['insured.phone'],
      },
      requestedInvoices,
    };

    // 모든 파일을 청구서 PDF로 저장 (보험사별로 여러 개)
    const pdfBuffers = files?.map(file => file.buffer) || [];
    return await this.invoiceService.createHistory(dto, pdfBuffers, insurerNames);
  }

  @ApiOperation({ summary: '청구서 첨부파일 업로드' })
  @UseGuards(JwtAuthGuard)
  @Post('histories/:ivhId/attachments')
  @UseInterceptors(FilesInterceptor('files', 10))
  async uploadAttachments(
    @Param('ivhId', ParseIntPipe) ivhId: number,
    @UploadedFiles() files: Express.Multer.File[],
    @Body() body: any,
  ) {
    // fileNames 배열 파싱 (프론트에서 전달한 원본 파일명)
    const fileNames: string[] = [];
    if (body.fileNames) {
      Object.keys(body.fileNames).forEach((key) => {
        fileNames.push(body.fileNames[key]);
      });
    }

    return await this.invoiceService.uploadAttachments(ivhId, files, fileNames);
  }
}
