import {
	PublicApiListDataTableQueryDto,
	PublicApiCreateDataTableDto,
	UpdateDataTableDto,
} from '@n8n/api-types';
import { Container } from '@n8n/di';

import type { DataTableRequest } from '../../../types';
import type { PublicAPIEndpoint } from '../../shared/handler.types';
import {
	publicApiScope,
	projectScope,
	validCursor,
} from '../../shared/middlewares/global.middleware';
import { encodeNextCursor } from '../../shared/services/pagination.service';

import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ConflictError } from '@/errors/response-errors/conflict.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { DataTableAggregateService } from '@/modules/data-table/data-table-aggregate.service';
import { DataTableService } from '@/modules/data-table/data-table.service';
import { DataTableAccessDeniedError } from '@/modules/data-table/errors/data-table-access-denied.error';
import { DataTableNameConflictError } from '@/modules/data-table/errors/data-table-name-conflict.error';
import { DataTableNotFoundError } from '@/modules/data-table/errors/data-table-not-found.error';
import { DataTableValidationError } from '@/modules/data-table/errors/data-table-validation.error';
import { ProjectNotFoundError } from '@/services/project.service.ee';

const handleError = (error: unknown) => {
	if (error instanceof DataTableValidationError) {
		throw new BadRequestError(error.message);
	}
	if (error instanceof ProjectNotFoundError) {
		throw new BadRequestError(`Project with ID "${error.projectId}" not found`);
	}
	if (error instanceof DataTableNotFoundError) {
		throw new NotFoundError(error.message);
	}
	if (error instanceof DataTableAccessDeniedError) {
		throw new ForbiddenError();
	}
	if (error instanceof DataTableNameConflictError) {
		throw new ConflictError(error.message);
	}

	throw error;
};

/**
 * Convert all query parameter values to strings for DTO validation.
 * Express/Supertest may parse some values as numbers/booleans.
 */
const stringifyQuery = (query: Record<string, unknown>): Record<string, string | undefined> => {
	const result: Record<string, string | undefined> = {};
	for (const [key, value] of Object.entries(query)) {
		if (value !== undefined && value !== null) {
			result[key] = String(value);
		}
	}
	return result;
};

function toPublicDataTable<T extends { project?: unknown }>(dataTable: T) {
	const { project: _project, ...rest } = dataTable;
	return rest;
}

type DataTableHandlers = {
	listDataTables: PublicAPIEndpoint<DataTableRequest.List>;
	createDataTable: PublicAPIEndpoint<DataTableRequest.Create>;
	getDataTable: PublicAPIEndpoint<DataTableRequest.Get>;
	updateDataTable: PublicAPIEndpoint<DataTableRequest.Update>;
	deleteDataTable: PublicAPIEndpoint<DataTableRequest.Delete>;
};

const dataTableHandlers: DataTableHandlers = {
	listDataTables: [
		publicApiScope('dataTable:list'),
		validCursor,
		async (req, res) => {
			try {
				const payload = PublicApiListDataTableQueryDto.safeParse(stringifyQuery(req.query));
				if (!payload.success) {
					throw new BadRequestError(payload.error.errors[0]?.message || 'Invalid query parameters');
				}

				const { offset, limit, filter, sortBy } = payload.data;

				const result = await Container.get(DataTableAggregateService).getManyAndCount(req.user, {
					skip: offset,
					take: limit,
					filter,
					sortBy,
				});

				const data = result.data.map(toPublicDataTable);

				return res.json({
					data,
					nextCursor: encodeNextCursor({
						offset,
						limit,
						numberOfTotalRecords: result.count,
					}),
				});
			} catch (error) {
				return handleError(error);
			}
		},
	],

	createDataTable: [
		publicApiScope('dataTable:create'),
		async (req, res) => {
			const payload = PublicApiCreateDataTableDto.safeParse(req.body);
			if (!payload.success) {
				throw new BadRequestError(payload.error.errors[0]?.message || 'Invalid request body');
			}

			const { projectId, name, columns, fileId, hasHeaders } = payload.data;
			const dataTableService = Container.get(DataTableService);

			try {
				const owningProjectId = await dataTableService.resolveOwningProjectId(req.user, projectId);
				const result = await dataTableService.createDataTable(owningProjectId, {
					name,
					columns,
					fileId,
					hasHeaders,
				});

				return res.status(201).json(toPublicDataTable(result));
			} catch (error) {
				return handleError(error);
			}
		},
	],

	getDataTable: [
		publicApiScope('dataTable:read'),
		projectScope('dataTable:read', 'dataTable'),
		async (req, res) => {
			try {
				const { dataTableId } = req.params;

				const projectId =
					await Container.get(DataTableService).getProjectIdForDataTable(dataTableId);

				const result = await Container.get(DataTableService).getOne(dataTableId, projectId);

				return res.json(toPublicDataTable(result));
			} catch (error) {
				return handleError(error);
			}
		},
	],

	updateDataTable: [
		publicApiScope('dataTable:update'),
		projectScope('dataTable:update', 'dataTable'),
		async (req, res) => {
			try {
				const { dataTableId } = req.params;

				const payload = UpdateDataTableDto.safeParse(req.body);
				if (!payload.success) {
					throw new BadRequestError(payload.error.errors[0]?.message || 'Invalid request body');
				}

				const dataTableService = Container.get(DataTableService);
				const projectId = await dataTableService.getProjectIdForDataTable(dataTableId);

				await dataTableService.updateDataTable(dataTableId, projectId, payload.data);

				const result = await dataTableService.getOne(dataTableId, projectId);

				return res.json(toPublicDataTable(result));
			} catch (error) {
				return handleError(error);
			}
		},
	],

	deleteDataTable: [
		publicApiScope('dataTable:delete'),
		projectScope('dataTable:delete', 'dataTable'),
		async (req, res) => {
			try {
				const { dataTableId } = req.params;

				const projectId =
					await Container.get(DataTableService).getProjectIdForDataTable(dataTableId);

				await Container.get(DataTableService).deleteDataTable(dataTableId, projectId);

				return res.status(204).send();
			} catch (error) {
				return handleError(error);
			}
		},
	],
};

export = dataTableHandlers;
