import type { Request, Response } from 'express';
import { paramStr, queryNum, queryStr } from '@/api/http/request';
import { toCsv } from '@/shared/csv';
import processFile from '@/middlewares/upload';
import ValidationError from '@/shared/errors/validation';

type CurrentUserArg = Request['currentUser'];

/**
 * The slice of a generic-CRUD service the controller calls. `CreateData`/
 * `UpdateData` are inferred from the service (the controller feeds them `any`
 * from `req.body`); list/count take the raw query, which the entity filters
 * already accept.
 */
export interface CrudControllerService<CreateData, UpdateData> {
  create(data: CreateData, currentUser?: CurrentUserArg): Promise<void>;
  bulkImport(fileBuffer: Buffer, currentUser?: CurrentUserArg): Promise<void>;
  update(
    data: UpdateData,
    id: string,
    currentUser?: CurrentUserArg,
  ): Promise<unknown>;
  remove(id: string, currentUser?: CurrentUserArg): Promise<void>;
  deleteByIds(ids: string[], currentUser?: CurrentUserArg): Promise<void>;
  list(
    filter: Request['query'],
    globalAccess: boolean,
    currentUser?: CurrentUserArg,
  ): Promise<{ rows: object[]; count: number }>;
  count(
    filter: Request['query'],
    globalAccess: boolean,
    currentUser?: CurrentUserArg,
  ): Promise<unknown>;
  autocomplete(
    query: string | undefined,
    limit: number | undefined,
    offset: number | undefined,
    globalAccess: boolean,
    organizationId?: string,
  ): Promise<unknown>;
  findById(id: string, currentUser?: CurrentUserArg): Promise<unknown>;
}

function globalAccessOf(req: Request): boolean {
  return req.currentUser?.app_role?.globalAccess ?? false;
}

/**
 * Builds the standard 9 generic-CRUD HTTP handlers (API layer) for a service.
 * `csvFields` selects the columns the `?filetype=csv` list export emits.
 */
export function createCrudController<CreateData, UpdateData>(
  service: CrudControllerService<CreateData, UpdateData>,
  { csvFields }: { csvFields: string[] },
) {
  return {
    async create(req: Request, res: Response): Promise<void> {
      await service.create(req.body.data, req.currentUser);
      res.status(200).send(true);
    },

    async bulkImport(req: Request, res: Response): Promise<void> {
      await processFile(req, res);
      if (!req.file) {
        throw new ValidationError('importer.errors.invalidFileEmpty');
      }
      await service.bulkImport(req.file.buffer, req.currentUser);
      res.status(200).send(true);
    },

    async update(req: Request, res: Response): Promise<void> {
      await service.update(req.body.data, req.body.id, req.currentUser);
      res.status(200).send(true);
    },

    async remove(req: Request, res: Response): Promise<void> {
      await service.remove(paramStr(req.params.id), req.currentUser);
      res.status(200).send(true);
    },

    async deleteByIds(req: Request, res: Response): Promise<void> {
      await service.deleteByIds(req.body.data, req.currentUser);
      res.status(200).send(true);
    },

    async list(req: Request, res: Response): Promise<void> {
      const payload = await service.list(
        req.query,
        globalAccessOf(req),
        req.currentUser,
      );

      if (req.query.filetype === 'csv') {
        const csv = toCsv(payload.rows, csvFields);
        res.status(200).attachment(csv);
        res.send(csv);
      } else {
        res.status(200).send(payload);
      }
    },

    async count(req: Request, res: Response): Promise<void> {
      const payload = await service.count(
        req.query,
        globalAccessOf(req),
        req.currentUser,
      );
      res.status(200).send(payload);
    },

    async autocomplete(req: Request, res: Response): Promise<void> {
      const payload = await service.autocomplete(
        queryStr(req.query.query),
        queryNum(req.query.limit),
        queryNum(req.query.offset),
        globalAccessOf(req),
        req.currentUser?.organizationId ?? undefined,
      );
      res.status(200).send(payload);
    },

    async findById(req: Request, res: Response): Promise<void> {
      const payload = await service.findById(
        paramStr(req.params.id),
        req.currentUser,
      );
      res.status(200).send(payload);
    },
  };
}

export type CrudController = ReturnType<typeof createCrudController>;
