import { AuthGuard, type Viewer } from '@authentication/auth.guard'
import { ViewerParam } from '@authentication/viewer.decorator'
import { Controller, Get, Logger, Param, Post, Query, Res, UseGuards } from '@nestjs/common'
import { UploadGuard } from '@upload/upload.guard'
import { UploadedFileData } from '@upload/uploaded-file.decorator'
import type { UploadedFileDataPayload } from '@upload/upload.types'

import { FastifyReply } from 'fastify'

import { ExcelIoService } from './excel-io.service'

const MAX_IMPORT_FILE_SIZE_BYTES = 20 * 1024 * 1024 // 20 MB

/**
 * Security note:
 * This controller is always registered and guarded with AuthGuard.
 * In local dev with AUTH=false, mock auth still provides a viewer context.
 */
@UseGuards(AuthGuard)
@Controller('excel-io')
export class ExcelIoController {
  private readonly logger = new Logger(ExcelIoController.name)

  constructor(private readonly excelIoService: ExcelIoService) {}

  @Get('export/all')
  async exportAll(@ViewerParam() viewer: Viewer, @Res({ passthrough: true }) res: FastifyReply): Promise<void> {
    try {
      const { filename, file } = await this.excelIoService.exportAllModelsToExcel(viewer)
      const disposition = createContentDisposition(filename)

      res.type('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
      res.header('Content-Disposition', disposition)
      res.header('Access-Control-Expose-Headers', 'Content-Disposition')
      res.send(file)
    } catch (error) {
      this.logger.error('Error exporting all models to Excel', error)
      res.status(500).send({ error: error instanceof Error ? error.message : String(error) })
    }
  }

  @Get('export/:model')
  async exportModel(
    @Param('model') model: string,
    @ViewerParam() viewer: Viewer,
    @Res({ passthrough: true }) res: FastifyReply,
    @Query('filters') filters?: string,
    @Query('sort') sort?: string,
  ): Promise<void> {
    try {
      const parsedModel = this.excelIoService.parseModelName(model)
      const rawFilters = parseJsonQuery(filters)
      const rawSort = parseJsonQuery(sort)

      const { filename, file } = await this.excelIoService.exportModelToExcel({
        model: parsedModel,
        rawFilters,
        rawSort,
        authorization: viewer,
      })
      const disposition = createContentDisposition(filename)

      res.type('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
      res.header('Content-Disposition', disposition)
      res.header('Access-Control-Expose-Headers', 'Content-Disposition')
      res.send(file)
    } catch (error) {
      this.logger.error(`Error exporting model ${model} to Excel`, error)
      res.status(500).send({ error: error instanceof Error ? error.message : String(error) })
    }
  }

  @Post('import')
  @UseGuards(
    UploadGuard({
      maxFileSizeBytes: MAX_IMPORT_FILE_SIZE_BYTES,
      allowedFileTypes: ['excel'],
    }),
  )
  async importExcel(
    @ViewerParam() viewer: Viewer,
    @UploadedFileData() uploadData: UploadedFileDataPayload,
    @Res({ passthrough: true }) res: FastifyReply,
  ): Promise<void> {
    try {
      const result = await this.excelIoService.importExcel({
        data: uploadData.buffer,
        filename: uploadData.filename,
        authorization: viewer,
      })

      res.status(200).send({ success: true, result })
    } catch (error) {
      this.logger.error('Error importing Excel file', error)
      res.status(500).send({ success: false, error: error instanceof Error ? error.message : String(error) })
    }
  }

  @Post('import/preview')
  @UseGuards(
    UploadGuard({
      maxFileSizeBytes: MAX_IMPORT_FILE_SIZE_BYTES,
      allowedFileTypes: ['excel'],
    }),
  )
  async previewImportExcel(
    @ViewerParam() _viewer: Viewer,
    @UploadedFileData() uploadData: UploadedFileDataPayload,
    @Res({ passthrough: true }) res: FastifyReply,
  ): Promise<void> {
    try {
      const result = await this.excelIoService.previewImportExcel({
        data: uploadData.buffer,
        filename: uploadData.filename,
        includeDetailedUnchangedRecords: parseMultipartBooleanField(
          uploadData.fields,
          'includeDetailedUnchangedRecords',
          false,
        ),
      })

      res.status(200).send({ success: true, result })
    } catch (error) {
      this.logger.error('Error previewing Excel import', error)
      res.status(500).send({ success: false, error: error instanceof Error ? error.message : String(error) })
    }
  }
}

function parseJsonQuery(value?: string): unknown {
  if (!value) {
    return undefined
  }

  try {
    return JSON.parse(value)
  } catch (error) {
    throw new Error(`Invalid JSON query parameter: ${error instanceof Error ? error.message : String(error)}`)
  }
}

function createContentDisposition(filename: string): string {
  const asciiFilename = filename.replaceAll(/[^A-Za-z0-9._-]/g, '_')
  const encodedFilename = encodeURIComponent(filename)

  return `attachment; filename="${asciiFilename}"; filename*=UTF-8''${encodedFilename}`
}

function parseMultipartBooleanField(fields: unknown, key: string, defaultValue: boolean): boolean {
  if (!fields || typeof fields !== 'object') {
    return defaultValue
  }

  const fieldValue = (fields as Record<string, unknown>)[key]
  if (!fieldValue || typeof fieldValue !== 'object') {
    return defaultValue
  }

  const value = 'value' in fieldValue ? (fieldValue as { value?: unknown }).value : undefined
  if (typeof value !== 'string') {
    return defaultValue
  }

  return value.toLowerCase() === 'true'
}
