/**
 * Amount parsing for imported files (#325).
 *
 * The function used to end on `parseFloat`, which returns the longest valid
 * PREFIX of a string instead of rejecting it. `"100,00 CAD"` therefore came out
 * as 10 000 and `"1 234,56 CR"` as 123 456 — a factor-100 error that passes
 * `isNaN`, so the row counted as VALID everywhere downstream. Validation is
 * ANCHORED now: after normalisation the whole remaining string must match a
 * numeric grammar, and any residual character yields `NaN`.
 *
 * `NaN` is the only safe answer for a cell we cannot read with certainty. A
 * trailing `CR` / `DB` carries a DIRECTION, not noise, so guessing a magnitude
 * for it would trade a loud failure for a silent sign error — the exact bug
 * class this chantier exists to remove. The absolute-amount + D/C-indicator
 * shape is refused, by design, upstream (#328).
 *
 * Two accounting forms ARE legitimate and supported: parentheses `(50,00)` and
 * a trailing sign `50,00-`, both meaning a negative amount.
 *
 * Separator arbitration: `1.234` means 1234 in a French column and 1.234 in an
 * English one, and nothing INSIDE the cell can tell. Callers that know the
 * column pass `decimalSeparator` (see `detectDecimalSeparator`); callers that
 * do not get the documented per-cell default.
 */

/** Which character separates the decimals in a given column. */
export type DecimalSeparator = "," | ".";

export interface AmountParseOptions {
  /**
   * Decimal separator arbitrated at COLUMN level. When set, the other character
   * is read as a grouping separator, whatever a single cell looks like.
   */
  decimalSeparator?: DecimalSeparator;
}

/** Currency symbols and every flavour of space are noise, never data. */
const NOISE = /[€$£\s\u00A0]/g;

/** Digits, optionally grouped in threes by `sep`. Anchored. */
function groupedRe(sep: "," | "."): RegExp {
  const s = sep === "." ? "\\." : ",";
  return new RegExp(`^\\d{1,3}(?:${s}\\d{3})+$`);
}

/** Digits, one `sep`, then decimals. Anchored. `limit` caps the decimal count. */
function decimalRe(sep: "," | ".", limit?: number): RegExp {
  const s = sep === "." ? "\\." : ",";
  const tail = limit === undefined ? "+" : `{1,${limit}}`;
  return new RegExp(`^\\d+${s}\\d${tail}$`);
}

/** Grouped digits then decimals, e.g. `1.234,56`. Anchored. */
function groupedDecimalRe(group: "," | ".", dec: "," | "."): RegExp {
  const g = group === "." ? "\\." : ",";
  const d = dec === "." ? "\\." : ",";
  return new RegExp(`^\\d{1,3}(?:${g}\\d{3})+${d}\\d+$`);
}

/**
 * Read a fully normalised body (digits plus `.` and `,` only, no sign, no
 * spaces) under a known decimal separator. Returns `NaN` when the body does not
 * match the grammar exactly.
 */
function readWithSeparator(body: string, decimal: DecimalSeparator): number {
  const group: DecimalSeparator = decimal === "," ? "." : ",";
  const hasDecimal = body.includes(decimal);
  const hasGroup = body.includes(group);

  let normalized: string | null = null;
  if (hasDecimal && hasGroup) {
    if (groupedDecimalRe(group, decimal).test(body)) normalized = body;
  } else if (hasDecimal) {
    if (decimalRe(decimal).test(body)) normalized = body;
  } else if (hasGroup) {
    if (groupedRe(group).test(body)) normalized = body;
  } else if (/^\d+$/.test(body)) {
    normalized = body;
  }
  if (normalized === null) return NaN;

  const stripped = normalized.split(group).join("");
  return Number(decimal === "," ? stripped.replace(",", ".") : stripped);
}

/**
 * Read a body with no column context. The rules reproduce the documented
 * per-cell behaviour, now anchored:
 *   - both separators present -> the LAST one is the decimal;
 *   - a single `,` followed by one or two digits -> French decimal;
 *   - a single `.` -> English decimal (`1.234` reads as 1.234 alone);
 *   - otherwise the separator must group digits in perfect threes.
 */
function readPerCell(body: string): number {
  const lastComma = body.lastIndexOf(",");
  const lastDot = body.lastIndexOf(".");

  if (lastComma >= 0 && lastDot >= 0) {
    return readWithSeparator(body, lastComma > lastDot ? "," : ".");
  }
  if (lastComma >= 0) {
    if (decimalRe(",", 2).test(body)) return readWithSeparator(body, ",");
    return groupedRe(",").test(body) ? readWithSeparator(body, ".") : NaN;
  }
  if (lastDot >= 0) {
    if (decimalRe(".").test(body)) return readWithSeparator(body, ".");
    return groupedRe(".").test(body) ? readWithSeparator(body, ",") : NaN;
  }
  return /^\d+$/.test(body) ? Number(body) : NaN;
}

/**
 * Parse an amount cell to a number, or `NaN` when it cannot be read with
 * certainty. Handles `1.234,56`, `1 234,56`, `1,234.56`, `-84,32`, `(50,00)`
 * and `50,00-`.
 */
export function parseFrenchAmount(
  raw: string,
  options: AmountParseOptions = {}
): number {
  if (!raw || typeof raw !== "string") return NaN;

  let cleaned = raw.trim();
  let negative = false;

  // Accounting parentheses. Only a balanced, outermost pair counts; `(50,00`
  // falls through and fails the anchored grammar below.
  const parens = /^\((.*)\)$/.exec(cleaned);
  if (parens) {
    negative = true;
    cleaned = parens[1].trim();
  }

  cleaned = cleaned.replace(NOISE, "");
  if (!cleaned) return NaN;

  // One sign, leading or trailing, never both, never inside parentheses.
  const leading = /^[+-]/.test(cleaned);
  const trailing = /[+-]$/.test(cleaned);
  if (leading && trailing) return NaN;
  if (leading || trailing) {
    if (negative) return NaN; // `(-50,00)` is not a form any bank emits
    negative = cleaned[leading ? 0 : cleaned.length - 1] === "-";
    cleaned = leading ? cleaned.slice(1) : cleaned.slice(0, -1);
  }

  // Anchored gate: nothing but digits and separators may remain. This is what
  // rejects `2024Montant`, `1e3`, `Infinity`, `5%` and `100,00CAD`.
  if (!/^[\d.,]+$/.test(cleaned) || !/\d/.test(cleaned)) return NaN;

  const value = options.decimalSeparator
    ? readWithSeparator(cleaned, options.decimalSeparator)
    : readPerCell(cleaned);

  if (!Number.isFinite(value)) return NaN;
  return negative ? -value : value;
}

/**
 * Arbitrate the decimal separator of a COLUMN from all of its cells.
 *
 * A cell is decisive when it carries both separators (the last one is the
 * decimal) or exactly one separator followed by one or two digits — a grouping
 * separator is always followed by three. Ambiguous cells such as `1.234` vote
 * for nothing. Returns `undefined` when the column gives no verdict, in which
 * case callers keep the per-cell default.
 */
export function detectDecimalSeparator(
  cells: Iterable<string>
): DecimalSeparator | undefined {
  let comma = 0;
  let dot = 0;

  for (const cell of cells) {
    if (!cell || typeof cell !== "string") continue;
    const body = cell.trim().replace(NOISE, "").replace(/^[+-]|[+-]$/g, "");
    if (!body) continue;

    const lastComma = body.lastIndexOf(",");
    const lastDot = body.lastIndexOf(".");
    if (lastComma >= 0 && lastDot >= 0) {
      if (lastComma > lastDot) comma++;
      else dot++;
      continue;
    }
    if (lastComma >= 0 && decimalRe(",", 2).test(body)) comma++;
    else if (lastDot >= 0 && decimalRe(".", 2).test(body)) dot++;
  }

  if (comma === dot) return undefined;
  return comma > dot ? "," : ".";
}
