/**
 * Fiche logement (TL-305) — logique PURE de l'écran : nombres, montants, dates,
 * libellés du dépôt de garantie et segments de la barre d'annulation.
 *
 * Aucun import React : le harnais Vitest tourne en environnement `node`. Les
 * textes passent par `t` (même signature que `useTranslation`) avec un repli
 * français, pour que les tests lisent de vrais libellés.
 */
import type { LogementShowCancellation, LogementShowPricing, LogementShowStay, LogementShowStayRules } from '@/types/logement-show';

export const NS = 'properties.info-logement';

/** Signature de `t()` de `useTranslation`, pour les fonctions qui la reçoivent. */
export type Translate = (namespace: string, key: string, fallback?: string) => string;

/** Locale Intl de l'écran : l'application ne connaît que `fr` et `en`. */
export function intlLocale(locale: string | null | undefined): string {
    return locale === 'en' ? 'en-GB' : 'fr-FR';
}

/** Remplace les `:param` d'un texte traduit. Un seul passage : une valeur injectée n'est jamais relue. */
export function fill(template: string, params: Record<string, string | number>): string {
    return template.replace(/:([a-z_]+)/g, (match, name: string) => (name in params ? String(params[name]) : match));
}

export function formatNumber(value: number, locale: string, maxFractionDigits = 0): string {
    return new Intl.NumberFormat(intlLocale(locale), { maximumFractionDigits: maxFractionDigits }).format(value);
}

/** « 1 500,00 USD » / « 1,500.00 USD » — deux décimales, code devise après le montant. */
export function formatMoney(amount: number, currency: string | null, locale: string): string {
    const value = new Intl.NumberFormat(intlLocale(locale), { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount);
    return currency ? `${value} ${currency}` : value;
}

/** Taux 0–100 → « 84,3 % » / « 84.3% ». */
export function formatPercent(rate: number, locale: string): string {
    return new Intl.NumberFormat(intlLocale(locale), { style: 'percent', maximumFractionDigits: 1 }).format(rate / 100);
}

/** Pluriel selon la langue : en français « 1,5 nuit », en anglais « 1.5 nights ». */
function isOne(count: number, locale: string): boolean {
    return new Intl.PluralRules(intlLocale(locale)).select(count) === 'one';
}

/** « 1 nuit » / « 3 nuits » / « 4,5 nuits ». */
export function nightsLabel(nights: number, locale: string, t: Translate): string {
    const template = isOne(nights, locale) ? t(NS, 'night_one', ':count nuit') : t(NS, 'night_other', ':count nuits');
    return fill(template, { count: formatNumber(nights, locale, 1) });
}

/** 0 → « Rez-de-chaussée », null → « Non défini », sinon le numéro. */
export function floorLabel(floor: number | null, locale: string, t: Translate): string {
    if (floor === null) {
        return t(NS, 'not_defined', 'Non défini');
    }
    if (floor === 0) {
        return t(NS, 'floor_ground', 'Rez-de-chaussée');
    }
    return formatNumber(floor, locale);
}

/** Libellé du dépôt de garantie selon son type. */
export function depositLabel(deposit: LogementShowPricing['deposit'], currency: string | null, locale: string, t: Translate): string {
    const { type, amount } = deposit;
    if (type === 'none') {
        return t(NS, 'deposit_none', 'Aucun dépôt');
    }
    if (type === 'unknown' || amount === null) {
        return t(NS, 'not_defined', 'Non défini');
    }
    switch (type) {
        case 'percent_without_cleaning':
            return fill(t(NS, 'deposit_percent_without_cleaning', ':percent % du total (hors ménage)'), { percent: formatNumber(amount, locale, 2) });
        case 'percent_total':
            return fill(t(NS, 'deposit_percent_total', ':percent % du total'), { percent: formatNumber(amount, locale, 2) });
        case 'per_night':
            return fill(t(NS, 'deposit_per_night', ':amount par nuit'), { amount: formatMoney(amount, currency, locale) });
        case 'per_stay':
            return fill(t(NS, 'deposit_per_stay', ':amount par séjour'), { amount: formatMoney(amount, currency, locale) });
        default:
            return t(NS, 'not_defined', 'Non défini');
    }
}

/** Un montant est saisi mais le type « aucun dépôt » l'annule : on le signale au lieu de le taire. */
export function isDepositAmountIgnored(deposit: LogementShowPricing['deposit']): boolean {
    return deposit.type === 'none' && (deposit.amount ?? 0) > 0;
}

/** Plage d'arrivée : « 16:00 → 02:00 », « dès 16:00 », ou « Non défini ». */
export function checkInWindow(rules: LogementShowStayRules, t: Translate): string {
    const { check_in_from: from, check_in_to: to } = rules;
    if (from && to) {
        return fill(t(NS, 'checkin_range', ':from → :to'), { from, to });
    }
    if (from) {
        return fill(t(NS, 'checkin_from_only', 'dès :from'), { from });
    }
    if (to) {
        return fill(t(NS, 'checkin_until_only', "jusqu'à :to"), { to });
    }
    return t(NS, 'not_defined', 'Non défini');
}

/* ─────────────────────────────────────────────────────────────────────────
 * Dates — toujours lues depuis les champs `*_iso` (aaaa-mm-jj).
 * ───────────────────────────────────────────────────────────────────────── */

function parseIsoDate(iso: string): Date | null {
    const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
    // Date locale (et non UTC) : « 2026-10-08 » reste le 8 quel que soit le fuseau.
    return match ? new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])) : null;
}

/** « 08/10 » depuis « 2026-10-08 ». */
export function dayMonth(iso: string): string {
    const match = /^\d{4}-(\d{2})-(\d{2})/.exec(iso);
    return match ? `${match[2]}/${match[1]}` : iso;
}

/** « 08/10/2026 » depuis « 2026-10-08 ». */
export function frenchDate(iso: string): string {
    const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
    return match ? `${match[3]}/${match[2]}/${match[1]}` : iso;
}

/** Ajoute des jours à une date ISO, en UTC pour ne jamais glisser d'un jour avec le fuseau. */
export function addDaysIso(iso: string, days: number): string {
    const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
    if (!match) {
        return iso;
    }
    return new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]) + days)).toISOString().slice(0, 10);
}

/** « jeudi 08/10 » / « Thursday 08/10 ». */
export function weekdayDayMonth(iso: string, locale: string): string {
    const date = parseIsoDate(iso);
    if (!date) {
        return iso;
    }
    const weekday = new Intl.DateTimeFormat(intlLocale(locale), { weekday: 'long' }).format(date);
    return `${weekday} ${dayMonth(iso)}`;
}

/** « octobre » / « October ». */
export function monthName(iso: string, locale: string): string {
    const date = parseIsoDate(iso);
    return date ? new Intl.DateTimeFormat(intlLocale(locale), { month: 'long' }).format(date) : '';
}

/** Ligne de détail d'un séjour : « 08/10/2026 → 11/10/2026 · 3 nuits · Airbnb · dès 16:00 ». */
export function stayMeta(stay: LogementShowStay, locale: string, t: Translate, options: { dates?: boolean; arrivalTime?: boolean } = {}): string {
    const { dates = true, arrivalTime = false } = options;
    const parts = [
        dates ? `${stay.check_in} → ${stay.check_out}` : null,
        nightsLabel(stay.nights, locale, t),
        stay.source || null,
        arrivalTime && stay.arrival_time ? fill(t(NS, 'arrival_from', 'dès :time'), { time: stay.arrival_time }) : null,
    ];
    return parts.filter((part): part is string => part !== null).join(' · ');
}

/* ─────────────────────────────────────────────────────────────────────────
 * Barre d'annulation — du plus lointain (gauche) à l'arrivée (droite).
 * Couleur = sens : vert gratuit, ambre partiellement facturé, rouge 100 %.
 * ───────────────────────────────────────────────────────────────────────── */

export type CancellationTone = 'free' | 'partial' | 'full';

export interface CancellationSegment {
    tone: CancellationTone;
    /** Part facturée, 0–100 (0 pour l'annulation gratuite). */
    percent: number;
    /** Jours avant l'arrivée, bornes incluses ; `toDays` null = sans borne (« 31 j et plus »). */
    fromDays: number;
    toDays: number | null;
    /** Largeur dans la barre, en % (somme = 100). */
    width: number;
}

/** Largeur minimale d'un segment, pour qu'un palier d'un jour reste visible. */
export const MIN_SEGMENT_WIDTH = 12;

function toneFor(percent: number): CancellationTone {
    if (percent <= 0) {
        return 'free';
    }
    return percent >= 100 ? 'full' : 'partial';
}

/**
 * Répartit 100 % entre des poids : chaque segment reçoit le minimum, le reste
 * est partagé au prorata. Au-delà de 100 / min segments, parts égales.
 */
export function distributeWidths(weights: number[], min = MIN_SEGMENT_WIDTH): number[] {
    const count = weights.length;
    if (count === 0) {
        return [];
    }
    if (count * min >= 100) {
        return weights.map(() => 100 / count);
    }
    const total = weights.reduce((sum, weight) => sum + weight, 0);
    const rest = 100 - count * min;
    return weights.map((weight) => min + (total > 0 ? (rest * weight) / total : rest / count));
}

/**
 * Segments de la barre. Vide quand la politique n'a aucun palier. Le segment
 * « gratuit » n'a pas de borne lointaine : il reçoit un tiers de la durée
 * couverte par les paliers, assez pour se lire sans écraser les autres.
 */
export function cancellationSegments(cancellation: LogementShowCancellation): CancellationSegment[] {
    if (cancellation.tiers.length === 0) {
        return [];
    }

    const tiers = [...cancellation.tiers].sort((a, b) => b.from_days - a.from_days);
    const spans = tiers.map((tier) => Math.max(1, tier.to_days - tier.from_days + 1));
    const tierDays = spans.reduce((sum, span) => sum + span, 0);

    const segments: Omit<CancellationSegment, 'width'>[] = [];
    const weights: number[] = [];

    if (cancellation.free_from_days !== null) {
        segments.push({ tone: 'free', percent: 0, fromDays: cancellation.free_from_days, toDays: null });
        weights.push(Math.max(1, Math.round(tierDays / 3)));
    }

    tiers.forEach((tier, index) => {
        segments.push({ tone: toneFor(tier.percent), percent: tier.percent, fromDays: tier.from_days, toDays: tier.to_days });
        weights.push(spans[index]);
    });

    const widths = distributeWidths(weights);
    return segments.map((segment, index) => ({ ...segment, width: Math.round(widths[index] * 100) / 100 }));
}

/** Titre et plage en jours d'un segment : « 50 % facturés » · « de 8 à 30 j avant l'arrivée ». */
export function cancellationSegmentLabel(segment: CancellationSegment, locale: string, t: Translate): { title: string; range: string } {
    const title =
        segment.percent <= 0
            ? t(NS, 'cancellation_free', 'Gratuite')
            : fill(t(NS, 'cancellation_charged', ':percent % facturés'), { percent: formatNumber(segment.percent, locale, 1) });

    let range: string;
    if (segment.toDays === null) {
        range = fill(t(NS, 'cancellation_range_open', ":days j et plus avant l'arrivée"), { days: segment.fromDays });
    } else if (segment.fromDays === segment.toDays) {
        range =
            segment.fromDays === 0
                ? t(NS, 'cancellation_range_arrival_day', "le jour de l'arrivée")
                : fill(
                      isOne(segment.fromDays, locale)
                          ? t(NS, 'cancellation_range_single_one', ":days j avant l'arrivée")
                          : t(NS, 'cancellation_range_single_other', ":days j avant l'arrivée"),
                      { days: segment.fromDays },
                  );
    } else {
        range = fill(t(NS, 'cancellation_range', "de :from à :to j avant l'arrivée"), { from: segment.fromDays, to: segment.toDays });
    }

    return { title, range };
}

/** Légende du bord gauche de la barre : « 31 j et plus » ou « 60 j avant ». */
export function cancellationAxisStart(segments: CancellationSegment[], t: Translate): string {
    const first = segments[0];
    if (!first) {
        return '';
    }
    return first.toDays === null
        ? fill(t(NS, 'cancellation_axis_far_open', ':days j et plus'), { days: first.fromDays })
        : fill(t(NS, 'cancellation_axis_far', ':days j avant'), { days: first.toDays });
}
