import { Injectable } from '@nestjs/common'

import { Client, Export, Import } from 'xlport'

import { XlPortConfig } from './xlport.config'

@Injectable()
export class XlPortService {
  private readonly client: Client

  constructor(config: XlPortConfig) {
    this.client = new Client({ apiKey: config.config.apiKey, url: config.config.url })
  }

  /**
   * Loads data from an Excel file and returns the result.
   *
   * ## Example:
   * Here is a typical controller method that uses this function:
   *
   * ```typescript
   * \@Post('/import/:id')
   * \@UseGuards(UploadAndLogGuard)
   * \@UseGuards(AuthGuard)
   * async importSurveyForParticipant(
   *   \@Param('id') id: string,
   *   \@Req() req: FastifyRequestWithFileAndId,
   *   \@FileStream() file: MultipartFile,
   *   \@UserParam() user: User,
   * ): Promise<SubmissionImportResult> {
   *
   *   if (!file) {
   *     throw new BadRequestException('No attachment was provided')
   *   }
   *
   *   const start = Date.now()
   *   const xlData = await this.xlPort.importFromFile(await file.toBuffer())
   *
   *   if (xlData.status !== 'success') {
   *     this.logger.warn(xlData.message)
   *     throw new Error(`Error in reading Excel data: ${JSON.stringify(xlData.message)}`)
   *   }
   *
   *   // NOTE: You need to provide a decoder for your data
   *   const dataParsed = decoder.safeParse(
   *     this.xlPort.replaceBlankTables(this.xlPort.replaceBlankStringsWithNull(xlData.data.tables)),
   *   )
   *
   *   if (!dataParsed.success) {
   *     this.logger.warn(dataParsed.error.format())
   *     throw new Error(`Error in parsing Excel data: ${JSON.stringify(dataParsed.error.flatten())}`)
   *   }
   *   this.logger.log(dataParsed.data)
   *
   *   const result = await this.handleImport({
   *     data: dataParsed.data,
   *     user,
   *   })
   *
   *   if (result.type === 'error') {
   *     this.logger.error(
   *       `Error importing data for (${id}) in ${file.filename}: ${JSON.stringify(result.error)}`,
   *     )
   *     return result
   *   }
   *
   *   return result
   * }
   * ```
   */
  public async importFromFile(file: string | Buffer, request?: Import.Request) {
    return this.client.importFromFile(file, request)
  }

  /**
   * Loads data into an Excel template and returns the result as a file.
   *
   * ## Example:
   * Here is a typical controller method that uses this function:
   *
   * ```typescript
   * \@Get('export/:id')
   * async export(@Res({ passthrough: true }) response: FastifyReply, \@Param('id') id: string) {
   *
   *   const data = await this.getData(id)
   *
   *   const request: Export.TemplateIdBody = {
   *     // NOTE: The template must be uploaded to xlport's templates folder
   *     templateId: 'project/template.xlsx',
   *     data,
   *   }
   *
   *   // NOTE: Change this to mimeTypes.xlsm in case template contains macros
   *   const returnType = mimeTypes.xlsx
   *   const filename = `${timeStamp()} ${submission.name}`
   *
   *   void response.type(returnType)
   *   void response.header('Content-Disposition', `attachment; filename=${filename}.{returnType === mimeTypes.xlsx ? 'xlsx' : 'xlsm'}`)
   *   return this.xlPort.exportToFile(request)
   * }
   * ```
   */
  public exportToFile(body: Export.Body) {
    return this.client.exportToFile(body)
  }

  /**
   * Replaces blank tables with empty arrays.
   *
   * A blank table in Excel still always has one row that is empty.
   * xlPort will return this row as an object with each field as null.
   * This function replaces these tables with blank arrays.
   *
   * Known shortcoming: This function will not replace tables if the table
   * has calculated columns that return non-null values by default!
   * This case must be accounted for manually.
   */
  public replaceBlankTables(body: Import.Data['tables']): Import.Data['tables'] {
    if (!body) {
      return body
    }

    for (const [key, table] of Object.entries(body)) {
      // we are only interested in tables that have exactly one row
      if (table.length !== 1) {
        continue
      }
      // we currently only support tables with objects as rows
      if (!Array.isArray(table)) {
        continue
      }

      const row = table[0] as object
      if (Object.values(row).every((value) => value === null)) {
        body[key] = []
      }
    }
    return body
  }

  /**
   * Replaces blank strings with null.
   *
   * Excel does not allow formula expressions to return null.
   * Instead, one often returns a blank string.
   * However, this distorts the decoders for any non-string types.
   */
  public replaceBlankStringsWithNull(body: Import.Data['tables'], tablesToReplace?: string[]): Import.Data['tables'] {
    if (!body) {
      return body
    }

    for (const [key, table] of Object.entries(body)) {
      if (tablesToReplace && !tablesToReplace.includes(key)) {
        continue
      }
      // we currently only support tables with objects as rows
      if (!Array.isArray(table)) {
        continue
      }
      for (const row of table) {
        for (const [field, value] of Object.entries(row)) {
          if (typeof value === 'string' && value.trim() === '') {
            row[field] = null
          }
        }
      }
    }
    return body
  }
}
