import type { Currency, Money } from './shared';

// Presentation helpers. The money one is the load-bearing piece: InvoiceShelf stores minor units as
// integers and describes how to render them in the company's `current_company_currency` record, so the
// separators, the precision and the symbol side all come from data rather than from the browser locale.
// Intl.NumberFormat is deliberately NOT used — it would apply the *viewer's* locale to the *company's*
// currency and quietly render "1,234.56 €" for an install configured the other way round.

/**
 * Coerce an API money field to a number of minor units.
 *
 * Upstream is genuinely inconsistent about the JSON type here — `invoice.total` arrives as the number
 * 450000 while `customer.due_amount` arrives as the string "1830000" — so every read goes through this
 * rather than trusting either. Non-finite input collapses to 0 so a missing total renders as "0,00 €"
 * instead of "NaN".
 */
export const toMinor = (value: Money): number => {
  if (value == null) return 0;
  const n = typeof value === 'number' ? value : Number(value);
  return Number.isFinite(n) ? n : 0;
};

const groupDigits = (digits: string, separator: string): string => digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator);

/**
 * The minor-unit divisor.
 *
 * Deliberately the constant 100 and NOT 10**precision, because that is what upstream does — its
 * `utils/format-money.ts` opens with `amount = amountInCents / 100` and then uses `precision` only to
 * decide how many decimal digits to *print*. On a 3-decimal currency the two rules disagree, and matching
 * InvoiceShelf's own arithmetic matters more here than being independently right: a figure that differs
 * from the one its web UI shows for the same invoice would read as a bug in this screen.
 */
const MINOR_PER_MAJOR = 100;

/**
 * Render minor units the way InvoiceShelf renders them.
 *
 * Ported from `resources/scripts/utils/format-money.ts` and matched on the details that are visible:
 * there is always a single space between symbol and number, the minus sign sits inside the number rather
 * than before the symbol (so "€ -1.234,56", not "-€ 1.234,56"), and `precision: 0` prints no decimal part
 * at all rather than a bare separator.
 */
export const formatMoney = (value: Money, currency?: Currency | null): string => {
  let precision = Math.abs(Number(currency?.precision ?? 2));
  if (!Number.isFinite(precision)) precision = 2;

  const amount = toMinor(value) / MINOR_PER_MAJOR;
  const negativeSign = amount < 0 ? '-' : '';
  const abs = Math.abs(amount);

  const thousands = currency?.thousand_separator ?? ',';
  const decimal = currency?.decimal_separator ?? '.';
  const symbol = currency?.symbol ?? '';

  const fixed = abs.toFixed(precision);
  const [whole = '0', frac = ''] = fixed.split('.');

  const combined = negativeSign + groupDigits(whole, thousands) + (precision ? decimal + frac : '');

  if (!symbol) return combined;
  // `swap_currency_symbol` arrives as a boolean from the API and as 0/1 from a settings write.
  return currency?.swap_currency_symbol ? `${combined} ${symbol}` : `${symbol} ${combined}`;
};

/** Bare number, no symbol — for chart axes and form inputs where the symbol lives elsewhere. */
export const formatAmount = (value: Money, currency?: Currency | null): string =>
  formatMoney(value, currency ? { ...currency, symbol: '' } : null);

/** Minor units → the major-unit number an editable input holds (450000 → 4500). */
export const toMajor = (value: Money): number => toMinor(value) / MINOR_PER_MAJOR;

/** The inverse, for writes. Rounds because floats reaching here have been through an <input>. */
export const fromMajor = (value: number | string): number => {
  const n = typeof value === 'number' ? value : Number(String(value).replace(',', '.'));
  return Number.isFinite(n) ? Math.round(n * MINOR_PER_MAJOR) : 0;
};

/** Strips symbol and separators back to minor units — the paste-into-a-money-field path. */
export const parseMoneyMinor = (text: string, currency?: Currency | null): number => {
  const thousands = currency?.thousand_separator ?? ',';
  const decimal = currency?.decimal_separator ?? '.';
  const cleaned = text
    .replace(currency?.symbol ?? '', '')
    .split(thousands)
    .join('')
    .replace(decimal, '.')
    .replace(/[^\d.-]/g, '');
  const n = Number(cleaned);
  return Number.isFinite(n) ? Math.round(n * MINOR_PER_MAJOR) : 0;
};

// ── dates ────────────────────────────────────────────────────────────────────────────────────────

/**
 * Parse the several date shapes the API mixes: "2026-06-21", "2026-06-21 00:00:00" and full ISO.
 * The space form is not valid ISO-8601, so it is normalised before Date sees it.
 */
const parseDate = (value: string | null | undefined): Date | null => {
  if (!value) return null;
  const normalised = value.includes('T') ? value : value.replace(' ', 'T');
  const d = new Date(normalised);
  return Number.isNaN(d.getTime()) ? null : d;
};

/**
 * Prefer upstream's own `formatted_*` field when the caller has one: it already reflects the company's
 * configured date format (dd/mm/yyyy here), which we cannot reconstruct from the raw value alone.
 */
export const formatDate = (raw: string | null | undefined, preformatted?: string | null): string => {
  if (preformatted) return preformatted;
  const d = parseDate(raw);
  if (!d) return '—';
  const pad = (n: number) => String(n).padStart(2, '0');
  return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`;
};

/** ISO yyyy-mm-dd, the form every write endpoint expects and `<input type="date">` speaks. */
export const toInputDate = (raw: string | null | undefined): string => {
  const d = parseDate(raw);
  if (!d) return '';
  const pad = (n: number) => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
};

/** Whole days until `raw`; negative once past. Used for the overdue hint next to a due date. */
export const daysUntil = (raw: string | null | undefined): number | null => {
  const d = parseDate(raw);
  if (!d) return null;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  d.setHours(0, 0, 0, 0);
  return Math.round((d.getTime() - today.getTime()) / 86_400_000);
};

export const relativeDue = (raw: string | null | undefined): string => {
  const days = daysUntil(raw);
  if (days == null) return '';
  if (days === 0) return 'due today';
  if (days > 0) return days === 1 ? 'due tomorrow' : `due in ${days} days`;
  const overdue = Math.abs(days);
  return overdue === 1 ? '1 day overdue' : `${overdue} days overdue`;
};

/** Strip the HTML upstream stores in note fields, for one-line previews. */
export const stripHtml = (html: string | null | undefined): string =>
  (html ?? '')
    .replace(/<[^>]*>/g, ' ')
    .replace(/&nbsp;/g, ' ')
    .replace(/&amp;/g, '&')
    .replace(/\s+/g, ' ')
    .trim();
