(function initPortfolioTransactionsUpload(global) {
  const portfolioSelection = global.PortfolioSelection;
  if (!portfolioSelection) {
    throw new Error('PortfolioSelection must be loaded before PortfolioTransactionsUpload.');
  }

  const {
    usePortfolios,
    PortfolioSelectField,
    getAuthorizedJsonHeaders,
    handleUnauthorizedApiResponse
  } = portfolioSelection;

  const sharedComponents = global.PortfolioSharedComponents || {};
  const BlockingScreenLoader = sharedComponents.BlockingScreenLoader;

  const TRANSACTION_TYPES = {
    stock: 'Stock',
    dividend: 'Dividend',
    cash: 'Cash'
  };

  const UPLOAD_COLUMN_SETS = {
    [TRANSACTION_TYPES.stock]: ['Date', 'Event', 'ISIN', 'Quantity', 'Price', 'Currency', 'Commission', 'TaxPaid', 'Amount', 'FxRate', 'Portfolio'],
    [TRANSACTION_TYPES.dividend]: ['Date', 'Event', 'ISIN', 'Country', 'Dividend', 'TaxPaid', 'Received', 'Currency', 'FxRate', 'Description', 'Portfolio'],
    [TRANSACTION_TYPES.cash]: ['Date', 'Event', 'Amount', 'Portfolio']
  };

  const OPTIONAL_UPLOAD_COLUMNS_BY_TYPE = {
    [TRANSACTION_TYPES.stock]: new Set(['TaxPaid', 'Portfolio']),
    [TRANSACTION_TYPES.dividend]: new Set(['Event', 'ISIN', 'Description', 'Portfolio']),
    [TRANSACTION_TYPES.cash]: new Set(['Portfolio'])
  };
  const IDENTITY_UPLOAD_COLUMNS = new Set(['ISIN']);
  const ZERO_COST_STOCK_EVENTS = new Set(['Split', 'TransferIn', 'TransferOut']);
  const UPLOAD_SOURCES = {
    DEGIRO_TRANSACTION: 'Degiro Transaction',
    DEGIRO_ACCOUNT_STATEMENT: 'Degiro Account Statement',
    CUSTOM_UPLOAD: 'Custom Upload'
  };
  const INVALID_DEGIRO_FILE_MESSAGE = 'Invalid Degiro file format. Only Transaction or Account Statement files are supported.';

  const DEGIRO_TRANSACTION_HEADERS = [
    'Date',
    'Time',
    'Product',
    'ISIN',
    'Reference exchange',
    'Venue',
    'Quantity',
    'Price',
    '',
    'Local value',
    '',
    'Value EUR',
    'Exchange rate',
    'AutoFX Fee',
    'Transaction and/or third party fees EUR',
    'Total EUR',
    'Order ID'
  ];

  const DEGIRO_ACCOUNT_HEADERS = [
    'Date',
    'Time',
    'Value date',
    'Product',
    'ISIN',
    'Description',
    'FX',
    'Change',
    '',
    'Balance',
    '',
    'Order Id'
  ];

  function parseCsvLine(line) {
    const cells = [];
    let current = '';
    let inQuotes = false;

    for (let index = 0; index < line.length; index += 1) {
      const char = line[index];

      if (char === '"') {
        if (inQuotes && line[index + 1] === '"') {
          current += '"';
          index += 1;
        } else {
          inQuotes = !inQuotes;
        }
        continue;
      }

      if (char === ',' && !inQuotes) {
        cells.push(current);
        current = '';
        continue;
      }

      current += char;
    }

    cells.push(current);
    return cells;
  }

  function parseDelimitedLine(line, delimiter) {
    if (!delimiter || delimiter === ',') {
      return parseCsvLine(line);
    }

    const cells = [];
    let current = '';
    let inQuotes = false;

    for (let index = 0; index < line.length; index += 1) {
      const char = line[index];

      if (char === '"') {
        if (inQuotes && line[index + 1] === '"') {
          current += '"';
          index += 1;
        } else {
          inQuotes = !inQuotes;
        }
        continue;
      }

      if (char === delimiter && !inQuotes) {
        cells.push(current);
        current = '';
        continue;
      }

      current += char;
    }

    cells.push(current);
    return cells;
  }

  function normalizeHeaderCell(value) {
    return String(value || '').trim().toLowerCase();
  }

  function normalizeDateSeparators(value) {
    const trimmed = String(value || '').trim();
    if (!trimmed) {
      return '';
    }

    return trimmed.replace(/\//g, '-');
  }

  function trimTrailingEmptyCells(cells) {
    const next = Array.isArray(cells) ? cells.slice() : [];
    while (next.length > 0 && String(next[next.length - 1] || '').trim() === '') {
      next.pop();
    }
    return next;
  }

  function headersMatch(actualHeaderCells, expectedHeaderCells) {
    const normalizedActual = trimTrailingEmptyCells(actualHeaderCells);
    const normalizedExpected = trimTrailingEmptyCells(expectedHeaderCells);

    if (!Array.isArray(normalizedActual) || normalizedActual.length !== normalizedExpected.length) {
      return false;
    }

    return normalizedExpected.every((expectedValue, index) => (
      normalizeHeaderCell(normalizedActual[index]) === normalizeHeaderCell(expectedValue)
    ));
  }

  function normalizeCustomIdentityValue(type, value) {
    const trimmed = String(value || '').trim();
    if (!trimmed) {
      return '';
    }

    if (type === 'Name') {
      return trimmed.toLowerCase();
    }

    return trimmed.toUpperCase();
  }

  function getPreferredCustomIdentityColumn(columns) {
    const values = Array.isArray(columns) ? columns : [];
    return values.find((column) => IDENTITY_UPLOAD_COLUMNS.has(column)) || '';
  }

  function resolvePortfolioIdByName(portfolios, name) {
    const trimmed = String(name || '').trim().toLowerCase();
    if (!trimmed) {
      return null;
    }

    const values = Array.isArray(portfolios) ? portfolios : [];
    const match = values.find((portfolio) => String((portfolio && portfolio.name) || '').trim().toLowerCase() === trimmed);
    return match ? String(match.id) : null;
  }

  function detectCustomInputDelimiter(firstLine) {
    if (String(firstLine || '').includes('\t')) {
      return '\t';
    }

    if (String(firstLine || '').includes(';')) {
      return ';';
    }

    return ',';
  }

  function isCustomHeaderRow(cells, columns) {
    const expectedColumns = Array.isArray(columns) ? columns : [];
    const normalizedActual = trimTrailingEmptyCells(cells).map((cell) => normalizeHeaderCell(cell));
    const normalizedExpected = expectedColumns.map((column) => normalizeHeaderCell(column));

    if (normalizedActual.length !== normalizedExpected.length) {
      return false;
    }

    return expectedColumns.every((column, index) => {
      if (column === 'Ignore') {
        // Ignore columns are placeholders and should match any header cell.
        return true;
      }

      return normalizedActual[index] === normalizedExpected[index];
    });
  }

  function getMissingColumnsFromLikelyHeaderRow(cells, selectedColumns) {
    const visibleColumns = (Array.isArray(selectedColumns) ? selectedColumns : []).filter((column) => column !== 'Ignore');
    if (visibleColumns.length === 0) {
      return {
        isLikelyHeader: false,
        missingColumns: []
      };
    }

    const normalizedActualCells = trimTrailingEmptyCells(cells).map((cell) => normalizeHeaderCell(cell));
    const normalizedActualSet = new Set(normalizedActualCells.filter(Boolean));
    const normalizedExpected = visibleColumns.map((column) => normalizeHeaderCell(column));
    const matchedColumns = normalizedExpected.filter((column) => normalizedActualSet.has(column));
    const missingColumns = visibleColumns.filter((column) => !normalizedActualSet.has(normalizeHeaderCell(column)));

    const hasHeaderKeyword = normalizedActualSet.has('date') || normalizedActualSet.has('event') || normalizedActualSet.has('isin');
    const isLikelyHeader = hasHeaderKeyword && matchedColumns.length >= 2;

    return {
      isLikelyHeader,
      missingColumns
    };
  }

  function normalizeHeaderToCustomColumn(headerCell, customType) {
    const normalized = normalizeHeaderCell(headerCell);
    if (!normalized) {
      return 'Ignore';
    }

    const knownColumns = UPLOAD_COLUMN_SETS[customType] || [];
    const directMatch = knownColumns.find((column) => normalizeHeaderCell(column) === normalized);
    return directMatch || 'Ignore';
  }

  function buildCorrectedColumnsFromHeaderCells(headerCells, customType) {
    const normalizedHeaderCells = trimTrailingEmptyCells(Array.isArray(headerCells) ? headerCells : []);
    const usedColumns = new Set();
    const correctedColumns = normalizedHeaderCells.map((cell) => {
      const mapped = normalizeHeaderToCustomColumn(cell, customType);
      if (mapped === 'Ignore') {
        return 'Ignore';
      }

      if (usedColumns.has(mapped)) {
        return 'Ignore';
      }

      usedColumns.add(mapped);
      return mapped;
    });

    const optionalColumns = OPTIONAL_UPLOAD_COLUMNS_BY_TYPE[customType] || new Set();
    const requiredColumns = (UPLOAD_COLUMN_SETS[customType] || []).filter((column) => !optionalColumns.has(column));
    const missingColumns = requiredColumns.filter((column) => !usedColumns.has(column));

    if (customType === TRANSACTION_TYPES.dividend && !usedColumns.has('ISIN') && !usedColumns.has('Description')) {
      missingColumns.push('ISIN or Description');
    }

    return {
      correctedColumns,
      missingColumns
    };
  }

  function guessCustomUploadLayoutFromHeaderCells(headerCells) {
    const normalizedHeaderCells = trimTrailingEmptyCells(headerCells)
      .map((cell) => normalizeHeaderCell(cell))
      .filter(Boolean);

    if (normalizedHeaderCells.length === 0) {
      return null;
    }

    const headerCellSet = new Set(normalizedHeaderCells);
    const hasDividendSignals = headerCellSet.has('country') || headerCellSet.has('received') || headerCellSet.has('dividend') || headerCellSet.has('dividend tax');
    const hasCashSignals = headerCellSet.has('amount')
      && normalizedHeaderCells.length <= 4
      && !headerCellSet.has('quantity')
      && !headerCellSet.has('price');
    const type = hasDividendSignals
      ? TRANSACTION_TYPES.dividend
      : (hasCashSignals ? TRANSACTION_TYPES.cash : TRANSACTION_TYPES.stock);

    const normalizedKnownColumns = new Map(
      (UPLOAD_COLUMN_SETS[type] || []).map((column) => [normalizeHeaderCell(column), column])
    );

    const guessedColumns = normalizedHeaderCells.map((cell) => (
      normalizedKnownColumns.has(cell) ? normalizedKnownColumns.get(cell) : 'Ignore'
    ));

    return {
      type,
      columns: guessedColumns
    };
  }

  function parseCustomUploadTextRows(text, customType, customColumns, portfolios) {
    const selectedColumns = Array.isArray(customColumns) ? customColumns.slice() : [];
    const visibleColumns = selectedColumns.filter((column) => column !== 'Ignore');
    const rawLines = String(text || '')
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter(Boolean);

    if (!customType || selectedColumns.length === 0 || rawLines.length === 0) {
      return {
        visibleColumns,
        parsedRows: [],
        failedRawRows: [],
        lookupType: getPreferredCustomIdentityColumn(selectedColumns),
        lookupIdentifiers: []
      };
    }

    const delimiter = detectCustomInputDelimiter(rawLines[0]);
    const firstLineCells = parseDelimitedLine(rawLines[0], delimiter);
    const exactHeaderMatch = isCustomHeaderRow(firstLineCells, selectedColumns);
    const likelyHeader = getMissingColumnsFromLikelyHeaderRow(firstLineCells, selectedColumns).isLikelyHeader;
    const hasHeaderRow = exactHeaderMatch || likelyHeader;
    const contentLines = hasHeaderRow ? rawLines.slice(1) : rawLines.slice();
    const lookupType = getPreferredCustomIdentityColumn(selectedColumns);
    const lookupIdentifiers = [];
    const parsedRows = [];
    const failedRawRows = [];

    const toVisibleValueMap = (cells) => {
      const valuesByColumn = {};
      for (let index = 0; index < selectedColumns.length; index += 1) {
        const column = selectedColumns[index];
        if (column === 'Ignore') {
          continue;
        }

        const rawValue = String(cells[index] || '').trim();
        valuesByColumn[column] = column === 'Date'
          ? normalizeDateSeparators(rawValue)
          : rawValue;
      }
      return valuesByColumn;
    };

    const hasExtraCells = (cells) => cells.length > selectedColumns.length
      && cells.slice(selectedColumns.length).some((value) => String(value || '').trim() !== '');

    for (const rawCsv of contentLines) {
      const cells = parseDelimitedLine(rawCsv, delimiter).map((cell) => String(cell || '').trim());
      if (cells.length === 0 || cells.every((cell) => cell === '')) {
        continue;
      }

      if (hasExtraCells(cells)) {
        failedRawRows.push(rawCsv);
        continue;
      }

      const valuesByColumn = toVisibleValueMap(cells.length < selectedColumns.length
        ? cells.concat(Array(selectedColumns.length - cells.length).fill(''))
        : cells);

      const identityValue = lookupType ? String(valuesByColumn[lookupType] || '').trim() : '';
      const previewValues = Object.assign({}, valuesByColumn);

      let portfolioId = null;
      if (selectedColumns.includes('Portfolio')) {
        portfolioId = resolvePortfolioIdByName(portfolios, valuesByColumn.Portfolio);
        if (!portfolioId) {
          failedRawRows.push(rawCsv);
          continue;
        }
      }

      if (customType === TRANSACTION_TYPES.stock) {
        const date = valuesByColumn.Date;
        const event = valuesByColumn.Event;
        const normalizedEvent = String(event || '').trim().toLowerCase().replace(/\s+/g, '');
        const name = valuesByColumn.ISIN || '';
        const isin = valuesByColumn.ISIN || '';
        const quantity = parseNumberValue(valuesByColumn.Quantity);
        const price = parseNumberValue(valuesByColumn.Price);
        const currency = String(valuesByColumn.Currency || '').trim().toUpperCase();
        const commissionField = parseOptionalNumberField(valuesByColumn.Commission, 0);
        const taxPaidField = parseOptionalNumberField(valuesByColumn.TaxPaid, 0);
        const fxRateField = parseOptionalNumberField(valuesByColumn.FxRate, 1);
        let amount = parseNumberValue(valuesByColumn.Amount);
        if (amount == null && normalizedEvent === 'writeoff') {
          amount = 0;
        }

        if (
          !date
          || !event
          || !name
          || !quantity
          || !Number.isFinite(price)
          || !currency
          || amount == null
          || commissionField.invalid
          || taxPaidField.invalid
          || fxRateField.invalid
        ) {
          failedRawRows.push(rawCsv);
          continue;
        }

        const commissionRaw = commissionField.value;
        const taxPaidRaw = taxPaidField.value;
        const fxRate = fxRateField.value;

        // Optional FxFee column support — parse if provided in the visible values
        const fxFeeField = parseOptionalNumberField(valuesByColumn.FxFee, 0);
        const fxFeeRaw = fxFeeField.value;

        // Normalise fee signs: fees should be negative. If a positive value is provided, convert to negative.
        const commission = Number.isFinite(commissionRaw) ? (commissionRaw > 0 ? -Math.abs(commissionRaw) : commissionRaw) : commissionRaw;
        const taxPaid = Number.isFinite(taxPaidRaw) ? (taxPaidRaw > 0 ? -Math.abs(taxPaidRaw) : taxPaidRaw) : taxPaidRaw;
        const fxFee = Number.isFinite(fxFeeRaw) ? (fxFeeRaw > 0 ? -Math.abs(fxFeeRaw) : fxFeeRaw) : fxFeeRaw;

        // Normalise amount sign based on event: Buys should be negative amounts, Sells positive amounts.
        let normalizedAmount = amount;
        try {
          if (normalizedAmount != null) {
            if (normalizedEvent === 'buy' && normalizedAmount > 0) {
              normalizedAmount = -Math.abs(normalizedAmount);
            } else if (normalizedEvent === 'sell' && normalizedAmount < 0) {
              normalizedAmount = Math.abs(normalizedAmount);
            }
          }
        } catch (e) {
          // noop — fall back to original amount on any error
          normalizedAmount = amount;
        }

        parsedRows.push({
          type: TRANSACTION_TYPES.stock,
          rawCsv,
          portfolioId,
          previewValues,
          securityIdentifierType: lookupType,
          securityIdentifier: identityValue,
          date,
          event,
          name,
          isin,
          mic: null,
          // Ensure quantity sign matches the event: Buy/TransferIn positive, Sell/TransferOut negative
          quantity: (function () {
            const q = Number.parseInt(quantity, 10);
            if (!Number.isFinite(q)) return q;
            const absQ = Math.abs(q);
            if (normalizedEvent === 'sell' || normalizedEvent === 'transferout') {
              return -Math.abs(absQ);
            }
            if (normalizedEvent === 'buy' || normalizedEvent === 'transferin') {
              return Math.abs(absQ);
            }
            return q;
          }()),
          price,
          currency,
          fxFee: fxFee,
          fxRatePreFee: null,
          fxRate,
          commission,
          taxPaid,
          amount: normalizedAmount,
          orderId: null
        });
        if (identityValue) {
          lookupIdentifiers.push(identityValue);
        }
        continue;
      }

      if (customType === TRANSACTION_TYPES.dividend) {
        const date = valuesByColumn.Date;
        const event = normalizeCustomDividendEvent(valuesByColumn.Event);
        const isin = valuesByColumn.ISIN || '';
        const descriptionValue = valuesByColumn.Description || '';
        const name = isin || descriptionValue;
        const country = String(valuesByColumn.Country || '').trim().toUpperCase();
        const declaredInSourceCurrency = parseNumberValue(valuesByColumn.Dividend);
        const taxPaidInSourceCurrency = parseNumberValue(valuesByColumn.TaxPaid);
        const amountInSourceCurrency = parseNumberValue(valuesByColumn.Received);
        const currency = String(valuesByColumn.Currency || '').trim().toUpperCase();
        const fxRateProvided = String(valuesByColumn.FxRate || '').trim() !== '';
        const fxRate = fxRateProvided ? parseNumberValue(valuesByColumn.FxRate) : 1;

        if (
          !date
          || !event
          || (!isin && !descriptionValue)
          || !country
          || declaredInSourceCurrency == null
          || taxPaidInSourceCurrency == null
          || amountInSourceCurrency == null
          || !currency
          || fxRate == null
        ) {
          failedRawRows.push(rawCsv);
          continue;
        }

        parsedRows.push({
          type: TRANSACTION_TYPES.dividend,
          rawCsv,
          portfolioId,
          previewValues,
          securityIdentifierType: lookupType,
          securityIdentifier: identityValue,
          date,
          event,
          name,
          isin,
          description: descriptionValue,
          country,
          declaredInSourceCurrency,
          taxPaidInSourceCurrency,
          amountInSourceCurrency,
          currency,
          fxRate,
          declaredConverted: fxRate !== 0 ? declaredInSourceCurrency / fxRate : declaredInSourceCurrency,
          taxPaidConverted: fxRate !== 0 ? taxPaidInSourceCurrency / fxRate : taxPaidInSourceCurrency,
          amount: fxRate !== 0 ? amountInSourceCurrency / fxRate : amountInSourceCurrency
        });
        if (identityValue) {
          lookupIdentifiers.push(identityValue);
        }
        continue;
      }

      if (customType === TRANSACTION_TYPES.cash) {
        const date = normalizeCustomUploadDate(valuesByColumn.Date);
        const event = normalizeCustomCashEvent(valuesByColumn.Event);
        const amount = parseNumberValue(valuesByColumn.Amount);
        if (!date || !event || amount == null) {
          failedRawRows.push(rawCsv);
          continue;
        }

        parsedRows.push({
          type: TRANSACTION_TYPES.cash,
          rawCsv,
          portfolioId,
          previewValues,
          date,
          event,
          description: event,
          isin: null,
          amount
        });
        continue;
      }

      failedRawRows.push(rawCsv);
    }

    return {
      visibleColumns,
      parsedRows,
      failedRawRows,
      lookupType,
      lookupIdentifiers: Array.from(new Set(lookupIdentifiers.map((value) => normalizeCustomIdentityValue(lookupType, value)).filter(Boolean)))
    };
  }

  function explainCustomRowParseFailure(rawCsv, customType, customColumns, portfolios) {
    const selectedColumns = Array.isArray(customColumns) ? customColumns : [];
    const delimiter = detectCustomInputDelimiter(rawCsv);
    const cells = parseDelimitedLine(rawCsv, delimiter).map((cell) => String(cell || '').trim());

    if (cells.length > selectedColumns.length && cells.slice(selectedColumns.length).some((value) => value !== '')) {
      return `the row has more values (${cells.length}) than expected columns (${selectedColumns.length}).`;
    }

    const valuesByColumn = {};
    selectedColumns.forEach((column, index) => {
      if (column === 'Ignore') {
        return;
      }
      const rawValue = String(cells[index] || '').trim();
      valuesByColumn[column] = column === 'Date' ? normalizeDateSeparators(rawValue) : rawValue;
    });

    if (selectedColumns.includes('Portfolio')) {
      const portfolioValue = valuesByColumn.Portfolio;
      if (!portfolioValue) {
        return 'the Portfolio column is blank.';
      }
      if (!resolvePortfolioIdByName(portfolios, portfolioValue)) {
        return `the Portfolio "${portfolioValue}" does not match any of your portfolios.`;
      }
    }

    if (!valuesByColumn.Date) {
      return 'the Date column is blank.';
    }

    if (customType === TRANSACTION_TYPES.stock) {
      if (!valuesByColumn.Event) {
        return 'the Event column is blank.';
      }
      if (!valuesByColumn.ISIN) {
        return 'the ISIN column is blank.';
      }
      if (parseNumberValue(valuesByColumn.Quantity) == null) {
        return 'the Quantity column is blank or not a valid number.';
      }
      if (!Number.isFinite(parseNumberValue(valuesByColumn.Price))) {
        return 'the Price column is blank or not a valid number.';
      }
      if (!valuesByColumn.Currency) {
        return 'the Currency column is blank.';
      }
      if (parseOptionalNumberField(valuesByColumn.Commission, 0).invalid) {
        return 'the Commission column is not a valid number.';
      }
      if (parseOptionalNumberField(valuesByColumn.TaxPaid, 0).invalid) {
        return 'the TaxPaid column is not a valid number.';
      }
      if (parseOptionalNumberField(valuesByColumn.FxRate, 1).invalid) {
        return 'the FxRate column is not a valid number.';
      }
      const normalizedEvent = String(valuesByColumn.Event || '').trim().toLowerCase().replace(/\s+/g, '');
      if (parseNumberValue(valuesByColumn.Amount) == null && normalizedEvent !== 'writeoff') {
        return 'the Amount column is blank or not a valid number.';
      }
      return 'the row does not match the expected Stock column format.';
    }

    if (customType === TRANSACTION_TYPES.dividend) {
      if (!normalizeCustomDividendEvent(valuesByColumn.Event)) {
        return 'the Event column must be Dividend or CapitalReturn.';
      }
      if (!valuesByColumn.ISIN && !valuesByColumn.Description) {
        return 'either the ISIN or Description column must be provided.';
      }
      if (!valuesByColumn.Country) {
        return 'the Country column is blank.';
      }
      if (parseNumberValue(valuesByColumn.Dividend) == null) {
        return 'the Dividend column is blank or not a valid number.';
      }
      if (parseNumberValue(valuesByColumn.TaxPaid) == null) {
        return 'the TaxPaid column is blank or not a valid number.';
      }
      if (parseNumberValue(valuesByColumn.Received) == null) {
        return 'the Received column is blank or not a valid number.';
      }
      if (!valuesByColumn.Currency) {
        return 'the Currency column is blank.';
      }
      const fxRateProvided = String(valuesByColumn.FxRate || '').trim() !== '';
      if (fxRateProvided && parseNumberValue(valuesByColumn.FxRate) == null) {
        return 'the FxRate column is not a valid number.';
      }
      return 'the row does not match the expected Dividend column format.';
    }

    if (customType === TRANSACTION_TYPES.cash) {
      if (!normalizeCustomUploadDate(valuesByColumn.Date)) {
        return 'the Date column is not a valid date.';
      }
      if (!normalizeCustomCashEvent(valuesByColumn.Event)) {
        return 'the Event column must be one of Deposit, Withdrawal, Interest, ExchangeFee, TransferFee, ADRFee.';
      }
      if (parseNumberValue(valuesByColumn.Amount) == null) {
        return 'the Amount column is blank or not a valid number.';
      }
      return 'the row does not match the expected Cash column format.';
    }

    return 'the row does not match the expected column format.';
  }

  function parseNumberValue(value) {
    const trimmed = String(value || '').trim();
    if (!trimmed) {
      return null;
    }

    const compact = trimmed.replace(/\s/g, '');
    let normalized = compact;
    if (compact.includes(',') && compact.includes('.')) {
      normalized = compact.replace(/,/g, '');
    } else if (compact.includes(',') && !compact.includes('.')) {
      // Interpret comma-only values as thousands when grouped (e.g. 1,000), otherwise decimal (e.g. 1,25).
      const thousandsGrouped = /^-?\d{1,3}(,\d{3})+$/.test(compact);
      normalized = thousandsGrouped ? compact.replace(/,/g, '') : compact.replace(/,/g, '.');
    }

    const numeric = Number.parseFloat(normalized);
    return Number.isFinite(numeric) ? numeric : null;
  }

  function normalizeCustomCashEvent(value) {
    const normalized = String(value || '').trim().toLowerCase().replace(/\s+/g, '');
    if (!normalized) {
      return null;
    }

    if (normalized === 'deposit') {
      return 'Deposit';
    }

    if (normalized === 'withdrawal') {
      return 'Withdrawal';
    }

    if (normalized === 'interest') {
      return 'Interest';
    }

    if (normalized === 'exchangefee') {
      return 'ExchangeFee';
    }

    if (normalized === 'transferfee') {
      return 'TransferFee';
    }

    if (normalized === 'adrfee') {
      return 'ADRFee';
    }

    return null;
  }

  function normalizeCustomDividendEvent(value) {
    const trimmed = String(value || '').trim();
    if (!trimmed) {
      return 'Dividend';
    }

    const normalized = trimmed.toLowerCase().replace(/\s+/g, '');
    if (normalized === 'dividend') {
      return 'Dividend';
    }

    if (normalized === 'capitalreturn') {
      return 'CapitalReturn';
    }

    return null;
  }

  function normalizeCustomUploadDate(value) {
    const raw = String(value || '').trim();
    if (!raw) {
      return null;
    }

    const normalizedSeparators = raw.replace(/\//g, '-');
    const ddmmyyyyMatch = normalizedSeparators.match(/^(\d{1,2})-(\d{1,2})-(\d{4})$/);
    if (ddmmyyyyMatch) {
      const day = Number.parseInt(ddmmyyyyMatch[1], 10);
      const month = Number.parseInt(ddmmyyyyMatch[2], 10);
      const year = Number.parseInt(ddmmyyyyMatch[3], 10);
      const utcDate = new Date(Date.UTC(year, month - 1, day));
      if (
        Number.isFinite(utcDate.getTime())
        && utcDate.getUTCFullYear() === year
        && utcDate.getUTCMonth() === month - 1
        && utcDate.getUTCDate() === day
      ) {
        return utcDate.toISOString().slice(0, 10);
      }
      return null;
    }

    const ordinalDateMatch = normalizedSeparators.match(/^(\d{1,2})(?:st|nd|rd|th)?\s+([A-Za-z]+)\s+(\d{4})$/i);
    if (ordinalDateMatch) {
      const monthMap = {
        jan: 1,
        january: 1,
        feb: 2,
        february: 2,
        mar: 3,
        march: 3,
        apr: 4,
        april: 4,
        may: 5,
        jun: 6,
        june: 6,
        jul: 7,
        july: 7,
        aug: 8,
        august: 8,
        sep: 9,
        sept: 9,
        september: 9,
        oct: 10,
        october: 10,
        nov: 11,
        november: 11,
        dec: 12,
        december: 12
      };

      const day = Number.parseInt(ordinalDateMatch[1], 10);
      const month = monthMap[String(ordinalDateMatch[2] || '').toLowerCase()] || null;
      const year = Number.parseInt(ordinalDateMatch[3], 10);
      if (month == null) {
        return null;
      }

      const utcDate = new Date(Date.UTC(year, month - 1, day));
      if (
        Number.isFinite(utcDate.getTime())
        && utcDate.getUTCFullYear() === year
        && utcDate.getUTCMonth() === month - 1
        && utcDate.getUTCDate() === day
      ) {
        return utcDate.toISOString().slice(0, 10);
      }
      return null;
    }

    const utcDate = new Date(`${normalizedSeparators}T00:00:00.000Z`);
    if (!Number.isFinite(utcDate.getTime())) {
      return null;
    }

    return utcDate.toISOString().slice(0, 10);
  }

  function parseOptionalNumberField(value, fallbackValue) {
    const raw = String(value == null ? '' : value).trim();
    if (!raw) {
      return {
        value: fallbackValue,
        invalid: false,
        isEmpty: true
      };
    }

    const parsed = parseNumberValue(raw);
    if (parsed == null) {
      return {
        value: null,
        invalid: true,
        isEmpty: false
      };
    }

    return {
      value: parsed,
      invalid: false,
      isEmpty: false
    };
  }

  function formatNumberDisplay(value, minimumFractionDigits, maximumFractionDigits) {
    if (!Number.isFinite(value)) {
      return '-';
    }

    return Number(value).toLocaleString(undefined, {
      minimumFractionDigits,
      maximumFractionDigits
    });
  }

  function formatDateDisplay(value) {
    if (!value) {
      return '-';
    }

    const raw = String(value).trim();
    const ddmmyyyy = raw.match(/^(\d{2})-(\d{2})-(\d{4})$/);
    if (ddmmyyyy) {
      const day = Number.parseInt(ddmmyyyy[1], 10);
      const month = Number.parseInt(ddmmyyyy[2], 10);
      const year = Number.parseInt(ddmmyyyy[3], 10);

      const parsed = new Date(Date.UTC(year, month - 1, day));
      if (
        parsed.getUTCFullYear() === year
        && parsed.getUTCMonth() === month - 1
        && parsed.getUTCDate() === day
      ) {
        return parsed.toLocaleDateString(undefined, {
          day: '2-digit',
          month: 'short',
          year: 'numeric'
        });
      }
    }

    const fallback = new Date(raw);
    if (!Number.isNaN(fallback.valueOf())) {
      return fallback.toLocaleDateString(undefined, {
        day: '2-digit',
        month: 'short',
        year: 'numeric'
      });
    }

    return String(value);
  }

  function formatExchangeRate(value) {
    return value.toFixed(8).replace(/\.?0+$/, '');
  }

  function roundToTwoDecimals(value) {
    if (!Number.isFinite(value)) {
      return value;
    }

    return Math.round(value * 100) / 100;
  }

  function roundToFourDecimals(value) {
    if (!Number.isFinite(value)) {
      return value;
    }

    return Math.round(value * 10000) / 10000;
  }

  function normalizeIsinValue(value) {
    return String(value || '').trim().toUpperCase();
  }

  function normalizeDescriptionValue(value) {
    return String(value || '').trim();
  }

  function normalizeDescriptionKey(value) {
    return normalizeDescriptionValue(value).toLowerCase();
  }

  function parseDdmmyyyyToDate(value) {
    const raw = String(value || '').trim();
    const match = raw.match(/^(\d{2})-(\d{2})-(\d{4})$/);
    if (!match) {
      return null;
    }

    const day = Number.parseInt(match[1], 10);
    const month = Number.parseInt(match[2], 10);
    const year = Number.parseInt(match[3], 10);
    const parsed = new Date(Date.UTC(year, month - 1, day));
    if (
      parsed.getUTCFullYear() !== year
      || parsed.getUTCMonth() !== month - 1
      || parsed.getUTCDate() !== day
    ) {
      return null;
    }

    return parsed;
  }

  function readSignedNumberFromDescriptionFragment(fragment) {
    const parsed = parseNumberValue(fragment);
    return Number.isFinite(parsed) ? parsed : null;
  }

  function parseStockTradeDescription(description) {
    const trimmed = normalizeDescriptionValue(description);
    const match = trimmed.match(/^(Buy|Sell)\s+([0-9.,-]+)\s+.+@\s*([0-9.,-]+)\s+([A-Za-z]{3})/i);
    if (!match) {
      return null;
    }

    const action = String(match[1] || '').toLowerCase();
    const quantityRaw = readSignedNumberFromDescriptionFragment(match[2]);
    const priceRaw = readSignedNumberFromDescriptionFragment(match[3]);
    const currency = String(match[4] || '').trim().toUpperCase();
    if (!Number.isFinite(quantityRaw) || !Number.isFinite(priceRaw) || !currency) {
      return null;
    }

    const quantity = action === 'sell' ? -Math.abs(quantityRaw) : Math.abs(quantityRaw);
    return {
      event: action === 'sell' ? 'Sell' : 'Buy',
      quantity,
      price: priceRaw,
      currency
    };
  }

  function parseSplitTradeDescription(description) {
    const trimmed = normalizeDescriptionValue(description);
    const match = trimmed.match(/^Split(?:\s+Adjustment:)?\s+([0-9.,-]+)\s+.+@\s*([0-9.,-]+)\s+([A-Za-z]{3})/i);
    if (!match) {
      return null;
    }

    const quantityRaw = readSignedNumberFromDescriptionFragment(match[1]);
    const priceRaw = readSignedNumberFromDescriptionFragment(match[2]);
    const currency = String(match[3] || '').trim().toUpperCase();
    if (!Number.isFinite(quantityRaw) || !Number.isFinite(priceRaw) || !currency) {
      return null;
    }

    return {
      event: 'Split',
      quantity: quantityRaw,
      price: priceRaw,
      currency
    };
  }

  function parseTransferInTradeDescription(description) {
    const trimmed = normalizeDescriptionValue(description);
    const match = trimmed.match(/^Incoming\s+Transfer:?\s+(?:Buy\s+)?([0-9.,-]+)\s+.+@\s*([0-9.,-]+)\s+([A-Za-z]{3})/i);
    if (!match) {
      return null;
    }

    const quantityRaw = readSignedNumberFromDescriptionFragment(match[1]);
    const priceRaw = readSignedNumberFromDescriptionFragment(match[2]);
    const currency = String(match[3] || '').trim().toUpperCase();
    if (!Number.isFinite(quantityRaw) || !Number.isFinite(priceRaw) || !currency) {
      return null;
    }

    return {
      event: 'TransferIn',
      quantity: Math.abs(quantityRaw),
      price: priceRaw,
      currency
    };
  }

  function parseDegiroAccountStatementRow(cells, rawCsv) {
    const bookingDate = normalizeDateSeparators(cells[0]);
    // Account Statement transactions use Value date as the transaction date.
    const date = normalizeDateSeparators(cells[2]);
    const product = String(cells[3] || '').trim();
    const isin = normalizeIsinValue(cells[4]);
    const description = normalizeDescriptionValue(cells[5]);
    const fxRaw = String(cells[6] || '').trim();
    const changeCurrency = String(cells[7] || '').trim().toUpperCase();
    const changeValueRaw = String(cells[8] || '').trim();
    const orderId = String(cells[11] || '').trim();

    const changeValue = parseNumberValue(changeValueRaw);
    const fxRate = parseNumberValue(fxRaw);

    return {
      rawCsv,
      bookingDate,
      bookingDateValue: parseDdmmyyyyToDate(bookingDate),
      date,
      dateValue: parseDdmmyyyyToDate(date),
      product,
      isin,
      description,
      descriptionKey: normalizeDescriptionKey(description),
      fxRate,
      changeCurrency,
      changeValue,
      orderId
    };
  }

  function findClosestSubsequentFxPair(rows, dividendRow, usedIndices) {
    if (!dividendRow || !dividendRow.bookingDateValue) {
      return null;
    }

    const candidateEntries = rows
      .map((row, index) => ({ row, index }))
      .filter((entry) => {
        if (usedIndices.has(entry.index)) {
          return false;
        }

        const row = entry.row;
        if (!row || !row.bookingDateValue) {
          return false;
        }

        if (row.product || row.isin) {
          return false;
        }

        if (row.descriptionKey !== 'fx debit' && row.descriptionKey !== 'fx credit') {
          return false;
        }

        return row.bookingDateValue.valueOf() >= dividendRow.bookingDateValue.valueOf();
      });

    if (candidateEntries.length === 0) {
      return null;
    }

    const groupedByDate = new Map();
    candidateEntries.forEach((entry) => {
      const key = entry.row.bookingDateValue.toISOString().slice(0, 10);
      if (!groupedByDate.has(key)) {
        groupedByDate.set(key, []);
      }
      groupedByDate.get(key).push(entry);
    });

    const sortedDates = Array.from(groupedByDate.keys()).sort();
    for (let index = 0; index < sortedDates.length; index += 1) {
      const dateKey = sortedDates[index];
      const entriesForDate = groupedByDate.get(dateKey) || [];
      const debit = entriesForDate.find((entry) => entry.row.descriptionKey === 'fx debit');
      const credit = entriesForDate.find((entry) => entry.row.descriptionKey === 'fx credit');
      if (!debit || !credit) {
        const orderedEntriesForDate = entriesForDate.slice().sort((left, right) => left.index - right.index);
        for (let pairIndex = 0; pairIndex < orderedEntriesForDate.length - 1; pairIndex += 1) {
          const first = orderedEntriesForDate[pairIndex];
          const second = orderedEntriesForDate[pairIndex + 1];
          if (!first || !second) {
            continue;
          }

          if (first.row.descriptionKey !== 'fx debit' || second.row.descriptionKey !== 'fx debit') {
            continue;
          }

          if (second.index !== first.index + 1) {
            continue;
          }

          const negativeDebit = [first, second].find((entry) => Number.isFinite(entry.row.changeValue) && entry.row.changeValue < 0) || null;
          const positiveDebit = [first, second].find((entry) => Number.isFinite(entry.row.changeValue) && entry.row.changeValue > 0) || null;
          if (!negativeDebit || !positiveDebit) {
            continue;
          }

          if (!Number.isFinite(negativeDebit.row.fxRate) || negativeDebit.row.fxRate === 0) {
            continue;
          }

          const convertedAmount = Math.abs(negativeDebit.row.changeValue) / negativeDebit.row.fxRate;
          if (!approximatelyEqualAbs(convertedAmount, positiveDebit.row.changeValue, 0.05)) {
            continue;
          }

          return {
            debitRow: negativeDebit.row,
            debitIndex: negativeDebit.index,
            creditRow: positiveDebit.row,
            creditIndex: positiveDebit.index
          };
        }

        continue;
      }

      return {
        debitRow: debit.row,
        debitIndex: debit.index,
        creditRow: credit.row,
        creditIndex: credit.index
      };
    }

    return null;
  }

  function approximatelyEqualAbs(left, right, tolerance) {
    const maxDelta = Number.isFinite(tolerance) ? tolerance : 0.01;
    if (!Number.isFinite(left) || !Number.isFinite(right)) {
      return false;
    }

    return Math.abs(Math.abs(left) - Math.abs(right)) <= maxDelta;
  }

  function collectSpecialCaseDividendGroups(rows, usedIndices, ignoredIndices) {
    const groups = [];
    const dividendGroups = new Map();

    rows.forEach((row, index) => {
      if (usedIndices.has(index) || ignoredIndices.has(index)) {
        return;
      }

      if (!row || row.descriptionKey !== 'dividend' || !row.isin) {
        return;
      }

      const currency = row.changeCurrency || 'EUR';
      if (currency === 'EUR') {
        return;
      }

      const dateKey = row.bookingDateValue ? row.bookingDateValue.toISOString().slice(0, 10) : row.bookingDate;
      const groupKey = `${dateKey}|${currency}`;
      if (!dividendGroups.has(groupKey)) {
        dividendGroups.set(groupKey, []);
      }
      dividendGroups.get(groupKey).push({ row, index });
    });

    Array.from(dividendGroups.keys()).forEach((groupKey) => {
      const dividendEntries = dividendGroups.get(groupKey) || [];
      if (dividendEntries.length <= 1) {
        return;
      }

      const reservedTaxIndices = new Set();
      const specialEntries = [];
      dividendEntries.forEach((entry) => {
        const specialEntry = {
          row: entry.row,
          index: entry.index,
          taxRow: null,
          taxIndex: null,
          declaredInSourceCurrency: entry.row.changeValue,
          taxPaidInSourceCurrency: null,
          currency: entry.row.changeCurrency || 'EUR',
          dateKey: entry.row.bookingDateValue ? entry.row.bookingDateValue.toISOString().slice(0, 10) : entry.row.bookingDate
        };

        const taxCandidate = rows
          .map((candidate, candidateIndex) => ({ candidate, candidateIndex }))
          .find((candidateEntry) => (
            !usedIndices.has(candidateEntry.candidateIndex)
            && !ignoredIndices.has(candidateEntry.candidateIndex)
            && !reservedTaxIndices.has(candidateEntry.candidateIndex)
            && candidateEntry.candidate
            && candidateEntry.candidate.descriptionKey === 'dividend tax'
            && candidateEntry.candidate.isin === entry.row.isin
            && candidateEntry.candidate.date === entry.row.date
            && candidateEntry.candidate.bookingDate === entry.row.bookingDate
          ));

        if (taxCandidate) {
          specialEntry.taxRow = taxCandidate.candidate;
          specialEntry.taxIndex = taxCandidate.candidateIndex;
          specialEntry.taxPaidInSourceCurrency = taxCandidate.candidate.changeValue;
          reservedTaxIndices.add(taxCandidate.candidateIndex);
        }

        specialEntries.push(specialEntry);
      });

      groups.push({
        key: groupKey,
        entries: specialEntries
      });
    });

    return groups;
  }

  function collectCapitalReturnGroups(rows, usedIndices, ignoredIndices) {
    const groupsByIsinAndBookingDate = new Map();

    rows.forEach((row, index) => {
      if (usedIndices.has(index) || ignoredIndices.has(index)) {
        return;
      }

      if (!row || !row.isin || !row.bookingDateValue) {
        return;
      }

      if (
        row.descriptionKey !== 'capital return'
        && row.descriptionKey !== 'dividend'
        && row.descriptionKey !== 'dividend tax'
      ) {
        return;
      }

      const bookingDateKey = row.bookingDateValue.toISOString().slice(0, 10);
      const groupKey = `${row.isin}|${bookingDateKey}`;

      if (!groupsByIsinAndBookingDate.has(groupKey)) {
        groupsByIsinAndBookingDate.set(groupKey, {
          key: groupKey,
          isin: row.isin,
          bookingDateKey,
          entries: []
        });
      }

      groupsByIsinAndBookingDate.get(groupKey).entries.push({ row, index });
    });

    return Array.from(groupsByIsinAndBookingDate.values())
      .map((group) => {
        const entries = Array.isArray(group.entries) ? group.entries : [];
        return {
          ...group,
          entries,
          capitalReturnEntries: entries.filter((entry) => entry.row.descriptionKey === 'capital return'),
          dividendEntries: entries.filter((entry) => entry.row.descriptionKey === 'dividend'),
          dividendTaxEntries: entries.filter((entry) => entry.row.descriptionKey === 'dividend tax')
        };
      })
      .filter((group) => (
        group.capitalReturnEntries.length > 0
        && group.dividendEntries.length > 0
        && group.dividendTaxEntries.length > 0
      ));
  }

  function parseDegiroAccountStatementRows(parsedRows) {
    const rows = Array.isArray(parsedRows) ? parsedRows : [];
    const usedIndices = new Set();
    const ignoredIndices = new Set();
    const stockTransactions = [];
    const dividendTransactions = [];
    const cashTransactions = [];
    const failedRawRows = [];

    const isIgnoredDescription = (descriptionKey) => (
      descriptionKey.includes('cash sweep transfer')
      || descriptionKey.includes('transfer from your cash account')
      || descriptionKey.includes('transfer to your cash account')
      || descriptionKey.includes('money market fund')
      || descriptionKey.includes('product change')
    );

    for (let index = 0; index < rows.length; index += 1) {
      const row = rows[index];
      if (isIgnoredDescription(row.descriptionKey)) {
        ignoredIndices.add(index);
      }
    }

    const splitGroupMap = new Map();
    rows.forEach((row, index) => {
      if (!row || !row.isin || !row.descriptionKey.startsWith('split ')) {
        return;
      }

      const groupKey = `${row.date}|${row.isin}`;
      if (!splitGroupMap.has(groupKey)) {
        splitGroupMap.set(groupKey, []);
      }
      splitGroupMap.get(groupKey).push({ row, index });
    });

    splitGroupMap.forEach((entries) => {
      const groupEntries = Array.isArray(entries) ? entries : [];
      if (groupEntries.length !== 2) {
        groupEntries.forEach((entry) => failedRawRows.push(entry.row.rawCsv));
        groupEntries.forEach((entry) => usedIndices.add(entry.index));
        return;
      }

      const negativeEntry = groupEntries.find((entry) => Number.isFinite(entry.row.changeValue) && entry.row.changeValue < 0);
      const positiveEntry = groupEntries.find((entry) => Number.isFinite(entry.row.changeValue) && entry.row.changeValue > 0);

      if (!negativeEntry || !positiveEntry) {
        groupEntries.forEach((entry) => failedRawRows.push(entry.row.rawCsv));
        groupEntries.forEach((entry) => usedIndices.add(entry.index));
        return;
      }

      if (!approximatelyEqualAbs(negativeEntry.row.changeValue, positiveEntry.row.changeValue, 0.01)) {
        groupEntries.forEach((entry) => failedRawRows.push(entry.row.rawCsv));
        groupEntries.forEach((entry) => usedIndices.add(entry.index));
        return;
      }

      const splitTrade = parseSplitTradeDescription(negativeEntry.row.description);
      if (!splitTrade) {
        groupEntries.forEach((entry) => failedRawRows.push(entry.row.rawCsv));
        groupEntries.forEach((entry) => usedIndices.add(entry.index));
        return;
      }

      stockTransactions.push({
        type: TRANSACTION_TYPES.stock,
        date: negativeEntry.row.date,
        event: splitTrade.event,
        description: negativeEntry.row.product || negativeEntry.row.isin,
        amount: 0,
        isin: negativeEntry.row.isin,
        currency: splitTrade.currency,
        quantity: splitTrade.quantity,
        price: splitTrade.price,
        fxRate: null,
        fxFee: null,
        fxRatePreFee: null,
        commission: 0,
        taxPaid: 0,
        orderId: negativeEntry.row.orderId || positiveEntry.row.orderId || null
      });

      groupEntries.forEach((entry) => usedIndices.add(entry.index));
    });

    rows.forEach((row, index) => {
      if (usedIndices.has(index) || ignoredIndices.has(index)) {
        return;
      }

      if (
        !row
        || !row.isin
        || !row.descriptionKey.startsWith('incoming transfer')
      ) {
        return;
      }

      const transferTrade = parseTransferInTradeDescription(row.description);
      if (!transferTrade) {
        failedRawRows.push(row.rawCsv);
        usedIndices.add(index);
        return;
      }

      stockTransactions.push({
        type: TRANSACTION_TYPES.stock,
        date: row.date,
        event: transferTrade.event,
        description: row.product || row.isin,
        amount: 0,
        isin: row.isin,
        currency: transferTrade.currency,
        quantity: transferTrade.quantity,
        price: transferTrade.price,
        fxRate: null,
        fxFee: null,
        fxRatePreFee: null,
        commission: 0,
        taxPaid: 0,
        orderId: null
      });

      usedIndices.add(index);
    });

    const stockGroupMap = new Map();
    rows.forEach((row, index) => {
      if (usedIndices.has(index) || ignoredIndices.has(index)) {
        return;
      }

      if (!row.orderId || !row.isin) {
        return;
      }

      const descriptionKey = row.descriptionKey;
      if (
        descriptionKey.startsWith('buy ')
        || descriptionKey.startsWith('sell ')
        || descriptionKey.includes('fees')
        || descriptionKey === 'fx debit'
        || descriptionKey === 'fx credit'
      ) {
        const groupKey = `${row.orderId}|${row.isin}`;
        if (!stockGroupMap.has(groupKey)) {
          stockGroupMap.set(groupKey, []);
        }
        stockGroupMap.get(groupKey).push({ row, index });
      }
    });

    stockGroupMap.forEach((entries) => {
      const tradeEntries = entries.filter((entry) => (
        entry.row.descriptionKey.startsWith('buy ')
        || entry.row.descriptionKey.startsWith('sell ')
      ));

      if (tradeEntries.length === 0) {
        return;
      }

      const first = tradeEntries[0].row;
      const parsedTrades = tradeEntries.map((entry) => parseStockTradeDescription(entry.row.description));
      if (parsedTrades.some((item) => !item)) {
        entries.forEach((entry) => failedRawRows.push(entry.row.rawCsv));
        entries.forEach((entry) => usedIndices.add(entry.index));
        return;
      }

      const event = parsedTrades[0].event;
      if (!parsedTrades.every((item) => item.event === event)) {
        entries.forEach((entry) => failedRawRows.push(entry.row.rawCsv));
        entries.forEach((entry) => usedIndices.add(entry.index));
        return;
      }

      const quantity = parsedTrades.reduce((sum, item) => sum + item.quantity, 0);
      const totalQuantityAbs = parsedTrades.reduce((sum, item) => sum + Math.abs(item.quantity), 0);
      if (!Number.isFinite(totalQuantityAbs) || totalQuantityAbs <= 0) {
        entries.forEach((entry) => failedRawRows.push(entry.row.rawCsv));
        entries.forEach((entry) => usedIndices.add(entry.index));
        return;
      }
      const firstParsedPrice = parsedTrades[0].price;
      const hasUniformPrice = parsedTrades.every((item) => Math.abs(item.price - firstParsedPrice) < 0.000000001);
      const weightedPriceSum = parsedTrades.reduce((sum, item) => sum + (Math.abs(item.quantity) * item.price), 0);
      const price = hasUniformPrice ? firstParsedPrice : (weightedPriceSum / totalQuantityAbs);
      const currency = parsedTrades[0].currency;
      const commission = entries
        .filter((entry) => entry.row.descriptionKey.includes('fees'))
        .reduce((sum, entry) => sum + (Number.isFinite(entry.row.changeValue) ? entry.row.changeValue : 0), 0);
      const normalizedCommission = roundToTwoDecimals(commission);

      const involvesFx = tradeEntries.some((entry) => entry.row.changeCurrency && entry.row.changeCurrency !== 'EUR');
      let fxRate = 1;
      let amount = null;

      if (involvesFx) {
        const fxRateSourceDescription = event === 'Sell' ? 'fx debit' : 'fx credit';
        const fxRateRow = entries.find((entry) => entry.row.descriptionKey === fxRateSourceDescription);
        if (!fxRateRow || !Number.isFinite(fxRateRow.row.fxRate)) {
          entries.forEach((entry) => failedRawRows.push(entry.row.rawCsv));
          entries.forEach((entry) => usedIndices.add(entry.index));
          return;
        }
        fxRate = fxRateRow.row.fxRate;

        const amountDescription = event === 'Sell' ? 'fx credit' : 'fx debit';
        const amountRows = entries.filter((entry) => entry.row.descriptionKey === amountDescription);
        if (amountRows.length === 0 || amountRows.some((entry) => !Number.isFinite(entry.row.changeValue))) {
          entries.forEach((entry) => failedRawRows.push(entry.row.rawCsv));
          entries.forEach((entry) => usedIndices.add(entry.index));
          return;
        }

        amount = amountRows.reduce((sum, entry) => sum + entry.row.changeValue, 0) + normalizedCommission;
      } else {
        const tradeAmountRows = tradeEntries.filter((entry) => Number.isFinite(entry.row.changeValue));
        if (tradeAmountRows.length === 0) {
          entries.forEach((entry) => failedRawRows.push(entry.row.rawCsv));
          entries.forEach((entry) => usedIndices.add(entry.index));
          return;
        }
        amount = tradeAmountRows.reduce((sum, entry) => sum + entry.row.changeValue, 0) + normalizedCommission;
      }

      const earliestDate = tradeEntries[0].row.date;
      const name = first.product || first.isin;

      stockTransactions.push({
        type: TRANSACTION_TYPES.stock,
        date: earliestDate,
        event,
        description: name,
        amount: roundToFourDecimals(amount),
        isin: first.isin,
        currency,
        quantity,
        price,
        fxRate,
        commission: normalizedCommission,
        orderId: first.orderId
      });

      entries.forEach((entry) => usedIndices.add(entry.index));
    });

    function markFailedRowAtIndex(index) {
      if (!Number.isInteger(index) || usedIndices.has(index) || ignoredIndices.has(index)) {
        return;
      }

      const row = rows[index];
      if (!row) {
        return;
      }

      failedRawRows.push(row.rawCsv);
      usedIndices.add(index);
    }

    const capitalReturnGroups = collectCapitalReturnGroups(rows, usedIndices, ignoredIndices);
    capitalReturnGroups.forEach((group) => {
      const entries = Array.isArray(group.entries) ? group.entries : [];
      const capitalReturnEntries = Array.isArray(group.capitalReturnEntries) ? group.capitalReturnEntries : [];

      if (entries.length === 0 || capitalReturnEntries.length === 0) {
        return;
      }

      const representativeEntry = capitalReturnEntries[0];
      const representativeRow = representativeEntry.row;
      const currency = representativeRow.changeCurrency || 'EUR';
      const involvesFx = currency !== 'EUR';

      const hasInvalidMainValues = entries.some((entry) => !Number.isFinite(entry.row.changeValue));
      if (hasInvalidMainValues) {
        entries.forEach((entry) => markFailedRowAtIndex(entry.index));
        return;
      }

      let fxPair = null;
      if (involvesFx) {
        fxPair = findClosestSubsequentFxPair(rows, representativeRow, usedIndices);
        if (
          !fxPair
          || !Number.isFinite(fxPair.debitRow.changeValue)
          || !Number.isFinite(fxPair.debitRow.fxRate)
          || fxPair.debitRow.fxRate === 0
          || !Number.isFinite(fxPair.creditRow.changeValue)
        ) {
          entries.forEach((entry) => markFailedRowAtIndex(entry.index));
          return;
        }
      }

      const mainGroupSum = entries.reduce((sum, entry) => sum + entry.row.changeValue, 0);
      const groupValidationSum = involvesFx
        ? (mainGroupSum + fxPair.debitRow.changeValue)
        : mainGroupSum;

      if (!approximatelyEqualAbs(groupValidationSum, 0, 0.05)) {
        entries.forEach((entry) => markFailedRowAtIndex(entry.index));
        return;
      }

      const fxRate = involvesFx ? fxPair.debitRow.fxRate : 1;
      const declaredInSourceCurrency = involvesFx
        ? Math.abs(fxPair.debitRow.changeValue)
        : capitalReturnEntries.reduce((sum, entry) => sum + entry.row.changeValue, 0);
      const amountInSourceCurrency = declaredInSourceCurrency;
      const declaredConverted = involvesFx
        ? fxPair.creditRow.changeValue
        : declaredInSourceCurrency;
      const amount = declaredConverted;
      const description = representativeRow.product || representativeRow.isin;
      const country = representativeRow.isin.slice(0, 2);

      dividendTransactions.push({
        type: TRANSACTION_TYPES.dividend,
        date: representativeRow.date,
        event: 'CapitalReturn',
        description,
        amount,
        isin: representativeRow.isin,
        currency,
        country,
        fxRate,
        declaredInSourceCurrency,
        taxPaidInSourceCurrency: 0,
        amountInSourceCurrency,
        declaredConverted,
        taxPaidConverted: 0
      });

      entries.forEach((entry) => usedIndices.add(entry.index));
      if (fxPair) {
        usedIndices.add(fxPair.debitIndex);
        usedIndices.add(fxPair.creditIndex);
      }
    });

    const specialCaseDividendGroups = collectSpecialCaseDividendGroups(rows, usedIndices, ignoredIndices);
    specialCaseDividendGroups.forEach((group) => {
      const entries = Array.isArray(group.entries) ? group.entries : [];
      if (entries.length === 0) {
        return;
      }

      const firstEntry = entries[0];
      const fxPair = findClosestSubsequentFxPair(rows, firstEntry.row, usedIndices);
      const hasInvalidEntry = entries.some((entry) => (
        !entry
        || !Number.isFinite(entry.declaredInSourceCurrency)
        || entry.taxIndex == null
        || !Number.isFinite(entry.taxPaidInSourceCurrency)
      ));

      if (
        hasInvalidEntry
        || !fxPair
        || !Number.isFinite(fxPair.debitRow.fxRate)
        || !Number.isFinite(fxPair.debitRow.changeValue)
        || !Number.isFinite(fxPair.creditRow.changeValue)
        || fxPair.debitRow.fxRate === 0
      ) {
        // Fall back to normal per-dividend parsing instead of hard-failing here.
        return;
      }

      const fxRate = fxPair.debitRow.fxRate;
      const normalizedEntries = entries.map((entry) => {
        const amountInSourceCurrency = entry.declaredInSourceCurrency + entry.taxPaidInSourceCurrency;
        const amount = amountInSourceCurrency / fxRate;
        return {
          entry,
          amountInSourceCurrency,
          amount,
          declaredConverted: entry.declaredInSourceCurrency / fxRate,
          taxPaidConverted: entry.taxPaidInSourceCurrency / fxRate
        };
      });

      const aggregateAmountInSourceCurrency = normalizedEntries.reduce((sum, item) => sum + item.amountInSourceCurrency, 0);
      const aggregateAmount = normalizedEntries.reduce((sum, item) => sum + item.amount, 0);
      const aggregateMatches = approximatelyEqualAbs(aggregateAmountInSourceCurrency, fxPair.debitRow.changeValue, 0.05)
        && approximatelyEqualAbs(aggregateAmount, fxPair.creditRow.changeValue, 0.05);

      if (!aggregateMatches) {
        // Fall back to normal per-dividend parsing instead of hard-failing here.
        return;
      }

      normalizedEntries.forEach((item) => {
        const entry = item.entry;
        const country = entry.row.isin.slice(0, 2);
        dividendTransactions.push({
          type: TRANSACTION_TYPES.dividend,
          date: entry.row.date,
          event: 'Dividend',
          description: entry.row.product || entry.row.isin,
          amount: item.amount,
          isin: entry.row.isin,
          currency: entry.currency,
          country,
          fxRate,
          declaredInSourceCurrency: entry.declaredInSourceCurrency,
          taxPaidInSourceCurrency: entry.taxPaidInSourceCurrency,
          amountInSourceCurrency: item.amountInSourceCurrency,
          declaredConverted: item.declaredConverted,
          taxPaidConverted: item.taxPaidConverted
        });

        usedIndices.add(entry.index);
        if (entry.taxIndex != null) {
          usedIndices.add(entry.taxIndex);
        }
      });

      usedIndices.add(fxPair.debitIndex);
      usedIndices.add(fxPair.creditIndex);
    });

    const exchangeFeeByDate = new Map();
    for (let index = 0; index < rows.length; index += 1) {
      if (usedIndices.has(index) || ignoredIndices.has(index)) {
        continue;
      }

      const row = rows[index];
      if (!row || !row.descriptionKey.includes('exchange connection fee')) {
        continue;
      }

      if (!Number.isFinite(row.changeValue)) {
        continue;
      }

      const dateKey = row.dateValue ? row.dateValue.toISOString().slice(0, 10) : row.date;
      if (!exchangeFeeByDate.has(dateKey)) {
        exchangeFeeByDate.set(dateKey, {
          date: row.date,
          amount: 0
        });
      }

      const aggregate = exchangeFeeByDate.get(dateKey);
      aggregate.amount += row.changeValue;
      usedIndices.add(index);
    }

    Array.from(exchangeFeeByDate.values()).forEach((item) => {
      cashTransactions.push({
        type: TRANSACTION_TYPES.cash,
        date: item.date,
        event: 'ExchangeFee',
        description: 'Exchange Connection Fee',
        isin: null,
        amount: item.amount
      });
    });

    for (let index = 0; index < rows.length; index += 1) {
      if (usedIndices.has(index) || ignoredIndices.has(index)) {
        continue;
      }

      const row = rows[index];
      if (!row || !row.descriptionKey.includes('adr/gdr pass-through fee')) {
        continue;
      }

      if (!Number.isFinite(row.changeValue) || !row.product) {
        failedRawRows.push(row.rawCsv);
        usedIndices.add(index);
        continue;
      }

      const fxPair = findClosestSubsequentFxPair(rows, row, usedIndices);
      if (!fxPair) {
        failedRawRows.push(row.rawCsv);
        usedIndices.add(index);
        continue;
      }

      const fxRateRow = [fxPair.debitRow, fxPair.creditRow].find((candidate) => candidate && Number.isFinite(candidate.fxRate));
      const eurAmountRow = [
        { row: fxPair.debitRow, index: fxPair.debitIndex },
        { row: fxPair.creditRow, index: fxPair.creditIndex }
      ].find((candidate) => (
        candidate.row
        && candidate.row.changeCurrency === 'EUR'
        && Number.isFinite(candidate.row.changeValue)
      ));

      if (!fxRateRow || !eurAmountRow || fxRateRow.fxRate === 0) {
        failedRawRows.push(row.rawCsv);
        usedIndices.add(index);
        continue;
      }

      const convertedMainAmount = Math.abs(row.changeValue) / fxRateRow.fxRate;
      if (!approximatelyEqualAbs(eurAmountRow.row.changeValue, convertedMainAmount, 0.05)) {
        failedRawRows.push(row.rawCsv);
        usedIndices.add(index);
        continue;
      }

      cashTransactions.push({
        type: TRANSACTION_TYPES.cash,
        date: row.date,
        event: 'ADRFee',
        description: row.product,
        isin: row.isin || null,
        amount: eurAmountRow.row.changeValue
      });
      usedIndices.add(index);
      usedIndices.add(fxPair.debitIndex);
      usedIndices.add(fxPair.creditIndex);
    }

    for (let index = 0; index < rows.length; index += 1) {
      if (usedIndices.has(index) || ignoredIndices.has(index)) {
        continue;
      }

      const row = rows[index];
      if (!row || !row.descriptionKey.includes('transfer portfolio fee')) {
        continue;
      }

      if (!Number.isFinite(row.changeValue) || !row.isin || !row.product) {
        failedRawRows.push(row.rawCsv);
        usedIndices.add(index);
        continue;
      }

      cashTransactions.push({
        type: TRANSACTION_TYPES.cash,
        date: row.date,
        event: 'TransferFee',
        description: row.product,
        isin: row.isin || null,
        amount: row.changeValue
      });
      usedIndices.add(index);
    }

    for (let index = 0; index < rows.length; index += 1) {
      if (usedIndices.has(index) || ignoredIndices.has(index)) {
        continue;
      }

      const row = rows[index];
      if (!row.isin || row.descriptionKey !== 'dividend') {
        continue;
      }

      const taxEntry = rows
        .map((candidate, candidateIndex) => ({ candidate, candidateIndex }))
        .find((entry) => (
          !usedIndices.has(entry.candidateIndex)
          && entry.candidateIndex !== index
          && entry.candidate.isin === row.isin
          && entry.candidate.descriptionKey === 'dividend tax'
          && entry.candidate.date === row.date
          && entry.candidate.bookingDate === row.bookingDate
        ));

      if (!taxEntry || !Number.isFinite(row.changeValue) || !Number.isFinite(taxEntry.candidate.changeValue)) {
        failedRawRows.push(row.rawCsv);
        usedIndices.add(index);
        if (taxEntry) {
          failedRawRows.push(taxEntry.candidate.rawCsv);
          usedIndices.add(taxEntry.candidateIndex);
        }
        continue;
      }

      const declaredInSourceCurrency = row.changeValue;
      const taxPaidInSourceCurrency = taxEntry.candidate.changeValue;
      const currency = row.changeCurrency || 'EUR';
      const involvesFx = currency !== 'EUR';

      let fxRate = 1;
      let amountInSourceCurrency = declaredInSourceCurrency + taxPaidInSourceCurrency;
      let amount = declaredInSourceCurrency + taxPaidInSourceCurrency;

      if (involvesFx) {
        const fxPair = findClosestSubsequentFxPair(rows, row, usedIndices);
        if (
          !fxPair
          || !Number.isFinite(fxPair.debitRow.fxRate)
          || !Number.isFinite(fxPair.debitRow.changeValue)
          || !Number.isFinite(fxPair.creditRow.changeValue)
        ) {
          failedRawRows.push(row.rawCsv);
          failedRawRows.push(taxEntry.candidate.rawCsv);
          usedIndices.add(index);
          usedIndices.add(taxEntry.candidateIndex);
          continue;
        }

        fxRate = fxPair.debitRow.fxRate;
        amountInSourceCurrency = fxPair.debitRow.changeValue;
        amount = fxPair.creditRow.changeValue;
        usedIndices.add(fxPair.debitIndex);
        usedIndices.add(fxPair.creditIndex);
      }

      const declaredConverted = fxRate !== 0 ? declaredInSourceCurrency / fxRate : declaredInSourceCurrency;
      const taxPaidConverted = fxRate !== 0 ? taxPaidInSourceCurrency / fxRate : taxPaidInSourceCurrency;
      const country = row.isin.slice(0, 2);

      dividendTransactions.push({
        type: TRANSACTION_TYPES.dividend,
        date: row.date,
        event: 'Dividend',
        description: row.product || row.isin,
        amount,
        isin: row.isin,
        currency,
        country,
        fxRate,
        declaredInSourceCurrency,
        taxPaidInSourceCurrency,
        amountInSourceCurrency,
        declaredConverted,
        taxPaidConverted
      });

      usedIndices.add(index);
      usedIndices.add(taxEntry.candidateIndex);
    }

    for (let index = 0; index < rows.length; index += 1) {
      if (usedIndices.has(index) || ignoredIndices.has(index)) {
        continue;
      }

      const row = rows[index];
      const descriptionKey = row.descriptionKey;
      if (descriptionKey.includes('interest') || descriptionKey.includes('deposit') || descriptionKey.includes('withdrawal')) {
        if (Number.isFinite(row.changeValue)) {
          let event = '';
          if (descriptionKey.includes('interest')) {
            event = 'Interest';
          } else if (descriptionKey.includes('deposit')) {
            event = 'Deposit';
          } else {
            event = 'Withdrawal';
          }

          // Interest rows with zero amount should be ignored without being treated as parse failures.
          if (event === 'Interest' && Math.abs(row.changeValue) < 0.0000001) {
            usedIndices.add(index);
            continue;
          }

          cashTransactions.push({
            type: TRANSACTION_TYPES.cash,
            date: row.date,
            event,
            description: row.description,
            amount: row.changeValue
          });
          usedIndices.add(index);
          continue;
        }
      }
    }

    rows.forEach((row, index) => {
      if (usedIndices.has(index) || ignoredIndices.has(index)) {
        return;
      }
      failedRawRows.push(row.rawCsv);
    });

    const allRows = stockTransactions
      .concat(dividendTransactions)
      .concat(cashTransactions)
      .sort((left, right) => {
        const leftDate = parseDdmmyyyyToDate(left.date);
        const rightDate = parseDdmmyyyyToDate(right.date);
        const leftValue = leftDate ? leftDate.valueOf() : 0;
        const rightValue = rightDate ? rightDate.valueOf() : 0;
        return rightValue - leftValue;
      });

    return {
      allRows,
      stockRows: stockTransactions,
      dividendRows: dividendTransactions,
      cashRows: cashTransactions,
      failedRawRows
    };
  }

  function getUniqueIsinsFromRows(rows) {
    const values = Array.isArray(rows) ? rows : [];
    const unique = values
      .map((row) => normalizeIsinValue(row && row.isin))
      .filter(Boolean);
    return Array.from(new Set(unique));
  }

  function isIsinMappedForRow(row, unmappedIsinSet) {
    if (!row || !row.isin) {
      return true;
    }

    return !unmappedIsinSet.has(normalizeIsinValue(row.isin));
  }

  function isRowSupportedBySecurityMapping(row) {
    if (!row) {
      return false;
    }

    return row.type === TRANSACTION_TYPES.stock || row.type === TRANSACTION_TYPES.dividend;
  }

  function isAccountPreviewRowExpandable(row) {
    if (!row) {
      return false;
    }

    return row.type === TRANSACTION_TYPES.stock || row.type === TRANSACTION_TYPES.dividend;
  }

  function formatAccountExpandedValue(value, options) {
    const settings = options || {};
    if (!Number.isFinite(value)) {
      return '-';
    }

    if (settings.kind === 'integer') {
      return formatNumberDisplay(value, 0, 6);
    }

    if (settings.kind === 'fx') {
      return formatNumberDisplay(value, 4, 8);
    }

    return formatNumberDisplay(value, 2, 2);
  }

  function getAccountExpandedDetails(row) {
    if (!row) {
      return [];
    }

    if (row.type === TRANSACTION_TYPES.stock) {
      const currency = row.currency || '-';
      return [
        { label: 'ISIN', value: row.isin || '-' },
        { label: 'Qty', value: formatAccountExpandedValue(row.quantity, { kind: 'integer' }) },
        { label: `Price (${currency})`, value: formatAccountExpandedValue(row.price) },
        { label: 'FX', value: formatAccountExpandedValue(row.fxRate, { kind: 'fx' }) },
        { label: 'Commission', value: formatAccountExpandedValue(row.commission) },
        { label: 'Order Id', value: row.orderId || '-' }
      ];
    }

    if (row.type === TRANSACTION_TYPES.dividend) {
      const currency = row.currency || '-';
      return [
        { label: 'ISIN', value: row.isin || '-' },
        { label: 'Country', value: row.country || '-' },
        { label: `Declared (${currency})`, value: formatAccountExpandedValue(row.declaredInSourceCurrency) },
        { label: `TaxPaid (${currency})`, value: formatAccountExpandedValue(row.taxPaidInSourceCurrency) },
        { label: `Amount (${currency})`, value: formatAccountExpandedValue(row.amountInSourceCurrency) },
        { label: 'FX', value: formatAccountExpandedValue(row.fxRate, { kind: 'fx' }) },
        { label: 'Declared', value: formatAccountExpandedValue(row.declaredConverted) },
        { label: 'TaxPaid', value: formatAccountExpandedValue(row.taxPaidConverted) }
      ];
    }

    return [];
  }

  function readOrderIdFromTransactionCells(cells) {
    for (let index = cells.length - 1; index >= 16; index -= 1) {
      const candidate = String(cells[index] || '').trim();
      if (candidate) {
        return candidate;
      }
    }

    return '';
  }

  function buildNormalizedTransactionFromRows(rows, options) {
    if (!Array.isArray(rows) || rows.length === 0) {
      return null;
    }

    const hasRequiredNumericValues = rows.every((row) => (
      Number.isFinite(row.localValue)
      && Number.isFinite(row.fxFee)
      && Number.isFinite(row.commission)
      && Number.isFinite(row.amount)
      && Number.isFinite(row.quantity)
    ));
    if (!hasRequiredNumericValues) {
      return null;
    }

    const settings = options || {};

    const first = rows[0];
    const quantity = rows.reduce((sum, row) => sum + row.quantity, 0);
    const localValue = rows.reduce((sum, row) => sum + row.localValue, 0);
    const fxFee = rows.reduce((sum, row) => sum + row.fxFee, 0);
    const commission = roundToTwoDecimals(rows.reduce((sum, row) => sum + row.commission, 0));
    const amount = roundToFourDecimals(rows.reduce((sum, row) => sum + row.amount, 0));

    const denominator = amount - commission;
    if (!Number.isFinite(denominator) || denominator === 0 || quantity === 0) {
      return null;
    }

    const exchangeRate = localValue / denominator;
    if (!Number.isFinite(exchangeRate)) {
      return null;
    }

    return {
      date: first.date,
      event: quantity > 0 ? 'Buy' : 'Sell',
      name: first.product,
      isin: first.isin,
      mic: first.mic,
      orderId: first.orderId,
      quantity,
      price: parseNumberValue(first.priceRaw),
      priceRaw: first.priceRaw,
      currency: first.currency,
      fxFee,
      fxRatePreFee: parseNumberValue(first.exchangeRatePreFxFee),
      exchangeRatePreFxFeeRaw: first.exchangeRatePreFxFee,
      fxRate: roundToFourDecimals(exchangeRate),
      commission,
      amount,
      wasCombinedFromOrderRows: Boolean(settings.wasCombinedFromOrderRows)
    };
  }

  function combineParsedDegiroTransactionRows(parsedRows) {
    const combinedOrderIds = [];
    const normalizedRows = [];
    const failedRawRows = [];

    const orderedRows = [];
    const rowsWithoutOrderId = [];

    parsedRows.forEach((row) => {
      if (row.orderId) {
        orderedRows.push(row);
      } else {
        rowsWithoutOrderId.push(row);
      }
    });

    const groupedRows = new Map();
    orderedRows.forEach((row) => {
      const groupKey = `order:${row.orderId}`;
      if (!groupedRows.has(groupKey)) {
        groupedRows.set(groupKey, []);
      }
      groupedRows.get(groupKey).push(row);
    });

    groupedRows.forEach((rows) => {
      const isOrderGroup = rows.length > 1 && rows[0].orderId;

      if (isOrderGroup) {
        const first = rows[0];
        const hasMismatch = rows.some((row) => (
          row.date !== first.date
          || row.isin !== first.isin
          || row.mic !== first.mic
          || row.product !== first.product
          || row.priceRaw !== first.priceRaw
          || row.currency !== first.currency
          || row.exchangeRatePreFxFee !== first.exchangeRatePreFxFee
        ));

        if (hasMismatch) {
          rows.forEach((row) => failedRawRows.push(row.rawCsv));
          return;
        }

        const normalized = buildNormalizedTransactionFromRows(rows, { wasCombinedFromOrderRows: true });
        if (!normalized) {
          rows.forEach((row) => failedRawRows.push(row.rawCsv));
          return;
        }

        combinedOrderIds.push(first.orderId);
        normalizedRows.push(normalized);
        return;
      }

      const normalizedSingle = buildNormalizedTransactionFromRows(rows, { wasCombinedFromOrderRows: false });
      if (!normalizedSingle) {
        rows.forEach((row) => failedRawRows.push(row.rawCsv));
        return;
      }

      normalizedRows.push(normalizedSingle);
    });

    const splitCandidateGroups = new Map();
    rowsWithoutOrderId.forEach((row) => {
      const key = `${row.date}|${row.isin}`;
      if (!splitCandidateGroups.has(key)) {
        splitCandidateGroups.set(key, []);
      }
      splitCandidateGroups.get(key).push(row);
    });

    splitCandidateGroups.forEach((rows) => {
      const values = Array.isArray(rows) ? rows.slice() : [];
      const consumed = new Set();

      for (let index = 0; index < values.length; index += 1) {
        if (consumed.has(index)) {
          continue;
        }

        const negativeRow = values[index];
        if (!negativeRow || !Number.isFinite(negativeRow.amount) || negativeRow.amount >= 0) {
          continue;
        }

        let positiveIndex = -1;
        for (let otherIndex = 0; otherIndex < values.length; otherIndex += 1) {
          if (otherIndex === index || consumed.has(otherIndex)) {
            continue;
          }

          const positiveRow = values[otherIndex];
          if (!positiveRow || !Number.isFinite(positiveRow.amount) || positiveRow.amount <= 0) {
            continue;
          }

          if (approximatelyEqualAbs(negativeRow.amount, positiveRow.amount, 0.01)) {
            positiveIndex = otherIndex;
            break;
          }
        }

        if (positiveIndex < 0) {
          continue;
        }

        consumed.add(index);
        consumed.add(positiveIndex);

        const positiveRow = values[positiveIndex];
        const quantitiesCancelOut = Number.isFinite(negativeRow.quantity)
          && Number.isFinite(positiveRow.quantity)
          && Math.abs(negativeRow.quantity + positiveRow.quantity) <= 0.000001;

        // Candidate split rows with quantities summing to zero are ignored.
        if (quantitiesCancelOut) {
          continue;
        }

        normalizedRows.push({
          date: negativeRow.date,
          event: 'Split',
          name: negativeRow.product,
          isin: negativeRow.isin,
          mic: negativeRow.mic,
          orderId: null,
          quantity: negativeRow.quantity,
          price: parseNumberValue(negativeRow.priceRaw),
          priceRaw: negativeRow.priceRaw,
          currency: negativeRow.currency,
          fxFee: null,
          fxRatePreFee: null,
          exchangeRatePreFxFeeRaw: null,
          fxRate: null,
          commission: 0,
          taxPaid: 0,
          amount: 0,
          wasCombinedFromOrderRows: false,
          wasCombinedFromSplitRows: true
        });
      }

      const remainingIndices = values
        .map((_, index) => index)
        .filter((index) => !consumed.has(index));

      if (remainingIndices.length === 1) {
        const candidateIndex = remainingIndices[0];
        const candidate = values[candidateIndex];
        const price = parseNumberValue(candidate && candidate.priceRaw);
        if (
          candidate
          && !candidate.orderId
          && Number.isFinite(candidate.quantity)
          && candidate.quantity > 0
          && Number.isFinite(price)
          && candidate.currency
        ) {
          normalizedRows.push({
            date: candidate.date,
            event: 'TransferIn',
            name: candidate.product,
            isin: candidate.isin,
            mic: candidate.mic,
            orderId: null,
            quantity: Number.parseInt(candidate.quantity, 10),
            price,
            priceRaw: candidate.priceRaw,
            currency: candidate.currency,
            fxFee: null,
            fxRatePreFee: null,
            exchangeRatePreFxFeeRaw: null,
            fxRate: null,
            commission: 0,
            taxPaid: 0,
            amount: 0,
            wasCombinedFromOrderRows: false,
            wasTransferIn: true
          });

          consumed.add(candidateIndex);
        }
      }

      values.forEach((row, index) => {
        if (consumed.has(index)) {
          return;
        }

        const normalizedSingle = buildNormalizedTransactionFromRows([row], { wasCombinedFromOrderRows: false });
        if (!normalizedSingle) {
          failedRawRows.push(row.rawCsv);
          return;
        }

        normalizedRows.push(normalizedSingle);
      });
    });

    return {
      normalizedRows,
      failedRawRows,
      combinedOrderIds
    };
  }

  function parseDegiroTransactionRow(cells, rawCsv) {
    const date = normalizeDateSeparators(cells[0]);
    const product = String(cells[2] || '').trim();
    const isin = String(cells[3] || '').trim();
    const venue = String(cells[5] || '').trim();
    const quantityRaw = String(cells[6] || '').trim();
    const priceRaw = String(cells[7] || '').trim();
    const currencyRaw = String(cells[8] || '').trim();
    const localValueRaw = String(cells[9] || '').trim();
    const exchangeRatePreFxFee = String(cells[12] || '').trim();
    const fxFeeRaw = String(cells[13] || '').trim();
    const commissionRaw = String(cells[14] || '').trim();
    const amountRaw = String(cells[15] || '').trim();
    const orderId = readOrderIdFromTransactionCells(cells);

    const quantity = parseNumberValue(quantityRaw);
    const localValue = parseNumberValue(localValueRaw);
    const fxFee = fxFeeRaw ? parseNumberValue(fxFeeRaw) : 0;
    const commission = commissionRaw ? parseNumberValue(commissionRaw) : 0;
    const amount = parseNumberValue(amountRaw);

    if (!date || !isin || !priceRaw || !currencyRaw || quantity == null || quantity === 0) {
      return {
        success: false,
        rawCsv
      };
    }

    // Rows with an Order ID are regular trade rows and require full value columns.
    if (orderId && (localValue == null || fxFee == null || commission == null || amount == null)) {
      return {
        success: false,
        rawCsv
      };
    }

    return {
      success: true,
      value: {
        rawCsv,
        orderId,
        date,
        product,
        isin,
        mic: venue,
        quantity,
        priceRaw,
        currency: currencyRaw,
        localValue,
        fxFee,
        exchangeRatePreFxFee,
        commission,
        amount
      }
    };
  }

  function TransactionsUploadPage({ navigate }) {
    const { portfolios } = usePortfolios({
      onUnauthorized: () => {
        handleUnauthorizedApiResponse();
      }
    });
    const [degiroPortfolio, setDegiroPortfolio] = React.useState('');
    const [degiroFileName, setDegiroFileName] = React.useState('');
    const [degiroFileKind, setDegiroFileKind] = React.useState(null);
    const [degiroStockRows, setDegiroStockRows] = React.useState([]);
    const [degiroAccountAllRows, setDegiroAccountAllRows] = React.useState([]);
    const [degiroAccountStockRows, setDegiroAccountStockRows] = React.useState([]);
    const [degiroAccountDividendRows, setDegiroAccountDividendRows] = React.useState([]);
    const [degiroAccountCashRows, setDegiroAccountCashRows] = React.useState([]);
    const [degiroAccountExpandedRows, setDegiroAccountExpandedRows] = React.useState({});
    const [degiroCombinedOrderIds, setDegiroCombinedOrderIds] = React.useState([]);
    const [degiroRawParseFailures, setDegiroRawParseFailures] = React.useState([]);
    const [degiroUploadDuplicates, setDegiroUploadDuplicates] = React.useState([]);
    const [degiroUploadSucceeded, setDegiroUploadSucceeded] = React.useState(false);
    const [degiroIsSecurityLookupLoading, setDegiroIsSecurityLookupLoading] = React.useState(false);
    const [degiroMappedIsins, setDegiroMappedIsins] = React.useState([]);
    const [degiroUnmappedIsins, setDegiroUnmappedIsins] = React.useState([]);
    const [degiroStatus, setDegiroStatus] = React.useState(null);
    const [degiroErrors, setDegiroErrors] = React.useState({});
    const [degiroIsUploadPosting, setDegiroIsUploadPosting] = React.useState(false);
    const [customPortfolio, setCustomPortfolio] = React.useState('');
    const [customType, setCustomType] = React.useState('');
    const [customColumns, setCustomColumns] = React.useState([]);
    const [customInputMode, setCustomInputMode] = React.useState('paste');
    const [clipboardInput, setClipboardInput] = React.useState('');
    const [customFileText, setCustomFileText] = React.useState('');
    const [customFileName, setCustomFileName] = React.useState('');
    const [customStatus, setCustomStatus] = React.useState(null);
    const [customErrors, setCustomErrors] = React.useState({});
    const [customParsedRows, setCustomParsedRows] = React.useState([]);
    const [customRawParseFailures, setCustomRawParseFailures] = React.useState([]);
    const [customMappedIdentifiers, setCustomMappedIdentifiers] = React.useState([]);
    const [customUnmappedIdentifiers, setCustomUnmappedIdentifiers] = React.useState([]);
    const [customIdentifierToIsinMap, setCustomIdentifierToIsinMap] = React.useState({});
    const [customIsinToNameMap, setCustomIsinToNameMap] = React.useState({});
    const [customSecurityLookupType, setCustomSecurityLookupType] = React.useState('');
    const [customIsSecurityLookupLoading, setCustomIsSecurityLookupLoading] = React.useState(false);
    const [customIsUploadPosting, setCustomIsUploadPosting] = React.useState(false);
    const [customUploadDuplicates, setCustomUploadDuplicates] = React.useState([]);
    const [customUploadSucceeded, setCustomUploadSucceeded] = React.useState(false);
    const [isCustomColumnHelpOpen, setIsCustomColumnHelpOpen] = React.useState(false);

    React.useEffect(() => {
      if (portfolios.length !== 1) {
        return;
      }

      const onlyPortfolioId = String(portfolios[0].id);
      setDegiroPortfolio((current) => (current ? current : onlyPortfolioId));
      setCustomPortfolio((current) => (current ? current : onlyPortfolioId));
    }, [portfolios]);

    const customInputText = customInputMode === 'paste' ? clipboardInput : customFileText;

    function clearCustomUploadPreviewState() {
      setClipboardInput('');
      setCustomFileText('');
      setCustomFileName('');
      setCustomParsedRows([]);
      setCustomRawParseFailures([]);
      setCustomMappedIdentifiers([]);
      setCustomUnmappedIdentifiers([]);
      setCustomIdentifierToIsinMap({});
      setCustomIsinToNameMap({});
      setCustomSecurityLookupType('');
      setCustomIsSecurityLookupLoading(false);
      setCustomUploadDuplicates([]);
      setCustomUploadSucceeded(false);
      setCustomStatus(null);
      setCustomErrors((current) => {
        const next = Object.assign({}, current);
        next.clipboardInput = undefined;
        next.customFileName = undefined;
        return next;
      });
    }

    function handleCustomInputModeChange(nextMode) {
      if (nextMode === customInputMode) {
        return;
      }

      const hasPreview = customParsedRows.length > 0 || customRawParseFailures.length > 0;
      if (hasPreview) {
        clearCustomUploadPreviewState();
      }

      setCustomInputMode(nextMode);
    }

    function getDefaultColumnsForType(type) {
      return (UPLOAD_COLUMN_SETS[type] || []).slice();
    }

    const isCashCustomType = customType === TRANSACTION_TYPES.cash;
    const isDividendCustomType = customType === TRANSACTION_TYPES.dividend;
    const hasPortfolioColumn = customColumns.includes('Portfolio');

    const customPastePlaceholder = 'Paste row data with headers...';

    const customSampleRowText = React.useMemo(() => {
      if (!customType) {
        return '';
      }

      const sampleValueByColumn = {
        Date: '2026-01-15',
        Event: isCashCustomType ? 'Deposit' : (isDividendCustomType ? 'Dividend' : 'Buy'),
        ISIN: 'US0378331005',
        Quantity: '10',
        Price: '185.42',
        Currency: 'USD',
        Commission: '5.00',
        TaxPaid: isDividendCustomType ? '2.50' : '',
        Amount: isCashCustomType ? '3000' : '1854.20',
        FxRate: '1.0843',
        Country: 'US',
        Dividend: '25.00',
        Received: '22.50',
        Description: 'Apple Inc',
        Portfolio: 'My Portfolio'
      };

      const visibleColumns = customColumns.filter((column) => column !== 'Ignore');
      const headerRow = visibleColumns.join(',');
      const dataRow = visibleColumns.map((column) => (sampleValueByColumn[column] != null ? sampleValueByColumn[column] : '')).join(',');
      return `${headerRow}\n${dataRow}`;
    }, [customType, customColumns, isCashCustomType, isDividendCustomType]);

    const customVisibleColumns = React.useMemo(
      () => customColumns.filter((column) => column !== 'Ignore'),
      [customColumns]
    );

    const customPreviewColumns = React.useMemo(() => {
      if (customType !== TRANSACTION_TYPES.stock) {
        return customVisibleColumns;
      }

      const withoutIsin = customVisibleColumns.filter((column) => column !== 'ISIN');
      const stockColumns = [];
      let insertedStock = false;

      withoutIsin.forEach((column) => {
        stockColumns.push(column);
        if (!insertedStock && column === 'Event') {
          stockColumns.push('Stock');
          insertedStock = true;
        }
      });

      if (!insertedStock) {
        stockColumns.unshift('Stock');
      }

      return stockColumns;
    }, [customType, customVisibleColumns]);

    function isCustomStockNumericPreviewColumn(column) {
      return column === 'Quantity'
        || column === 'Price'
        || column === 'Commission'
        || column === 'TaxPaid';
    }

    function getCustomPreviewCellClass(row, column) {
      if (customType !== TRANSACTION_TYPES.stock) {
        return '';
      }

      if (column === 'Amount' && Number.isFinite(row.amount)) {
        return `txn-cell-amount ${row.amount < 0 ? 'txn-cell-amount-negative' : (row.amount > 0 ? 'txn-cell-amount-positive' : '')}`.trim();
      }

      if (isCustomStockNumericPreviewColumn(column)) {
        return 'txn-cell-amount';
      }

      return '';
    }

    function getCustomPreviewCellValue(row, column) {
      if (column === 'Stock') {
        const directIsin = normalizeIsinValue(row && row.isin);
        const identifierType = row && row.securityIdentifierType ? row.securityIdentifierType : customSecurityLookupType;
        const identifier = normalizeCustomIdentityValue(identifierType, row && row.securityIdentifier);
        const resolvedIsin = directIsin || (identifier ? customIdentifierToIsinMap[identifier] : '') || '';
        const mappedName = resolvedIsin ? customIsinToNameMap[normalizeIsinValue(resolvedIsin)] : '';
        return mappedName || row.name || row.isin || '-';
      }

      if (customType === TRANSACTION_TYPES.stock) {
        if (column === 'Amount' && Number.isFinite(row.amount)) {
          return formatNumberDisplay(row.amount, 2, 2);
        }

        if (column === 'Quantity' && Number.isFinite(row.quantity)) {
          return formatNumberDisplay(row.quantity, 0, 6);
        }

        if (column === 'Price' && Number.isFinite(row.price)) {
          return formatNumberDisplay(row.price, 2, 6);
        }

        if (column === 'Commission' && Number.isFinite(row.commission)) {
          return formatNumberDisplay(row.commission, 2, 2);
        }

        if (column === 'TaxPaid' && Number.isFinite(row.taxPaid)) {
          return formatNumberDisplay(row.taxPaid, 2, 2);
        }
      }

      const rawValue = row.previewValues[column];
      if (rawValue === '' || rawValue == null) {
        return '-';
      }

      return String(rawValue);
    }

    const customUnmappedIdentifierSet = React.useMemo(
      () => new Set(customUnmappedIdentifiers.map((identifier) => normalizeCustomIdentityValue(customSecurityLookupType, identifier)).filter(Boolean)),
      [customUnmappedIdentifiers, customSecurityLookupType]
    );

    const customValidRows = React.useMemo(
      () => customParsedRows.filter((row) => (
        customType === TRANSACTION_TYPES.cash
        || !row.securityIdentifier
        || !customUnmappedIdentifierSet.has(normalizeCustomIdentityValue(row.securityIdentifierType, row.securityIdentifier))
      )),
      [customParsedRows, customType, customUnmappedIdentifierSet]
    );

    const customValidSummary = React.useMemo(() => {
      const summary = {
        totalRows: customValidRows.length,
        stockRows: 0,
        dividendRows: 0,
        cashRows: 0
      };

      customValidRows.forEach((row) => {
        if (row.type === TRANSACTION_TYPES.stock) {
          summary.stockRows += 1;
          return;
        }

        if (row.type === TRANSACTION_TYPES.dividend) {
          summary.dividendRows += 1;
          return;
        }

        if (row.type === TRANSACTION_TYPES.cash) {
          summary.cashRows += 1;
        }
      });

      return summary;
    }, [customValidRows]);

    const degiroUnmappedIsinSet = React.useMemo(
      () => new Set(degiroUnmappedIsins.map((isin) => normalizeIsinValue(isin)).filter(Boolean)),
      [degiroUnmappedIsins]
    );

    const hasMappedDegiroSecurities = degiroMappedIsins.length > 0;

    const degiroAccountValidRows = React.useMemo(
      () => degiroAccountAllRows.filter((row) => (
        !isRowSupportedBySecurityMapping(row) || isIsinMappedForRow(row, degiroUnmappedIsinSet)
      )),
      [degiroAccountAllRows, degiroUnmappedIsinSet]
    );

    const degiroAccountValidSummary = React.useMemo(() => {
      const summary = {
        totalRows: degiroAccountValidRows.length,
        stockRows: 0,
        dividendRows: 0,
        cashRows: 0
      };

      degiroAccountValidRows.forEach((row) => {
        if (row.type === TRANSACTION_TYPES.stock) {
          summary.stockRows += 1;
          return;
        }

        if (row.type === TRANSACTION_TYPES.dividend) {
          summary.dividendRows += 1;
          return;
        }

        if (row.type === TRANSACTION_TYPES.cash) {
          summary.cashRows += 1;
        }
      });

      return summary;
    }, [degiroAccountValidRows]);

    const canUploadDegiroTransactionFile = !degiroUploadSucceeded
      && !degiroIsSecurityLookupLoading
      && hasMappedDegiroSecurities
      && degiroFileKind === 'transaction';

    const canUploadDegiroAccountStatementFile = !degiroUploadSucceeded
      && !degiroIsSecurityLookupLoading
      && degiroFileKind === 'account-statement'
      && degiroAccountValidSummary.totalRows > 0;

    const isScreenBusy = degiroIsUploadPosting || customIsUploadPosting;
    const canUploadCustomTransactions = !customUploadSucceeded
      && !customIsSecurityLookupLoading
      && customValidSummary.totalRows > 0
      && customType;

    async function checkDegiroSecuritiesByIsins(rows) {
      const isins = getUniqueIsinsFromRows(rows);
      setDegiroMappedIsins([]);
      setDegiroUnmappedIsins([]);

      if (isins.length === 0) {
        return;
      }

      setDegiroIsSecurityLookupLoading(true);
      try {
        const headers = getAuthorizedJsonHeaders();
        const response = await fetch(`/api/securities?isins=${encodeURIComponent(isins.join(','))}`, {
          method: 'GET',
          headers
        });

        if (response.status === 401 || response.status === 403) {
          handleUnauthorizedApiResponse();
          return;
        }

        if (!response.ok) {
          const payload = await response.json().catch(() => ({}));
          const errorCode = payload && payload.code != null ? payload.code : 'Unknown';
          setDegiroStatus({ type: 'error', message: `Securities lookup failed. Technical Code = ${errorCode}.` });
          return;
        }

        const payload = await response.json().catch(() => ({}));
        const securities = Array.isArray(payload.securities) ? payload.securities : [];
        const mappedIsins = Array.from(new Set(
          securities
            .map((security) => normalizeIsinValue(security && security.isin))
            .filter(Boolean)
        ));

        const responseUnmapped = Array.isArray(payload.unmapped) ? payload.unmapped : [];
        const explicitUnmappedIsins = responseUnmapped
          .map((value) => normalizeIsinValue(value))
          .filter(Boolean);

        const mappedSet = new Set(mappedIsins);
        const explicitUnmappedSet = new Set(explicitUnmappedIsins);
        const inferredUnmapped = isins.filter((isin) => !mappedSet.has(isin) && !explicitUnmappedSet.has(isin));
        const unmappedIsins = Array.from(new Set(explicitUnmappedIsins.concat(inferredUnmapped)));

        setDegiroMappedIsins(mappedIsins);
        setDegiroUnmappedIsins(unmappedIsins);
      } catch (error) {
        setDegiroStatus({ type: 'error', message: 'Securities lookup failed. Please try again.' });
      } finally {
        setDegiroIsSecurityLookupLoading(false);
      }
    }

    async function checkCustomSecuritiesByIdentifiers(rows, identifierType) {
      const normalizedType = identifierType || '';
      const values = Array.isArray(rows)
        ? rows
          .map((row) => normalizeCustomIdentityValue(normalizedType, row && row.securityIdentifier))
          .filter(Boolean)
        : [];

      setCustomSecurityLookupType(normalizedType);
      setCustomMappedIdentifiers([]);
      setCustomUnmappedIdentifiers([]);
      setCustomIdentifierToIsinMap({});
      setCustomIsinToNameMap({});

      if (customType === TRANSACTION_TYPES.cash || !normalizedType) {
        setCustomIsSecurityLookupLoading(false);
        return {
          lookupType: normalizedType,
          mappedIdentifiers: [],
          unmappedIdentifiers: []
        };
      }

      if (values.length === 0) {
        setCustomIsSecurityLookupLoading(false);
        if (Array.isArray(rows) && rows.length > 0) {
          // Rows may have no ISIN (e.g. Description-only Dividend rows); skip the lookup instead of blocking upload.
          return {
            lookupType: normalizedType,
            mappedIdentifiers: [],
            unmappedIdentifiers: []
          };
        }
        setCustomStatus({ type: 'error', message: 'No valid securities were found for this custom file.' });
        return null;
      }

      setCustomIsSecurityLookupLoading(true);
      try {
        const paramsName = normalizedType === 'Ticker'
          ? 'tickers'
          : (normalizedType === 'Name' ? 'names' : 'isins');
        const headers = getAuthorizedJsonHeaders();
        const response = await fetch(`/api/securities?${paramsName}=${encodeURIComponent(Array.from(new Set(values)).join(','))}`, {
          method: 'GET',
          headers
        });

        if (response.status === 401 || response.status === 403) {
          handleUnauthorizedApiResponse();
          return;
        }

        if (!response.ok) {
          const payload = await response.json().catch(() => ({}));
          const errorCode = payload && payload.code != null ? payload.code : 'Unknown';
          setCustomStatus({ type: 'error', message: `Securities lookup failed. Technical Code = ${errorCode}.` });
          return null;
        }

        const payload = await response.json().catch(() => ({}));
        const securities = Array.isArray(payload.securities) ? payload.securities : [];
        const identifierToIsinMap = {};
        const isinToNameMap = {};
        securities.forEach((security) => {
          const isinValue = normalizeIsinValue(security && security.isin);
          if (!isinValue) {
            return;
          }

          const securityName = String(security && security.name ? security.name : '').trim();
          if (securityName) {
            isinToNameMap[isinValue] = securityName;
          }

          const identifierValue = normalizeCustomIdentityValue(
            normalizedType,
            normalizedType === 'Ticker'
              ? (security && security.ticker)
              : (normalizedType === 'Name' ? (security && security.name) : (security && security.isin))
          );

          if (identifierValue) {
            identifierToIsinMap[identifierValue] = isinValue;
          }
        });
        const mappedIdentifiers = Array.from(new Set(
          securities
            .map((security) => normalizeCustomIdentityValue(normalizedType, normalizedType === 'Ticker'
              ? (security && security.ticker)
              : (normalizedType === 'Name' ? (security && security.name) : (security && security.isin))))
            .filter(Boolean)
        ));

        const responseUnmapped = Array.isArray(payload.unmapped) ? payload.unmapped : [];
        const explicitUnmappedIdentifiers = responseUnmapped
          .map((value) => normalizeCustomIdentityValue(normalizedType, value))
          .filter(Boolean);

        const mappedSet = new Set(mappedIdentifiers);
        const explicitUnmappedSet = new Set(explicitUnmappedIdentifiers);
        const inferredUnmapped = values.filter((identifier) => !mappedSet.has(identifier) && !explicitUnmappedSet.has(identifier));
        const unmappedIdentifiers = Array.from(new Set(explicitUnmappedIdentifiers.concat(inferredUnmapped)));

        setCustomMappedIdentifiers(mappedIdentifiers);
        setCustomUnmappedIdentifiers(unmappedIdentifiers);
        setCustomIdentifierToIsinMap(identifierToIsinMap);
        setCustomIsinToNameMap(isinToNameMap);
        return {
          lookupType: normalizedType,
          mappedIdentifiers,
          unmappedIdentifiers
        };
      } catch (error) {
        setCustomStatus({ type: 'error', message: 'Securities lookup failed. Please try again.' });
        return null;
      } finally {
        setCustomIsSecurityLookupLoading(false);
      }
    }

    React.useEffect(() => {
      let active = true;

      async function parseCustomInput() {
        const rawText = customInputText.trim();
        if (!rawText) {
          setCustomParsedRows([]);
          setCustomRawParseFailures([]);
          setCustomMappedIdentifiers([]);
          setCustomUnmappedIdentifiers([]);
          setCustomIdentifierToIsinMap({});
          setCustomIsinToNameMap({});
          setCustomSecurityLookupType('');
          setCustomIsSecurityLookupLoading(false);
          setCustomUploadDuplicates([]);
          setCustomUploadSucceeded(false);
          setCustomStatus(null);
          return;
        }

        const rawLines = rawText.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
        const firstLine = rawLines[0] || '';
        const firstLineDelimiter = detectCustomInputDelimiter(firstLine);
        const firstLineCells = parseDelimitedLine(firstLine, firstLineDelimiter);
        const guessedLayout = !customType ? guessCustomUploadLayoutFromHeaderCells(firstLineCells) : null;

        if (!customType && guessedLayout) {
          setCustomType(guessedLayout.type);
          setCustomColumns(guessedLayout.columns);
          setCustomStatus({
            type: 'success',
            message: `${guessedLayout.type} layout detected from header row.`
          });
          return;
        }

        if (!customType) {
          setCustomParsedRows([]);
          setCustomRawParseFailures([]);
          setCustomMappedIdentifiers([]);
          setCustomUnmappedIdentifiers([]);
          setCustomIdentifierToIsinMap({});
          setCustomIsinToNameMap({});
          setCustomSecurityLookupType('');
          setCustomIsSecurityLookupLoading(false);
          setCustomUploadDuplicates([]);
          setCustomUploadSucceeded(false);
          return;
        }

        // A header row is mandatory for all Transaction Types and drives the column mapping.
        const mapping = buildCorrectedColumnsFromHeaderCells(firstLineCells, customType);
        if (mapping.missingColumns.length > 0) {
          setCustomParsedRows([]);
          setCustomRawParseFailures([]);
          setCustomMappedIdentifiers([]);
          setCustomUnmappedIdentifiers([]);
          setCustomIdentifierToIsinMap({});
          setCustomIsinToNameMap({});
          setCustomSecurityLookupType('');
          setCustomIsSecurityLookupLoading(false);
          setCustomUploadDuplicates([]);
          setCustomUploadSucceeded(false);
          setCustomStatus({
            type: 'error',
            message: `Header row is missing required columns: ${mapping.missingColumns.join(', ')}`
          });
          return;
        }

        const selectedColumns = Array.isArray(customColumns) ? customColumns : [];
        const hasChangedColumns = mapping.correctedColumns.length !== selectedColumns.length
          || mapping.correctedColumns.some((column, index) => column !== selectedColumns[index]);

        if (hasChangedColumns) {
          setCustomColumns(mapping.correctedColumns);
          return;
        }

        const parsed = parseCustomUploadTextRows(customInputText, customType, customColumns, portfolios);
        if (!active) {
          return;
        }

        setCustomParsedRows(parsed.parsedRows);
        setCustomRawParseFailures(parsed.failedRawRows);
        setCustomUploadDuplicates([]);
        setCustomUploadSucceeded(false);

        if (parsed.parsedRows.length === 0) {
          setCustomMappedIdentifiers([]);
          setCustomUnmappedIdentifiers([]);
          setCustomIdentifierToIsinMap({});
          setCustomIsinToNameMap({});
          setCustomSecurityLookupType(parsed.lookupType || '');
          setCustomIsSecurityLookupLoading(false);
          setCustomStatus(parsed.failedRawRows.length > 0
            ? {
              type: 'error',
              message: 'No valid rows were found to upload.',
              hint: explainCustomRowParseFailure(parsed.failedRawRows[0], customType, customColumns, portfolios)
            }
            : null);
          return;
        }

        if (customType === TRANSACTION_TYPES.cash) {
          setCustomSecurityLookupType('');
          setCustomMappedIdentifiers([]);
          setCustomUnmappedIdentifiers([]);
          setCustomIdentifierToIsinMap({});
          setCustomIsinToNameMap({});
          setCustomIsSecurityLookupLoading(false);
          setCustomStatus({
            type: 'success',
            message: `${parsed.parsedRows.length} valid transactions found in custom upload ready to upload. Duplicates will be ignored.`
          });
          return;
        }

        setCustomStatus({
          type: 'success',
          message: `Checking securities for identifiers in this custom upload...`
        });

        const lookupResult = await checkCustomSecuritiesByIdentifiers(parsed.parsedRows, parsed.lookupType);

        if (!active) {
          return;
        }

        const lookupUnmappedIdentifiers = Array.isArray(lookupResult && lookupResult.unmappedIdentifiers)
          ? lookupResult.unmappedIdentifiers
          : [];
        const lookupType = lookupResult && lookupResult.lookupType ? lookupResult.lookupType : parsed.lookupType;

        const validAfterLookup = parsed.parsedRows.filter((row) => (
          !row.securityIdentifier
          || !lookupUnmappedIdentifiers.includes(normalizeCustomIdentityValue(row.securityIdentifierType || lookupType, row.securityIdentifier))
        ));
        const summary = {
          totalRows: validAfterLookup.length,
          stockRows: 0,
          dividendRows: 0,
          cashRows: 0
        };

        validAfterLookup.forEach((row) => {
          if (row.type === TRANSACTION_TYPES.stock) {
            summary.stockRows += 1;
            return;
          }

          if (row.type === TRANSACTION_TYPES.dividend) {
            summary.dividendRows += 1;
            return;
          }

          if (row.type === TRANSACTION_TYPES.cash) {
            summary.cashRows += 1;
          }
        });

        setCustomSecurityLookupType(lookupType || '');
        setCustomMappedIdentifiers(Array.isArray(lookupResult && lookupResult.mappedIdentifiers) ? lookupResult.mappedIdentifiers : []);
        setCustomUnmappedIdentifiers(lookupUnmappedIdentifiers);

        const readyCount = customType === TRANSACTION_TYPES.dividend ? summary.dividendRows : summary.stockRows;
        setCustomStatus({
          type: 'success',
          message: `${readyCount} ${customType} Transactions ready to upload. Duplicates will be ignored.`
        });
      }

      parseCustomInput();

      return () => {
        active = false;
      };
    }, [customType, customColumns, customInputText, portfolios]);

    function validateDegiroUpload() {
      const nextErrors = {};

      if (!degiroPortfolio) {
        nextErrors.degiroPortfolio = 'Portfolio is required.';
      }

      if (!degiroFileName) {
        nextErrors.degiroFileName = 'Degiro Transaction File is required.';
      }

      if (degiroFileName && !degiroFileKind) {
        nextErrors.degiroFileName = INVALID_DEGIRO_FILE_MESSAGE;
      }

      setDegiroErrors(nextErrors);
      if (Object.keys(nextErrors).length > 0) {
        if (nextErrors.degiroFileName === INVALID_DEGIRO_FILE_MESSAGE) {
          setDegiroStatus({ type: 'error', message: INVALID_DEGIRO_FILE_MESSAGE });
        } else {
          const portfolioHint = nextErrors.degiroPortfolio ? ' Portfolio is a required field.' : '';
          setDegiroStatus({ type: 'error', message: `Please resolve the highlighted fields before continuing.${portfolioHint}` });
        }
        return false;
      }

      setDegiroErrors({});
      return true;
    }

    function validateCustomUpload() {
      const nextErrors = {};

      if (!hasPortfolioColumn && !customPortfolio) {
        nextErrors.customPortfolio = 'Portfolio is required.';
      }

      if (!customType) {
        nextErrors.customType = 'Transaction Type is required.';
      }

      setCustomErrors((current) => Object.assign({}, current, nextErrors));

      if (Object.keys(nextErrors).length > 0) {
        const portfolioHint = nextErrors.customPortfolio ? ' Portfolio is a required field.' : '';
        setCustomStatus({ type: 'error', message: `Please resolve the highlighted fields before continuing.${portfolioHint}` });
        return false;
      }

      setCustomErrors((current) => {
        const next = Object.assign({}, current);
        next.customPortfolio = undefined;
        next.customType = undefined;
        return next;
      });

      return true;
    }

    async function uploadCustomTransactions() {
      const isValid = validateCustomUpload();
      if (!isValid) {
        return;
      }

      if (!customType || customIsSecurityLookupLoading || customParsedRows.length === 0) {
        return;
      }

      setCustomUploadDuplicates([]);
      setCustomIsUploadPosting(true);
      try {
        const rowsToUpload = customValidRows;
        if (rowsToUpload.length === 0) {
          setCustomStatus({ type: 'error', message: 'No rows with mapped securities are available to upload.' });
          return;
        }

        const resolveCustomRowIsinForUpload = (row) => {
          const directIsin = normalizeIsinValue(row && row.isin);
          if (directIsin) {
            return directIsin;
          }

          const identifierType = row && row.securityIdentifierType ? row.securityIdentifierType : customSecurityLookupType;
          const identifierValue = normalizeCustomIdentityValue(identifierType, row && row.securityIdentifier);
          if (!identifierValue) {
            return null;
          }

          return customIdentifierToIsinMap[identifierValue] || null;
        };

        const buildStockTransactions = (rows) => rows
          .filter((row) => row.type === TRANSACTION_TYPES.stock)
          .map((row) => ({
            date: row.date,
            event: row.event,
            name: row.name,
            isin: resolveCustomRowIsinForUpload(row),
            mic: null,
            quantity: row.quantity,
            price: row.price,
            currency: row.currency,
            fxFee: row.fxFee != null ? row.fxFee : 0,
            fxRatePreFee: row.fxRatePreFee,
            fxRate: row.fxRate,
            commission: row.commission != null ? row.commission : 0,
            amount: row.amount,
            taxPaid: row.taxPaid != null ? row.taxPaid : 0,
            orderId: null
          }));

        const buildDividendTransactions = (rows) => rows
          .filter((row) => row.type === TRANSACTION_TYPES.dividend)
          .map((row) => ({
            date: row.date,
            event: row.event,
            name: row.name,
            isin: resolveCustomRowIsinForUpload(row),
            description: row.description,
            currency: row.currency,
            fxRate: row.fxRate,
            country: row.country,
            declaredInSourceCurrency: row.declaredInSourceCurrency,
            taxPaidInSourceCurrency: row.taxPaidInSourceCurrency,
            amountInSourceCurrency: row.amountInSourceCurrency,
            declaredConverted: row.declaredConverted,
            taxPaidConverted: row.taxPaidConverted,
            amount: row.amount
          }));

        const buildCashTransactions = (rows) => rows
          .filter((row) => row.type === TRANSACTION_TYPES.cash)
          .map((row) => ({
            date: row.date,
            event: row.event,
            description: row.description,
            isin: null,
            amount: row.amount
          }));

        const uploadSource = UPLOAD_SOURCES.CUSTOM_UPLOAD;
        const headers = Object.assign({}, getAuthorizedJsonHeaders(), {
          'Content-Type': 'application/json'
        });

        // Rows may each carry their own portfolio (from an optional Portfolio column), so group before posting.
        const rowsByPortfolioId = new Map();
        rowsToUpload.forEach((row) => {
          const portfolioId = row.portfolioId || customPortfolio;
          if (!rowsByPortfolioId.has(portfolioId)) {
            rowsByPortfolioId.set(portfolioId, []);
          }
          rowsByPortfolioId.get(portfolioId).push(row);
        });

        setCustomStatus({ type: 'success', message: `Uploading Custom ${customType} transactions...` });

        let totalInsertedCount = 0;
        let totalInsertedStockCount = 0;
        let totalInsertedDividendCount = 0;
        let totalInsertedCashCount = 0;
        let allDuplicates = [];

        for (const [portfolioId, groupRows] of rowsByPortfolioId) {
          const response = await fetch('/api/transactions', {
            method: 'POST',
            headers,
            body: JSON.stringify({
              uploadSource,
              portfolioId,
              stockTransactions: buildStockTransactions(groupRows),
              dividendTransactions: buildDividendTransactions(groupRows),
              cashTransactions: buildCashTransactions(groupRows)
            })
          });

          if (response.status === 401 || response.status === 403) {
            handleUnauthorizedApiResponse();
            return;
          }

          const payload = await response.json().catch(() => ({}));
          if (!response.ok) {
            const errorCode = response.status === 413
              ? 9413
              : (payload && payload.code != null ? payload.code : 'Unknown');
            setCustomStatus({ type: 'error', message: `Upload failed. Technical Code = ${errorCode}.` });
            return;
          }

          totalInsertedCount += Number.isFinite(payload.insertedCount) ? payload.insertedCount : 0;
          totalInsertedStockCount += Number.isFinite(payload.insertedStockCount) ? payload.insertedStockCount : 0;
          totalInsertedDividendCount += Number.isFinite(payload.insertedDividendCount) ? payload.insertedDividendCount : 0;
          totalInsertedCashCount += Number.isFinite(payload.insertedCashCount) ? payload.insertedCashCount : 0;
          allDuplicates = allDuplicates.concat(Array.isArray(payload.duplicates) ? payload.duplicates : []);
        }

        setCustomUploadDuplicates(allDuplicates);
        setCustomUploadSucceeded(true);
        setCustomStatus({
          type: 'success',
          message: `Upload complete. Inserted ${totalInsertedCount} transactions from custom ${customType} data (${totalInsertedStockCount} stock, ${totalInsertedDividendCount} dividend, ${totalInsertedCashCount} cash). ${allDuplicates.length} duplicates skipped.`
        });
      } catch (error) {
        setCustomStatus({ type: 'error', message: 'Upload failed. Please try again.' });
      } finally {
        setCustomIsUploadPosting(false);
      }
    }

    function mapAccountStatementStockRowsForUpload(rows) {
      const values = Array.isArray(rows) ? rows : [];
      return values
        .filter((row) => row && row.type === TRANSACTION_TYPES.stock)
        .map((row) => ({
          date: row.date,
          event: row.event,
          name: row.description,
          isin: row.isin,
          mic: null,
          quantity: row.quantity,
          price: row.price,
          currency: row.currency,
          fxFee: ZERO_COST_STOCK_EVENTS.has(row.event) ? null : 0,
          fxRatePreFee: null,
          fxRate: ZERO_COST_STOCK_EVENTS.has(row.event) ? null : (Number.isFinite(row.fxRate) ? row.fxRate : 1),
          commission: Number.isFinite(row.commission) ? row.commission : 0,
          amount: row.amount,
          orderId: row.orderId || null
        }))
        .filter((row) => (
          row.date
          && row.event
          && row.name
          && row.isin
          && Number.isFinite(row.quantity)
          && Number.isFinite(row.price)
          && Number.isFinite(row.amount)
        ));
    }

    function mapAccountStatementDividendRowsForUpload(rows) {
      const values = Array.isArray(rows) ? rows : [];
      return values
        .filter((row) => row && row.type === TRANSACTION_TYPES.dividend)
        .map((row) => ({
          date: row.date,
          event: row.event,
          name: row.description,
          isin: row.isin,
          currency: row.currency,
          fxRate: row.fxRate,
          country: row.country,
          declaredInSourceCurrency: row.declaredInSourceCurrency,
          taxPaidInSourceCurrency: row.taxPaidInSourceCurrency,
          amountInSourceCurrency: row.amountInSourceCurrency,
          declaredConverted: row.declaredConverted,
          taxPaidConverted: row.taxPaidConverted,
          amount: row.amount
        }))
        .filter((row) => (
          row.date
          && row.event
          && row.name
          && row.isin
          && row.currency
          && row.country
          && Number.isFinite(row.fxRate)
          && Number.isFinite(row.declaredInSourceCurrency)
          && Number.isFinite(row.taxPaidInSourceCurrency)
          && Number.isFinite(row.amountInSourceCurrency)
          && Number.isFinite(row.declaredConverted)
          && Number.isFinite(row.taxPaidConverted)
          && Number.isFinite(row.amount)
        ));
    }

    function mapAccountStatementCashRowsForUpload(rows) {
      const values = Array.isArray(rows) ? rows : [];
      return values
        .filter((row) => row && row.type === TRANSACTION_TYPES.cash)
        .map((row) => ({
          date: row.date,
          event: row.event,
          description: row.description,
          isin: row.isin || null,
          amount: row.amount
        }))
        .filter((row) => (
          row.date
          && row.event
          && Number.isFinite(row.amount)
        ));
    }

    async function uploadDegiroTransactions() {
      const isValid = validateDegiroUpload();
      if (!isValid) {
        return;
      }

      if (!degiroPortfolio || !degiroFileName || !degiroFileKind) {
        return;
      }

      setDegiroUploadDuplicates([]);

      let stockTransactions = [];
      let dividendTransactions = [];
      let cashTransactions = [];
      let uploadSource = UPLOAD_SOURCES.DEGIRO_TRANSACTION;
      if (degiroFileKind === 'transaction') {
        if (!hasMappedDegiroSecurities) {
          setDegiroStatus({ type: 'error', message: 'No valid securities were found for this Degiro file.' });
          return;
        }

        stockTransactions = degiroStockRows
          .filter((row) => !degiroUnmappedIsinSet.has(normalizeIsinValue(row.isin)))
          .map((row) => ({
            date: row.date,
            event: row.event,
            name: row.name,
            isin: row.isin,
            mic: row.mic,
            quantity: row.quantity,
            price: row.price,
            currency: row.currency,
            fxFee: row.fxFee,
            fxRatePreFee: row.fxRatePreFee,
            fxRate: row.fxRate,
            commission: row.commission,
            amount: row.amount,
            orderId: row.orderId
          }));
      }

      if (degiroFileKind === 'account-statement') {
        uploadSource = UPLOAD_SOURCES.DEGIRO_ACCOUNT_STATEMENT;
        stockTransactions = mapAccountStatementStockRowsForUpload(degiroAccountValidRows);
        dividendTransactions = mapAccountStatementDividendRowsForUpload(degiroAccountValidRows);
        cashTransactions = mapAccountStatementCashRowsForUpload(degiroAccountValidRows);
      }

      if (stockTransactions.length === 0 && dividendTransactions.length === 0 && cashTransactions.length === 0) {
        if (degiroFileKind === 'account-statement') {
          setDegiroStatus({
            type: 'success',
            message: 'No valid rows were found to upload. Account Statement parsing preview is complete.'
          });
          return;
        }

        setDegiroStatus({ type: 'error', message: 'No rows with mapped securities are available to upload.' });
        return;
      }

      setDegiroIsUploadPosting(true);
      try {
        setDegiroStatus({ type: 'success', message: 'Uploading Degiro Transaction file...' });
        const headers = Object.assign({}, getAuthorizedJsonHeaders(), {
          'Content-Type': 'application/json'
        });

        const response = await fetch('/api/transactions', {
          method: 'POST',
          headers,
          body: JSON.stringify({
            uploadSource,
            portfolioId: degiroPortfolio,
            stockTransactions,
            dividendTransactions,
            cashTransactions
          })
        });

        if (response.status === 401 || response.status === 403) {
          handleUnauthorizedApiResponse();
          return;
        }

        const payload = await response.json().catch(() => ({}));
        if (!response.ok) {
          const errorCode = response.status === 413
            ? 9413
            : (payload && payload.code != null ? payload.code : 'Unknown');
          setDegiroStatus({ type: 'error', message: `Upload failed. Technical Code = ${errorCode}.` });
          return;
        }

        const insertedCount = Number.isFinite(payload.insertedCount) ? payload.insertedCount : 0;
        const duplicates = Array.isArray(payload.duplicates) ? payload.duplicates : [];
        const duplicateCount = duplicates.length;
        setDegiroUploadDuplicates(duplicates);
        setDegiroUploadSucceeded(true);
        if (degiroFileKind === 'account-statement') {
          const summaryBits = [
            `${degiroAccountValidSummary.totalRows} valid rows parsed`,
            `${degiroAccountValidSummary.stockRows} stock`,
            `${degiroAccountValidSummary.dividendRows} dividend`,
            `${degiroAccountValidSummary.cashRows} cash`
          ];
          const insertedStockCount = Number.isFinite(payload.insertedStockCount) ? payload.insertedStockCount : 0;
          const insertedDividendCount = Number.isFinite(payload.insertedDividendCount) ? payload.insertedDividendCount : 0;
          const insertedCashCount = Number.isFinite(payload.insertedCashCount) ? payload.insertedCashCount : 0;
          setDegiroStatus({
            type: 'success',
            message: `Upload complete. Inserted ${insertedCount} transactions from account statement (${insertedStockCount} stock, ${insertedDividendCount} dividend, ${insertedCashCount} cash). ${duplicateCount} duplicates skipped. ${summaryBits.join(', ')}.`
          });
          return;
        }

        setDegiroStatus({
          type: 'success',
          message: `Upload complete. Inserted ${insertedCount} stock transactions. ${duplicateCount} duplicates skipped.`
        });
      } catch (error) {
        setDegiroStatus({ type: 'error', message: 'Upload failed. Please try again.' });
      } finally {
        setDegiroIsUploadPosting(false);
      }
    }

    async function onDegiroFileChange(event) {
      const file = event.target.files && event.target.files[0];
      setDegiroFileName(file ? file.name : '');
      setDegiroFileKind(null);
      setDegiroStockRows([]);
      setDegiroAccountAllRows([]);
      setDegiroAccountStockRows([]);
      setDegiroAccountDividendRows([]);
      setDegiroAccountCashRows([]);
      setDegiroAccountExpandedRows({});
      setDegiroCombinedOrderIds([]);
      setDegiroRawParseFailures([]);
      setDegiroUploadDuplicates([]);
      setDegiroUploadSucceeded(false);
      setDegiroMappedIsins([]);
      setDegiroUnmappedIsins([]);
      setDegiroIsSecurityLookupLoading(false);

      if (degiroErrors.degiroFileName) {
        setDegiroErrors((current) => {
          const next = Object.assign({}, current);
          next.degiroFileName = undefined;
          return next;
        });
      }

      if (!file) {
        setDegiroStatus(null);
        return;
      }

      if (!file.name.toLowerCase().endsWith('.csv')) {
        setDegiroStatus({ type: 'error', message: INVALID_DEGIRO_FILE_MESSAGE });
        return;
      }

      try {
        const csvText = await file.text();
        const rawLines = csvText.split(/\r?\n/).filter((line) => line.trim() !== '');
        if (rawLines.length === 0) {
          setDegiroStatus({ type: 'error', message: INVALID_DEGIRO_FILE_MESSAGE });
          return;
        }

        const headerCells = parseCsvLine(rawLines[0]);
        if (headersMatch(headerCells, DEGIRO_TRANSACTION_HEADERS)) {
          const parsedRows = [];
          const failedRows = [];

          for (let lineIndex = 1; lineIndex < rawLines.length; lineIndex += 1) {
            const rawCsv = rawLines[lineIndex];
            const cells = parseCsvLine(rawCsv);
            if (cells.every((value) => String(value || '').trim() === '')) {
              continue;
            }

            const parsed = parseDegiroTransactionRow(cells, rawCsv);
            if (parsed.success) {
              parsedRows.push(parsed.value);
            } else {
              failedRows.push(parsed.rawCsv);
            }
          }

          const combined = combineParsedDegiroTransactionRows(parsedRows);

          setDegiroFileKind('transaction');
          setDegiroStockRows(combined.normalizedRows);
          setDegiroCombinedOrderIds(combined.combinedOrderIds);
          setDegiroRawParseFailures(failedRows.concat(combined.failedRawRows));
          setDegiroStatus({ type: 'success', message: `${combined.normalizedRows.length} Stock Transactions found in Degiro Transaction file ready to upload. Duplicates will be ignored.` });
          await checkDegiroSecuritiesByIsins(combined.normalizedRows);
          return;
        }

        if (headersMatch(headerCells, DEGIRO_ACCOUNT_HEADERS)) {
          const parsedRows = [];
          const failedRows = [];

          for (let lineIndex = 1; lineIndex < rawLines.length; lineIndex += 1) {
            const rawCsv = rawLines[lineIndex];
            const cells = parseCsvLine(rawCsv);
            if (cells.every((value) => String(value || '').trim() === '')) {
              continue;
            }

            const parsed = parseDegiroAccountStatementRow(cells, rawCsv);
            if (parsed.date && parsed.dateValue && parsed.description) {
              parsedRows.push(parsed);
            } else {
              failedRows.push(rawCsv);
            }
          }

          const parsedResult = parseDegiroAccountStatementRows(parsedRows);

          setDegiroFileKind('account-statement');
          setDegiroStockRows([]);
          setDegiroCombinedOrderIds([]);
          setDegiroAccountAllRows(parsedResult.allRows);
          setDegiroAccountStockRows(parsedResult.stockRows);
          setDegiroAccountDividendRows(parsedResult.dividendRows);
          setDegiroAccountCashRows(parsedResult.cashRows);
          setDegiroRawParseFailures(failedRows.concat(parsedResult.failedRawRows));

          setDegiroStatus({
            type: 'success',
            message: `${parsedResult.allRows.length} transactions found in Degiro Account Statement file ready to upload (${parsedResult.stockRows.length} stock, ${parsedResult.dividendRows.length} dividend, ${parsedResult.cashRows.length} cash). Duplicates will be ignored.`
          });

          const rowsToLookup = parsedResult.stockRows.concat(parsedResult.dividendRows);
          await checkDegiroSecuritiesByIsins(rowsToLookup);
          return;
        }

        setDegiroStatus({ type: 'error', message: INVALID_DEGIRO_FILE_MESSAGE });
      } catch (error) {
        setDegiroStatus({ type: 'error', message: INVALID_DEGIRO_FILE_MESSAGE });
      }
    }

    return (
      <div className="content auth-shell" style={{ marginTop: '0.5rem' }} aria-busy={isScreenBusy}>
        {typeof BlockingScreenLoader === 'function' ? (
          <BlockingScreenLoader
            isActive={isScreenBusy}
            title="Uploading Transactions"
            message="Please wait."
          />
        ) : null}

        <section className="hero-card upload-hero-card">
          <div>
            <h1 className="hero-title" style={{ fontSize: 'clamp(1.8rem, 2.4vw, 2.4rem)' }}>Upload Transactions</h1>
            <p className="hero-copy">Upload a Degiro extract or define your own custom format.</p>
          </div>
          <div className="hero-actions upload-hero-actions">
            <button className="secondary-btn" onClick={() => navigate('/transactions')}>Back to Transactions</button>
          </div>
        </section>

        <section className="upload-grid">
          <article className="panel-card upload-panel upload-panel-degiro">
            <header className="upload-panel-header">
              <span className="upload-vendor-icon" aria-hidden="true">
                <img className="upload-vendor-logo" src="/images/degiro.svg" alt="" />
              </span>
              <div>
                <h3>Degiro Transaction Upload</h3>
                <p className="muted">Upload a Degiro file using one of the following supported formats:</p>
              </div>
            </header>

            <section className="guidance" aria-label="Degiro file guidance">
              <div className="upload-table-wrap">
                <table className="transactions-table guidance-table">
                  <thead>
                    <tr>
                      <th>Degiro File Type</th>
                      <th>Description</th>
                      <th>Location (in Degiro)</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr>
                      <td>Degiro Transaction File</td>
                      <td>
                        <ul>
                          <li>Contains stock transactions only.</li>
                          <li>Supports capturing Fx Fees for stock transactions.</li>
                        </ul>
                      </td>
                      <td>
                        <ol>
                          <li>Click Inbox, then click <b>Transactions</b></li>
                          <li>Select Date Range, then click Download and choose .csv</li>
                        </ol>
                      </td>
                    </tr>
                    <tr>
                      <td>Degiro Account Statement File</td>
                      <td>
                        <ul>
                          <li>Contains stock, dividend and cash transactions.</li>
                          <li>Fx Fees will not be captured for stock transactions in this file type.</li>
                        </ul>
                      </td>
                      <td><ol>
                          <li>Click Inbox, then click <b>Account Statement</b></li>
                          <li>Select Date Range, then click Download and choose .csv</li>
                        </ol></td>
                    </tr>
                  </tbody>
                </table>
              </div>

              <div className="upload-warning-box" role="note" aria-label="Degiro upload limitations">
                <strong>Degiro upload limitations</strong>
                <ul>
                  <li>Only English language Degiro files are currently supported.</li>
                  <li>Only EUR Degiro cash accounts with AutoFx enabled is currently supported.</li>
                  <li>Only Degiro .csv export files are supported.</li>
                </ul>
              </div>
            </section>

            <PortfolioSelectField
              portfolios={portfolios}
              value={degiroPortfolio}
              onChange={(nextValue) => {
                setDegiroPortfolio(nextValue);
                if (degiroErrors.degiroPortfolio) {
                  setDegiroErrors((current) => {
                    const next = Object.assign({}, current);
                    next.degiroPortfolio = undefined;
                    return next;
                  });
                }
              }}
              errorMessage={degiroErrors.degiroPortfolio}
            />

            <label className={`field ${degiroErrors.degiroFileName ? 'invalid' : ''}`}>
              <span>Degiro File</span>
              <input
                type="file"
                accept=".csv"
                disabled={isScreenBusy}
                onChange={onDegiroFileChange}
              />
              {degiroErrors.degiroFileName ? <span className="error-message">{degiroErrors.degiroFileName}</span> : null}
            </label>

            {degiroFileKind === 'transaction' ? (
              <section className="upload-degiro-preview">
                <div className="upload-table-wrap">
                  <table className="transactions-table">
                    <thead>
                      <tr>
                        <th>Date</th>
                        <th>Event</th>
                        <th>Stock</th>
                        <th className="txn-cell-amount">Quantity</th>
                        <th className="txn-cell-amount">Price</th>
                        <th className="txn-cell-amount">FxFee</th>
                        <th className="txn-cell-amount">FX (Pre Fee)</th>
                        <th className="txn-cell-amount">FX</th>
                        <th className="txn-cell-amount">Commission</th>
                        <th className="txn-cell-amount">Amount</th>
                      </tr>
                    </thead>
                    <tbody>
                      {degiroStockRows.length === 0 ? (
                        <tr>
                          <td colSpan={10} className="muted">No stock transaction rows parsed.</td>
                        </tr>
                      ) : degiroStockRows.map((row, index) => (
                        <tr
                          key={`${row.date}-${row.name}-${index}`}
                          className={[
                            row.wasCombinedFromOrderRows ? 'upload-combined-transaction-row' : '',
                            degiroUnmappedIsinSet.has(normalizeIsinValue(row.isin)) ? 'upload-unmapped-transaction-row' : ''
                          ].filter(Boolean).join(' ')}
                        >
                          <td>{formatDateDisplay(row.date)}</td>
                          <td>{row.event}</td>
                          <td>{row.name}</td>
                          <td className="txn-cell-amount">{formatNumberDisplay(row.quantity, 0, 6)}</td>
                          <td className="txn-cell-amount">
                            ({row.currency}) {Number.isFinite(row.price)
                              ? formatNumberDisplay(row.price, 2, 2)
                              : row.priceRaw}
                          </td>
                          <td className="txn-cell-amount">{formatNumberDisplay(row.fxFee, 2, 2)}</td>
                          <td className="txn-cell-amount">
                            {Number.isFinite(row.fxRatePreFee)
                              ? formatNumberDisplay(row.fxRatePreFee, 4, 4)
                              : (row.exchangeRatePreFxFeeRaw || '-')}
                          </td>
                          <td className="txn-cell-amount">{formatNumberDisplay(row.fxRate, 4, 4)}</td>
                          <td className="txn-cell-amount">{formatNumberDisplay(row.commission, 0, 0)}</td>
                          <td
                            className={`txn-cell-amount ${row.amount < 0 ? 'txn-cell-amount-negative' : (row.amount > 0 ? 'txn-cell-amount-positive' : '')}`.trim()}
                          >
                            {formatNumberDisplay(row.amount, 2, 2)}
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>

                {degiroCombinedOrderIds.length > 0 ? (
                  <p className="upload-combined-legend" role="note" aria-label="Combined transaction rows legend">
                    <span className="upload-combined-legend-swatch" aria-hidden="true" />
                    <span>Transactions combined from multiple rows in the file with the same Order Id.</span>
                  </p>
                ) : null}
              </section>
            ) : null}

            {degiroFileKind === 'account-statement' ? (
              <section className="upload-degiro-preview">
                <div className="upload-table-wrap">
                  <table className="transactions-table">
                    <thead>
                      <tr>
                        <th>Date</th>
                        <th>Type</th>
                        <th>Event</th>
                        <th>Description</th>
                        <th className="txn-cell-amount">Amount</th>
                        <th className="txn-expand-column"><span className="sr-only">Expand</span></th>
                      </tr>
                    </thead>
                    <tbody>
                      {degiroAccountAllRows.length === 0 ? (
                        <tr>
                          <td colSpan={6} className="muted">No account statement rows could be parsed.</td>
                        </tr>
                      ) : degiroAccountAllRows.map((row, index) => {
                        const rowKey = `${row.type}-${row.date}-${row.description}-${row.isin || 'none'}-${index}`;
                        const isExpandable = isAccountPreviewRowExpandable(row);
                        const isExpanded = Boolean(degiroAccountExpandedRows[rowKey]);
                        const detailEntries = getAccountExpandedDetails(row);

                        return (
                          <React.Fragment key={rowKey}>
                            <tr
                              className={[
                                isExpandable ? 'txn-summary-row txn-summary-row-expandable' : 'txn-summary-row',
                                isIsinMappedForRow(row, degiroUnmappedIsinSet) ? '' : 'upload-unmapped-transaction-row'
                              ].filter(Boolean).join(' ')}
                              onClick={isExpandable ? () => {
                                setDegiroAccountExpandedRows((previous) => ({
                                  ...previous,
                                  [rowKey]: !previous[rowKey]
                                }));
                              } : undefined}
                              onKeyDown={isExpandable ? (event) => {
                                if (event.key === 'Enter' || event.key === ' ') {
                                  event.preventDefault();
                                  setDegiroAccountExpandedRows((previous) => ({
                                    ...previous,
                                    [rowKey]: !previous[rowKey]
                                  }));
                                }
                              } : undefined}
                              tabIndex={isExpandable ? 0 : undefined}
                              aria-expanded={isExpandable ? isExpanded : undefined}
                            >
                              <td>{formatDateDisplay(row.date)}</td>
                              <td>{row.type}</td>
                              <td>{row.event || '-'}</td>
                              <td>{row.description || '-'}</td>
                              <td
                                className={`txn-cell-amount ${row.amount < 0 ? 'txn-cell-amount-negative' : (row.amount > 0 ? 'txn-cell-amount-positive' : '')}`.trim()}
                              >
                                {formatNumberDisplay(row.amount, 2, 2)}
                              </td>
                              <td className="txn-expand-column">
                                {isExpandable ? (
                                  <span className="txn-expand-toggle" aria-hidden="true">
                                    <span className={isExpanded ? 'txn-expand-arrow txn-expand-arrow-open' : 'txn-expand-arrow'} />
                                  </span>
                                ) : (
                                  <span className="txn-expand-placeholder" aria-hidden="true" />
                                )}
                              </td>
                            </tr>
                            {isExpandable && isExpanded ? (
                              <tr className="txn-expanded-row">
                                <td colSpan={6}>
                                  <div className="txn-expanded-panel">
                                    <div className="txn-details-grid">
                                      {detailEntries.map((entry) => (
                                        <div key={`${rowKey}-${entry.label}`} className="txn-details-row">
                                          <span className="muted">{entry.label}</span>
                                          <strong>{entry.value}</strong>
                                        </div>
                                      ))}
                                    </div>
                                  </div>
                                </td>
                              </tr>
                            ) : null}
                          </React.Fragment>
                        );
                      })}
                    </tbody>
                  </table>
                </div>
              </section>
            ) : null}

            {degiroRawParseFailures.length > 0 ? (
              <div className="upload-parse-failures">
                <p className="upload-note"><strong>Rows that could not be parsed ({degiroRawParseFailures.length})</strong></p>
                <pre className="upload-parse-failures-raw">{degiroRawParseFailures.join('\n')}</pre>
              </div>
            ) : null}

            {degiroStatus ? (
              <div className={`status-message ${degiroStatus.type}`} role="status">
                <span>{degiroStatus.message}</span>
              </div>
            ) : null}

            {degiroIsSecurityLookupLoading ? (
              <p className="muted upload-note">Checking securities for ISINs in this file...</p>
            ) : null}

            {!degiroIsSecurityLookupLoading && degiroUnmappedIsins.length > 0 ? (
              <div className="status-message error" role="status">
                <span>Unmapped ISINs (these rows will not be uploaded): {degiroUnmappedIsins.join(', ')}</span>
              </div>
            ) : null}

            {canUploadDegiroTransactionFile ? (
              <div className="hero-actions upload-hero-actions">
                <button type="button" className="primary-btn" onClick={uploadDegiroTransactions}>Upload Degiro Transactions</button>
              </div>
            ) : null}

            {canUploadDegiroAccountStatementFile ? (
              <div className="hero-actions upload-hero-actions">
                <button type="button" className="primary-btn" onClick={uploadDegiroTransactions}>Upload Degiro Account Statement</button>
              </div>
            ) : null}

            {degiroUploadDuplicates.length > 0 ? (
              <section className="upload-duplicates-preview">
                <h4>Duplicate Transactions Skipped ({degiroUploadDuplicates.length})</h4>
                <div className="upload-table-wrap">
                  <table className="transactions-table">
                    <thead>
                      <tr>
                        <th>Order Id</th>
                        <th>Date</th>
                        <th>Event</th>
                        <th>Stock</th>
                        <th className="txn-cell-amount">Amount</th>
                      </tr>
                    </thead>
                    <tbody>
                      {degiroUploadDuplicates.map((duplicate, index) => (
                        <tr key={`${duplicate.brokerOrderId || 'duplicate'}-${index}`}>
                          <td>{duplicate.brokerOrderId || '-'}</td>
                          <td>{formatDateDisplay(duplicate.date)}</td>
                          <td>{duplicate.event || (duplicate.eventId != null ? String(duplicate.eventId) : '-')}</td>
                          <td>{duplicate.name || '-'}</td>
                          <td
                            className={`txn-cell-amount ${duplicate.amount < 0 ? 'txn-cell-amount-negative' : (duplicate.amount > 0 ? 'txn-cell-amount-positive' : '')}`.trim()}
                          >
                            {Number.isFinite(duplicate.amount)
                              ? formatNumberDisplay(duplicate.amount, 2, 2)
                              : '-'}
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              </section>
            ) : null}
          </article>

          <article className="panel-card upload-panel">
            <header className="upload-panel-header">
              <div>
                <h3>Custom Upload</h3>
                <p className="muted">Paste your data or select a file.</p>
                <ul>
                  <li>A header row for the Transaction type is required. Columns are matched by their header name, so any order is supported.</li>
                  <li>An optional Portfolio column is supported. If included, the Portfolio selection is not required.</li>
                  <li>ISO date format (YYYY-MM-DD) is recommended for the Date column. Other formats such as DD-MM-YYYY and DD/MM/YYYY are also accepted.</li>
                </ul>
              </div>
            </header>

            <div className="upload-warning-box" role="note" aria-label="Custom upload limitations">
              <strong>Custom Upload Limitations</strong>
              <ul>
                <li>Only customer .csv files are currently supported and a header row must be included.</li>
              </ul>
            </div>

            <div className="upload-custom-selectors">
              <PortfolioSelectField
                portfolios={portfolios}
                value={customPortfolio}
                label={hasPortfolioColumn ? 'Portfolio (optional - Portfolio column found in data)' : 'Portfolio'}
                onChange={(nextValue) => {
                  setCustomPortfolio(nextValue);
                  if (customErrors.customPortfolio) {
                    setCustomErrors((current) => {
                      const next = Object.assign({}, current);
                      next.customPortfolio = undefined;
                      return next;
                    });
                  }
                }}
                errorMessage={customErrors.customPortfolio}
              />

              <label className={`field ${customErrors.customType ? 'invalid' : ''}`}>
                <span>Transaction Type</span>
                <select
                  value={customType}
                  onChange={(event) => {
                    const nextType = event.target.value;
                    setCustomType(nextType);
                    setCustomColumns(nextType && UPLOAD_COLUMN_SETS[nextType] ? getDefaultColumnsForType(nextType) : []);
                    setCustomErrors((current) => {
                      const next = Object.assign({}, current);
                      next.customType = undefined;
                      return next;
                    });
                  }}
                >
                  <option value="">Select Transaction Type</option>
                  <option value={TRANSACTION_TYPES.stock}>Stock</option>
                  <option value={TRANSACTION_TYPES.dividend}>Dividend</option>
                  <option value={TRANSACTION_TYPES.cash}>Cash</option>
                </select>
                {customErrors.customType ? <span className="error-message">{customErrors.customType}</span> : null}
              </label>
            </div>

            <div className="upload-sample-and-help">
              <div className="upload-sample-row" role="note" aria-label="Custom upload sample header row">
                {!customType ? (
                  <p className="muted">Please select a Transaction Type to see a sample .csv header row...</p>
                ) : (
                  <React.Fragment>
                    <p className="upload-box-title">Sample .csv</p>
                    <pre className="upload-code-box">{customSampleRowText}</pre>
                  </React.Fragment>
                )}
              </div>

              {customType ? (
              <div className="upload-column-help" role="region" aria-label="Custom upload column help">
                <p className="upload-help-row">
                  <span>Need help understanding the Columns?</span>
                  <button
                    type="button"
                    className="upload-help-icon-btn"
                    aria-label="Show custom upload column help"
                    aria-expanded={isCustomColumnHelpOpen ? 'true' : 'false'}
                    onClick={() => {
                      setIsCustomColumnHelpOpen((current) => !current);
                    }}
                  >
                    ?
                  </button>
                </p>
                {isCustomColumnHelpOpen ? (
                  <div className="upload-table-wrap">
                    <table className="transactions-table upload-column-help-table">
                      <thead>
                      <tr>
                        <th>Column</th>
                        <th>Description</th>
                        <th>Valid Sample Values</th>
                        <th>Optional</th>
                      </tr>
                    </thead>
                    <tbody>
                      {isCashCustomType ? (
                        <React.Fragment>
                          <tr>
                            <td>Date</td>
                            <td>Transaction date.<br/>ISO format (YYYY-MM-DD) is recommended.</td>
                            <td>2026-01-15 (recommended),<br/>15-01-2026, 15/01/2026</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Event</td>
                            <td>Cash event type.</td>
                            <td>Deposit, Withdrawal, Interest,<br/>ExchangeFee, TransferFee, ADRFee</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Amount</td>
                            <td>Total cash amount for the cash transaction in the Portfolio base currency.</td>
                            <td>3000, -1000, -12.50</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Portfolio</td>
                            <td>Portfolio name for the row. Required if no Portfolio is selected.</td>
                            <td>My Portfolio</td>
                            <td>✓</td>
                          </tr>
                        </React.Fragment>
                      ) : isDividendCustomType ? (
                        <React.Fragment>
                          <tr>
                            <td>Date</td>
                            <td>Dividend payment date.<br/>ISO format <b>(YYYY-MM-DD)</b> is recommended.</td>
                            <td>2026-01-15 (recommended),<br/>15-01-2026, 15/01/2026</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Event</td>
                            <td>Defaults to Dividend if not provided.</td>
                            <td>Dividend, CapitalReturn</td>
                            <td>✓</td>
                          </tr>
                          <tr>
                            <td>ISIN</td>
                            <td>12-character security identifier.<br/>Either ISIN or Description must be provided.</td>
                            <td>US0378331005, NL0010273215</td>
                            <td>✓</td>
                          </tr>
                          <tr>
                            <td>Country</td>
                            <td>2-character country code for source country of the dividend.</td>
                            <td>US, IE, NL</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Dividend</td>
                            <td>Declared dividend amount in the source currency.</td>
                            <td>25.00, 100.50</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>TaxPaid</td>
                            <td>Tax paid amount in the source currency.</td>
                            <td>0, 2.50, -5.00</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Received</td>
                            <td>Net amount received in the source currency.</td>
                            <td>22.50, 95.00</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Currency</td>
                            <td>ISO currency code for the source amounts.</td>
                            <td>EUR, USD, GBP</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>FxRate</td>
                            <td>FX conversion rate to portfolio base currency.<br/>Blank defaults to 1.</td>
                            <td>blank, 1, 1.0843</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Description</td>
                            <td>Free text description.<br/>Either ISIN or Description must be provided.</td>
                            <td>Apple Inc</td>
                            <td>✓</td>
                          </tr>
                          <tr>
                            <td>Portfolio</td>
                            <td>Portfolio name for the row. Required if no Portfolio is selected.</td>
                            <td>My Portfolio</td>
                            <td>✓</td>
                          </tr>
                        </React.Fragment>
                      ) : (
                        <React.Fragment>
                          <tr>
                            <td>Date</td>
                            <td>Transaction date.<br/>ISO format <b>(YYYY-MM-DD)</b> is recommended.</td>
                            <td>2026-01-15 (recommended),<br/>15-01-2026, 15/01/2026</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Event</td>
                            <td>Stock event type.</td>
                            <td>Buy, Sell, 
                              <br/>Split, WriteOff
                              <br/>TransferIn, TransferOut</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>ISIN</td>
                            <td>12-character security identifier.</td>
                            <td>US0378331005, NL0010273215</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Quantity</td>
                            <td>Number of shares transacted.
                              <br/>This should be a positive value for a Buy or a TransferIn
                              <br/>This should be a negative value for a sell or a TransferOut.
                              <br/>For a Split, this should be a positive value representing the total new quantity.
                            </td>
                            <td>10, 2, -10</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Price</td>
                            <td>Price per share in the trade currency.</td>
                            <td>185.42, 24.90</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Currency</td>
                            <td>ISO currency code for the trade.</td>
                            <td>EUR, USD, GBP</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Commission</td>
                            <td>Fee amount for the trade in the portfolio base currency.<br/>Fee values should be negative if provided.</td>
                            <td>blank, 0, -1.50</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>TaxPaid</td>
                            <td>Tax amount paid in the portfolio base currency.<br/>This should be negative if provided.</td>
                            <td>blank, 0, -0.25</td>
                            <td>✓</td>
                          </tr>
                          <tr>
                            <td>Amount</td>
                            <td>Total cash impact in the portfolio currency.<br/><pre>Calc = ((Price / FxRate) * Quantity) + Commission + TaxPaid</pre>
                              <br/>A Negative value should be provided for a Buy.
                              <br/>A Positive value should be provided for a Sell.
                              <br/>Provide 0 or blank for Split, WriteOff, TransferIn & TransferOut.
                            </td>
                            <td>blank, 0, -1854.20, 1245.00</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>FxRate</td>
                            <td>FX conversion rate to portfolio base currency.<br/>Blank defaults to 1.</td>
                            <td>blank, 1, 1.0843</td>
                            <td></td>
                          </tr>
                          <tr>
                            <td>Portfolio</td>
                            <td>Portfolio name for the row. Required if no portfolio is selected.</td>
                            <td>My Portfolio</td>
                            <td>✓</td>
                          </tr>
                        </React.Fragment>
                      )}
                    </tbody>
                  </table>
                </div>
                ) : null}
              </div>
            ) : null}
            </div>

            <div className="upload-input-mode" role="radiogroup" aria-label="Upload input method">
              <button
                type="button"
                className={`chip ${customInputMode === 'paste' ? 'active' : ''}`}
                aria-pressed={customInputMode === 'paste'}
                onClick={() => handleCustomInputModeChange('paste')}
              >
                Paste Data
              </button>
              <button
                type="button"
                className={`chip ${customInputMode === 'file' ? 'active' : ''}`}
                aria-pressed={customInputMode === 'file'}
                onClick={() => handleCustomInputModeChange('file')}
              >
                Upload File
              </button>
            </div>

            {customInputMode === 'paste' ? (
              <label className={`field ${customErrors.clipboardInput ? 'invalid' : ''}`}>
                <span>Paste Data (Clipboard)</span>
                <textarea
                  className="upload-textarea"
                  value={clipboardInput}
                  onChange={(event) => {
                    setClipboardInput(event.target.value);
                    if (customErrors.clipboardInput) {
                      setCustomErrors((current) => {
                        const next = Object.assign({}, current);
                        next.clipboardInput = undefined;
                        return next;
                      });
                    }
                  }}
                  placeholder={customPastePlaceholder}
                />
                {customErrors.clipboardInput ? <span className="error-message">{customErrors.clipboardInput}</span> : null}
              </label>
            ) : (
              <label className={`field ${customErrors.customFileName ? 'invalid' : ''}`}>
                <span>Upload a File</span>
                <input
                  type="file"
                  accept=".csv,.txt"
                  onChange={(event) => {
                    const file = event.target.files && event.target.files[0];
                    setCustomFileName(file ? file.name : '');
                    if (!file) {
                      setCustomFileText('');
                    }
                    if (customErrors.customFileName) {
                      setCustomErrors((current) => {
                        const next = Object.assign({}, current);
                        next.customFileName = undefined;
                        return next;
                      });
                    }

                    if (file) {
                      file.text().then((text) => {
                        setCustomFileText(text);
                      }).catch(() => {
                        setCustomFileText('');
                        setCustomStatus({ type: 'error', message: 'Unable to read the selected file.' });
                      });
                    }
                  }}
                />
                {customErrors.customFileName ? <span className="error-message">{customErrors.customFileName}</span> : null}
              </label>
            )}

            {customInputMode === 'file' && customFileName ? (
              <p className="muted upload-note">Selected file: {customFileName}</p>
            ) : null}

            <section className="upload-preview">
              <h4>Preview</h4>
              {customParsedRows.length === 0 ? (
                <p className="muted upload-note">Paste or upload data to generate a preview.</p>
              ) : (
                <div className="upload-table-wrap">
                  <table className="transactions-table">
                    <thead>
                      <tr>
                        {customPreviewColumns.map((column) => (
                          <th key={column} className={customType === TRANSACTION_TYPES.stock && (column === 'Amount' || isCustomStockNumericPreviewColumn(column)) ? 'txn-cell-amount' : ''}>{column}</th>
                        ))}
                      </tr>
                    </thead>
                    <tbody>
                      {customParsedRows.map((row, index) => {
                        const isUnmapped = Boolean(row.securityIdentifier)
                          && customUnmappedIdentifierSet.has(normalizeCustomIdentityValue(row.securityIdentifierType, row.securityIdentifier));

                        return (
                          <tr key={`${row.type}-${row.date}-${index}`} className={isUnmapped ? 'upload-unmapped-transaction-row' : ''}>
                            {customPreviewColumns.map((column) => (
                              <td key={`${row.type}-${index}-${column}`} className={getCustomPreviewCellClass(row, column)}>
                                {getCustomPreviewCellValue(row, column)}
                              </td>
                            ))}
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                </div>
              )}
            </section>

            {customRawParseFailures.length > 0 ? (
              <div className="upload-parse-failures">
                <p className="upload-note"><strong>Rows that could not be parsed ({customRawParseFailures.length})</strong></p>
                <pre className="upload-parse-failures-raw">{customRawParseFailures.join('\n')}</pre>
              </div>
            ) : null}

            {customStatus ? (
              <div className={`status-message ${customStatus.type}`} role="status">
                <span>{customStatus.message}</span>
                {customStatus.hint ? (
                  <p className="upload-status-hint"><b>Hint:</b> {customStatus.hint}</p>
                ) : null}
              </div>
            ) : null}

            {customIsSecurityLookupLoading ? (
              <p className="muted upload-note">Checking securities for identifiers in this file...</p>
            ) : null}

            {!customIsSecurityLookupLoading && customUnmappedIdentifiers.length > 0 ? (
              <div className="status-message error" role="status">
                <span>Unmapped identifiers (these rows will not be uploaded): {customUnmappedIdentifiers.join(', ')}</span>
              </div>
            ) : null}

            {canUploadCustomTransactions ? (
              <div className="hero-actions upload-hero-actions">
                <button type="button" className="primary-btn" onClick={uploadCustomTransactions}>
                  Upload Custom {customType} Transactions
                </button>
              </div>
            ) : null}

            {customUploadDuplicates.length > 0 ? (
              <section className="upload-duplicates-preview">
                <h4>Duplicate Transactions Skipped ({customUploadDuplicates.length})</h4>
                <div className="upload-table-wrap">
                  <table className="transactions-table">
                    <thead>
                      <tr>
                        <th>Order Id</th>
                        <th>Date</th>
                        <th>Event</th>
                        <th>Stock</th>
                        <th className="txn-cell-amount">Amount</th>
                      </tr>
                    </thead>
                    <tbody>
                      {customUploadDuplicates.map((duplicate, index) => (
                        <tr key={`${duplicate.brokerOrderId || 'duplicate'}-${index}`}>
                          <td>{duplicate.brokerOrderId || '-'}</td>
                          <td>{formatDateDisplay(duplicate.date)}</td>
                          <td>{duplicate.event || (duplicate.eventId != null ? String(duplicate.eventId) : '-')}</td>
                          <td>{duplicate.name || '-'}</td>
                          <td className={`txn-cell-amount ${duplicate.amount < 0 ? 'txn-cell-amount-negative' : (duplicate.amount > 0 ? 'txn-cell-amount-positive' : '')}`.trim()}>
                            {Number.isFinite(duplicate.amount) ? formatNumberDisplay(duplicate.amount, 2, 2) : '-'}
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              </section>
            ) : null}

          </article>
        </section>
      </div>
    );
  }

  global.PortfolioTransactionsUpload = {
    TransactionsUploadPage
  };
})(window);
