/**
 * Unités des règles du yield — logique PURE, testée par rule-units.test.ts.
 *
 * ═══════════════════════════════════════════════════════════════════════════
 * POURQUOI CE FICHIER EXISTE
 * ═══════════════════════════════════════════════════════════════════════════
 * Trois conventions cohabitent dans le même panneau : saisons en POURCENTS
 * (+40), last-minute, OBA et week-end en MULTIPLICATEURS (×0,70, ×1,05), prime long
 * terme stockée en FRACTION (0.10 = +10 %). La prime s'affichait BRUTE à côté
 * d'un « % » : l'écran lisait « +0.1 % », et taper 10 pour +10 % enregistrait
 * ×11.
 *
 * Le stockage ne change pas. La prime se SAISIT et s'AFFICHE en % entier, et
 * la conversion ne vit qu'ici, dans farOutPremiumToField() et
 * farOutPremiumFromField(), que le composant appelle telles quelles — c'est le
 * seul moyen de tester le branchement sans monter React. Chaque autre valeur
 * reste dans son unité, mais affiche son équivalent dans l'autre : « ×0,70 →
 * soit −30 % », « +40 % → soit ×1,40 ».
 */
import type { FarOutPremium } from '../types';
import { normalizeFarOut } from './rule-sections';

/**
 * Copie INDICATIVE de config/yield.php → rule_bounds, pour les attributs HTML
 * min/max des champs. Seul le serveur fait foi : un écart ici ne change qu'un
 * indice de saisie, l'aperçu affiche le refus du serveur en moins d'une
 * seconde. La prime est exprimée en % (0–50), comme à l'écran. Seules les
 * bornes d'un champ SAISISSABLE y figurent : `days_before` n'est éditable
 * nulle part, sa borne (0–30 j) ne vit que côté serveur.
 */
export const RULE_BOUNDS = {
    seasonPct: { min: -50, max: 200 },
    lastMinuteMultiplier: { min: 0.5, max: 1 },
    farOutPremiumPercent: { min: 0, max: 50 },
    farOutThresholdDays: { min: 1, max: 90 },
    // ×1 au plus bas : le remplissage ne fait que monter les prix (lib/oba-rules.ts).
    obaMult: { min: 1, max: 2 },
    weekendMultiplier: { min: 0.5, max: 2 },
} as const;

/** Arrondi décimal qui efface le bruit flottant (0.07 × 100 = 7.000000000000001). */
function round(value: number, decimals: number): number {
    const factor = 10 ** decimals;
    return Math.round(value * factor) / factor;
}

/** Fraction stockée → pourcentage affiché : 0.1 → 10, 0.075 → 7.5. Non fini → 0. */
export function fractionToPercent(fraction: unknown): number {
    const n = typeof fraction === 'number' ? fraction : Number(fraction);
    return Number.isFinite(n) ? round(n * 100, 2) : 0;
}

/** Pourcentage saisi → fraction stockée : 10 → 0.1, 7.5 → 0.075. Non fini → 0. */
export function percentToFraction(percent: number): number {
    return Number.isFinite(percent) ? round(percent / 100, 6) : 0;
}

/**
 * Ce que le champ « prime » affiche : un % entier (10 pour une prime stockée
 * 0.10). Une prime mal formée ([] venu de PHP, undefined) affiche 0, jamais NaN.
 */
export function farOutPremiumToField(farOut: unknown): number {
    return fractionToPercent(normalizeFarOut(farOut).premium_pct);
}

/**
 * La prime après une saisie dans le champ : le % tapé redevient une fraction,
 * le seuil est conservé. Un champ vidé vaut 0 %, c'est-à-dire pas de prime.
 */
export function farOutPremiumFromField(farOut: unknown, raw: string): FarOutPremium {
    const typed = raw.trim() === '' ? 0 : Number(raw.replace(',', '.'));
    return { ...normalizeFarOut(farOut), premium_pct: percentToFraction(typed) };
}

/**
 * Texte d'un champ « multiplicateur » → nombre. Un champ vidé (ou illisible :
 * un champ numérique rend alors « ») vaut NaN — jamais un 0 ni un 1
 * inventés. Posté, NaN devient `null` et le serveur répond « doit être un
 * nombre » : l'écran dit la vérité sur un champ vide.
 */
export function multiplierFromField(raw: string): number {
    const text = raw.trim().replace(',', '.');
    return text === '' ? Number.NaN : Number(text);
}

/**
 * Ce qu'affiche un champ « multiplicateur » : le texte TAPÉ (`typed`, null
 * tant que rien n'a été tapé) tant qu'il dit le même nombre que la valeur du
 * formulaire, sinon la valeur.
 *
 * Pourquoi : un champ lié à une valeur recalculée à chaque frappe se fait
 * réécrire sous le curseur. « 0 » (début de « 0.9 ») ou « » (champ vidé)
 * redevenaient « 1 », et la frappe suivante s'y ajoutait : ×1,9 au lieu de
 * ×0,9. Garder le texte tapé laisse écrire « 0 », « 0. », « » en paix.
 *
 * Quand la valeur change d'AILLEURS (rallumer la section, revenir au groupe),
 * le texte tapé ne lui correspond plus : le champ montre alors la valeur.
 */
export function multiplierFieldText(typed: string | null, multiplier: number): string {
    if (typed !== null && Object.is(multiplierFromField(typed), multiplier)) return typed;
    return Number.isFinite(multiplier) ? String(multiplier) : '';
}

/** ×0,70 → −30 ; ×1,05 → +5. */
export function multiplierToPercentDelta(multiplier: number): number {
    return round((multiplier - 1) * 100, 2);
}

/** +45 → ×1,45 ; −15 → ×0,85. */
export function percentDeltaToMultiplier(percent: number): number {
    return round(1 + percent / 100, 4);
}

const isFrench = (locale: string | undefined): boolean => (locale ?? 'fr').toLowerCase().startsWith('fr');

/** Nombre sans zéros inutiles, virgule en français : 7.5 → « 7,5 », 30 → « 30 ». */
function formatNumber(value: number, locale: string | undefined, decimals: number): string {
    const text = String(round(value, decimals));
    return isFrench(locale) ? text.replace('.', ',') : text;
}

/**
 * Pourcentage SIGNÉ, écrit à la main pour ne pas dépendre de la version d'ICU :
 * français « −30 % » (signe moins U+2212, espace insécable U+00A0), anglais
 * « -30% ». Zéro n'a pas de signe.
 */
export function formatSignedPercent(percent: number, locale: string | undefined): string {
    if (!Number.isFinite(percent)) return '—';
    const french = isFrench(locale);
    const sign = percent > 0 ? '+' : percent < 0 ? (french ? '−' : '-') : '';
    return `${sign}${formatNumber(Math.abs(percent), locale, 2)}${french ? ' %' : '%'}`;
}

/**
 * Multiplicateur à deux décimales AU MOINS, quatre au plus : « ×1,45 »,
 * « ×0,70 », mais « ×1,075 » pour +7,5 % — un toFixed(2) écrivait « ×1,07 »
 * (et « ×1,13 » pour +12,5 %, arrondi dans l'autre sens), un « soit … » faux
 * sur les demi-pourcents. Anglais : « ×1.45 ».
 */
export function formatMultiplier(multiplier: number, locale: string | undefined): string {
    if (!Number.isFinite(multiplier)) return '—';
    // 4 décimales, puis on retire les zéros de queue sans descendre sous 2.
    const text = round(multiplier, 4)
        .toFixed(4)
        .replace(/(\.\d{2}\d*?)0+$/, '$1');
    return `×${isFrench(locale) ? text.replace('.', ',') : text}`;
}

/** Palier last-minute, seuil OBA ou week-end (multiplicateur) → son effet en % : « −30 % », « +10 % ». */
export function multiplierEquivalent(multiplier: number, locale: string | undefined): string {
    return formatSignedPercent(multiplierToPercentDelta(Number(multiplier)), locale);
}

/** Saison (pourcentage) → son multiplicateur : « ×1,45 ». */
export function percentEquivalent(percent: number, locale: string | undefined): string {
    return formatMultiplier(percentDeltaToMultiplier(Number(percent)), locale);
}

/** Prime long terme (fraction stockée) → son multiplicateur : 0.10 → « ×1,10 ». */
export function farOutEquivalent(farOut: unknown, locale: string | undefined): string {
    return formatMultiplier(1 + normalizeFarOut(farOut).premium_pct, locale);
}

/**
 * Le libellé « soit … » : `template` vient de t(NS, 'unit_equivalent') —
 * « soit :value » en français, « i.e. :value » en anglais.
 */
export function equivalentLabel(template: string, value: string): string {
    return template.replace(':value', value);
}
