const csv = require('csv-parser');
const stream = require('stream');

const processFile = require('../middlewares/upload');
const ValidationError = require('./notifications/errors/validation');

const ALLOWED_CSV_MIME_TYPES = new Set([
  'text/csv',
  'application/csv',
  'application/vnd.ms-excel',
  'text/plain',
  'application/octet-stream',
]);

const isCsvFilename = (filename = '') => filename.toLowerCase().endsWith('.csv');

const isBlankRow = (row = {}) =>
  Object.values(row).every((value) => value === undefined || value === null || String(value).trim() === '');

const createCsvValidationError = (message) => {
  const error = new Error(message);
  error.code = 400;
  return error;
};

const splitCsvHeaderLine = (line = '') => {
  const headers = [];
  let current = '';
  let insideQuotes = false;

  for (let index = 0; index < line.length; index += 1) {
    const character = line[index];
    const nextCharacter = line[index + 1];

    if (character === '"') {
      if (insideQuotes && nextCharacter === '"') {
        current += '"';
        index += 1;
      } else {
        insideQuotes = !insideQuotes;
      }
      continue;
    }

    if (character === ',' && !insideQuotes) {
      headers.push(current.trim());
      current = '';
      continue;
    }

    current += character;
  }

  headers.push(current.trim());

  return headers
    .map((header) => header.replace(/^\uFEFF/, '').trim())
    .filter((header) => header.length);
};

const getCsvHeadersFromBuffer = (buffer) => {
  const fileContents = Buffer.from(buffer).toString('utf8').replace(/^\uFEFF/, '');
  const firstNonEmptyLine = fileContents
    .split(/\r?\n/)
    .find((line) => line.trim().length > 0);

  if (!firstNonEmptyLine) {
    return [];
  }

  return splitCsvHeaderLine(firstNonEmptyLine);
};

const validateCsvHeaders = (headers = [], options = {}) => {
  const allowedHeaders = (options.allowedHeaders || options.columns || []).map((header) => header.trim());
  const requiredHeaders = (options.requiredHeaders || []).map((header) => header.trim());

  if (!allowedHeaders.length) {
    return;
  }

  const unknownHeaders = headers.filter((header) => !allowedHeaders.includes(header));
  const missingRequiredHeaders = requiredHeaders.filter((header) => !headers.includes(header));

  if (!unknownHeaders.length && !missingRequiredHeaders.length) {
    return;
  }

  const details = [];

  if (missingRequiredHeaders.length) {
    details.push(`Missing required columns: ${missingRequiredHeaders.join(', ')}`);
  }

  if (unknownHeaders.length) {
    details.push(`Unexpected columns: ${unknownHeaders.join(', ')}`);
  }

  details.push(`Allowed columns: ${allowedHeaders.join(', ')}`);

  throw createCsvValidationError(details.join('. '));
};

const parseCsvImportFile = async (req, res, options = {}) => {
  await processFile(req, res);

  if (!req.file || !req.file.buffer || !req.file.buffer.length) {
    throw new ValidationError('importer.errors.invalidFileEmpty');
  }

  const filename = req.file.originalname || req.body?.filename || '';
  const mimetype = req.file.mimetype || '';

  if (!isCsvFilename(filename) && !ALLOWED_CSV_MIME_TYPES.has(mimetype)) {
    throw new ValidationError('importer.errors.invalidFileUpload');
  }

  const csvHeaders = getCsvHeadersFromBuffer(req.file.buffer);
  validateCsvHeaders(csvHeaders, options);

  const bufferStream = new stream.PassThrough();
  const results = [];

  bufferStream.end(Buffer.from(req.file.buffer));

  await new Promise((resolve, reject) => {
    bufferStream
      .pipe(
        csv({
          mapHeaders: ({ header }) => (header ? header.trim() : header),
          mapValues: ({ value }) => (typeof value === 'string' ? value.trim() : value),
        }),
      )
      .on('data', (data) => {
        if (!isBlankRow(data)) {
          results.push(data);
        }
      })
      .on('end', resolve)
      .on('error', reject);
  });

  if (!results.length) {
    throw new ValidationError('importer.errors.invalidFileEmpty');
  }

  return results;
};

module.exports = {
  parseCsvImportFile,
};
