import type {
  CsvParsedFile,
  CsvParsedRow,
  CsvPreviewError,
  CsvPreviewWarning,
  CsvTemplateDefinition
} from './types';

function buildIssue(
  file: string,
  row: number,
  column: string,
  code: string,
  message: string,
  suggestedFix: string
): CsvPreviewError {
  return { file, row, column, code, message, suggestedFix };
}

function decodeUtf8(fileName: string, bytes: Uint8Array): {
  text: string;
  errors: CsvPreviewError[];
  warnings: CsvPreviewWarning[];
} {
  const errors: CsvPreviewError[] = [];
  const warnings: CsvPreviewWarning[] = [];

  if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
    warnings.push(
      buildIssue(
        fileName,
        1,
        '__file__',
        'UTF8_BOM',
        'CSV file contains a UTF-8 BOM.',
        'Export the CSV as UTF-8 without BOM when possible.'
      )
    );
  }

  try {
    return {
      text: new TextDecoder('utf-8', { fatal: true }).decode(bytes).replace(/^\uFEFF/, ''),
      errors,
      warnings
    };
  } catch {
    errors.push(
      buildIssue(
        fileName,
        1,
        '__file__',
        'INVALID_ENCODING',
        'CSV file is not valid UTF-8.',
        'Save the file as UTF-8 CSV and upload it again.'
      )
    );
    return { text: '', errors, warnings };
  }
}

function parseCsvText(text: string): string[][] {
  const rows: string[][] = [];
  let currentRow: string[] = [];
  let currentCell = '';
  let inQuotes = false;

  for (let index = 0; index < text.length; index += 1) {
    const char = text[index];
    const next = text[index + 1];

    if (char === '"') {
      if (inQuotes && next === '"') {
        currentCell += '"';
        index += 1;
      } else {
        inQuotes = !inQuotes;
      }
      continue;
    }

    if (char === ',' && !inQuotes) {
      currentRow.push(currentCell);
      currentCell = '';
      continue;
    }

    if ((char === '\n' || char === '\r') && !inQuotes) {
      if (char === '\r' && next === '\n') index += 1;
      currentRow.push(currentCell);
      rows.push(currentRow);
      currentRow = [];
      currentCell = '';
      continue;
    }

    currentCell += char;
  }

  if (currentCell.length > 0 || currentRow.length > 0) {
    currentRow.push(currentCell);
    rows.push(currentRow);
  }

  return rows;
}

export function parseCsvFile(input: {
  fileName: string;
  bytes: Uint8Array;
  template: CsvTemplateDefinition;
}): CsvParsedFile {
  const decoded = decodeUtf8(input.fileName, input.bytes);
  const errors = [...decoded.errors];
  const warnings = [...decoded.warnings];

  if (decoded.errors.length > 0) {
    return {
      fileName: input.fileName,
      template: input.template,
      headers: [],
      rows: [],
      errors,
      warnings
    };
  }

  const parsedRows = parseCsvText(decoded.text);
  const [headerRow, ...dataRows] = parsedRows;

  if (!headerRow) {
    errors.push(
      buildIssue(
        input.fileName,
        1,
        '__header__',
        'EMPTY_FILE',
        'CSV file is empty.',
        'Upload a CSV file with the official template header row.'
      )
    );
  }

  const headers = (headerRow ?? []).map((header) => header.trim());
  const rows: CsvParsedRow[] = [];
  const rowFingerprints = new Map<string, number>();

  dataRows.forEach((raw, index) => {
    const rowNumber = index + 2;
    const isEmpty = raw.every((cell) => cell.trim() === '');

    if (isEmpty) {
      warnings.push(
        buildIssue(
          input.fileName,
          rowNumber,
          '__row__',
          'EMPTY_ROW',
          'CSV row is empty and will be skipped.',
          'Remove empty rows from the file.'
        )
      );
      return;
    }

    if (raw.length !== headers.length) {
      errors.push(
        buildIssue(
          input.fileName,
          rowNumber,
          '__row__',
          'INCONSISTENT_COLUMN_COUNT',
          `Row has ${raw.length} columns but header has ${headers.length}.`,
          'Fix quoting or column separators so every row matches the template header.'
        )
      );
    }

    const fingerprint = raw.map((cell) => cell.trim()).join('\u001f');
    const firstRow = rowFingerprints.get(fingerprint);
    if (firstRow) {
      warnings.push(
        buildIssue(
          input.fileName,
          rowNumber,
          '__row__',
          'DUPLICATE_ROW',
          `Row duplicates row ${firstRow}.`,
          'Remove duplicate rows or confirm that the later row is intentional.'
        )
      );
    } else {
      rowFingerprints.set(fingerprint, rowNumber);
    }

    const values = headers.reduce<Record<string, string>>((accumulator, header, cellIndex) => {
      accumulator[header] = (raw[cellIndex] ?? '').trim();
      return accumulator;
    }, {});

    rows.push({ rowNumber, values, raw });
  });

  return {
    fileName: input.fileName,
    template: input.template,
    headers,
    rows,
    errors,
    warnings
  };
}
