/*
 * Copyright © 2024 Hexastack. All rights reserved.
 *
 * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
 * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
 * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
 */

import fs from 'fs';
import path from 'path';

import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  NotFoundException,
  Param,
  Patch,
  Post,
  Query,
  UseInterceptors,
} from '@nestjs/common';
import { BadRequestException } from '@nestjs/common/exceptions';
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
import Papa from 'papaparse';

import { AttachmentService } from '@/attachment/services/attachment.service';
import { config } from '@/config';
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { LoggerService } from '@/logger/logger.service';
import { BaseController } from '@/utils/generics/base-controller';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { TFilterQuery } from '@/utils/types/filter.types';

import { ContentCreateDto, ContentUpdateDto } from '../dto/content.dto';
import {
  Content,
  ContentFull,
  ContentPopulate,
  ContentStub,
} from '../schemas/content.schema';

import { ContentTypeService } from './../services/content-type.service';
import { ContentService } from './../services/content.service';

@UseInterceptors(CsrfInterceptor)
@Controller('content')
export class ContentController extends BaseController<
  Content,
  ContentStub,
  ContentPopulate,
  ContentFull
> {
  constructor(
    private readonly contentService: ContentService,
    private readonly contentTypeService: ContentTypeService,
    private readonly attachmentService: AttachmentService,
    private readonly logger: LoggerService,
  ) {
    super(contentService);
  }

  /**
   * Creates new content based on the provided DTO, filtering dynamic fields to match
   * the associated content type before persisting it.
   *
   * @param contentDto - The DTO containing the content data to be created.
   *
   * @returns The created content document.
   */
  @CsrfCheck(true)
  @Post()
  async create(@Body() contentDto: ContentCreateDto): Promise<Content> {
    // Find the content type that corresponds to the given content
    const contentType = await this.contentTypeService.findOne(
      contentDto.entity,
    );
    this.validate({
      dto: contentDto,
      allowedIds: {
        entity: contentType?.id,
      },
    });
    return await this.contentService.create(contentDto);
  }

  /**
   * Imports content from a CSV file based on the provided content type and file ID.
   *
   * @param idTargetContentType - The content type to match the CSV data against.
   * @param idFileToImport - The ID of the file to be imported.
   *
   * @returns A promise that resolves to the newly created content documents.
   */
  @Get('import/:idTargetContentType/:idFileToImport')
  async import(
    @Param()
    {
      idTargetContentType: targetContentType,
      idFileToImport: fileToImport,
    }: {
      idTargetContentType: string;
      idFileToImport: string;
    },
  ) {
    // Check params
    if (!fileToImport || !targetContentType) {
      this.logger.warn(`Parameters are missing`);
      throw new NotFoundException(`Missing params`);
    }

    // Find the content type that corresponds to the given content
    const contentType =
      await this.contentTypeService.findOne(targetContentType);
    if (!contentType) {
      this.logger.warn(
        `Failed to fetch content type with id ${targetContentType}. Content type not found.`,
      );
      throw new NotFoundException(`Content type is not found`);
    }

    // Get file location
    const file = await this.attachmentService.findOne(fileToImport);
    // Check if file is present
    const filePath = file
      ? path.join(config.parameters.uploadDir, file.location)
      : undefined;

    if (!file || !filePath || !fs.existsSync(filePath)) {
      this.logger.warn(`Failed to find file type with id ${fileToImport}.`);
      throw new NotFoundException(`File does not exist`);
    }
    //read file sync
    const data = fs.readFileSync(filePath, 'utf8');

    const result = Papa.parse<Record<string, string | boolean | number>>(data, {
      header: true,
      skipEmptyLines: true,
      dynamicTyping: true,
    });

    if (result.errors.length > 0) {
      this.logger.warn(
        `Errors parsing the file: ${JSON.stringify(result.errors)}`,
      );

      throw new BadRequestException(result.errors, {
        cause: result.errors,
        description: 'Error while parsing CSV',
      });
    }

    const contentsDto = result.data.reduce(
      (acc, { title, status, ...rest }) => [
        ...acc,
        {
          title: String(title),
          status: Boolean(status),
          entity: targetContentType,
          dynamicFields: Object.keys(rest)
            .filter((key) =>
              contentType.fields?.map((field) => field.name).includes(key),
            )
            .reduce((filtered, key) => ({ ...filtered, [key]: rest[key] }), {}),
        },
      ],
      [],
    );

    // Create content
    return await this.contentService.createMany(contentsDto);
  }

  /**
   * Retrieves paginated content based on filters and optional population of related entities.
   *
   * @param pageQuery - Pagination parameters.
   * @param populate - Fields to populate in the query.
   * @param filters - Filters for content retrieval.
   *
   * @returns Paginated content list.
   */
  @Get()
  async findPage(
    @Query(PageQueryPipe) pageQuery: PageQueryDto<Content>,
    @Query(PopulatePipe) populate: string[],
    @Query(
      new SearchFilterPipe<Content>({ allowedFields: ['entity', 'title'] }),
    )
    filters: TFilterQuery<Content>,
  ) {
    return this.canPopulate(populate)
      ? await this.contentService.findAndPopulate(filters, pageQuery)
      : await this.contentService.find(filters, pageQuery);
  }

  /**
   * Counts the filtered number of contents based on the provided filters.
   *
   * @param filters - Optional filters for counting content.
   *
   * @returns The count of content matching the filters.
   */
  @Get('count')
  async filterCount(
    @Query(
      new SearchFilterPipe<Content>({ allowedFields: ['entity', 'title'] }),
    )
    filters?: TFilterQuery<Content>,
  ) {
    return await this.count(filters);
  }

  /**
   * Retrieves a single content by ID, with optional population of related entities.
   *
   * @param id - The ID of the content to retrieve.
   * @param populate - Fields to populate in the query.
   *
   * @returns The requested content document.
   */
  @Get(':id')
  async findOne(
    @Param('id') id: string,
    @Query(PopulatePipe) populate: string[],
  ) {
    const doc = this.canPopulate(populate)
      ? await this.contentService.findOneAndPopulate(id)
      : await this.contentService.findOne(id);

    if (!doc) {
      this.logger.warn(
        `Failed to fetch content with id ${id}. Content not found.`,
      );
      throw new NotFoundException(`Content of id ${id} not found`);
    }
    return doc;
  }

  /**
   * Deletes a content document by ID.
   *
   * @param id - The ID of the content to delete.
   *
   * @returns The result of the delete operation.
   */
  @CsrfCheck(true)
  @Delete(':id')
  @HttpCode(204)
  async deleteOne(@Param('id') id: string) {
    const removedContent = await this.contentService.deleteOne(id);
    if (removedContent.deletedCount === 0) {
      this.logger.warn(
        `Failed to delete content with id ${id}. Content not found.`,
      );
      throw new NotFoundException(`Content of id ${id} not found`);
    }
    return removedContent;
  }

  /**
   * Retrieves content based on content type ID with optional pagination.
   *
   * @param contentType - The content type ID to filter by.
   * @param pageQuery - Pagination parameters.
   *
   * @returns List of content documents matching the content type.
   */
  @Get('/type/:id')
  async findByType(
    @Param('id') contentType: string,
    @Query(PageQueryPipe) pageQuery: PageQueryDto<Content>,
  ): Promise<Content[]> {
    const type = await this.contentTypeService.findOne(contentType);
    if (!type) {
      this.logger.warn(
        `Failed to find content with contentType ${contentType}. ContentType not found.`,
      );
      throw new NotFoundException(`ContentType of id ${contentType} not found`);
    }
    return await this.contentService.find({ entity: contentType }, pageQuery);
  }

  /**
   * Updates a content document by ID, after filtering dynamic fields to match the associated content type.
   *
   * @param contentDto - The DTO containing the updated content data.
   * @param id - The ID of the content to update.
   *
   * @returns The updated content document.
   */
  @CsrfCheck(true)
  @Patch(':id')
  async updateOne(
    @Body() contentDto: ContentUpdateDto,
    @Param('id') id: string,
  ): Promise<Content> {
    const updatedContent = await this.contentService.updateOne(id, contentDto);
    if (!updatedContent) {
      this.logger.warn(
        `Failed to update content with id ${id}. Content not found.`,
      );
      throw new NotFoundException(`Content of id ${id} not found`);
    }
    return updatedContent;
  }
}
