/**
 * Lecture des réponses d'erreur de l'API Yield — logique PURE (aucun appel
 * réseau, aucun DOM), pour qu'elle soit testable et partagée par tous les
 * appelants `fetch` du dossier yield.
 *
 * ═══════════════════════════════════════════════════════════════════════════
 * POURQUOI CE FICHIER EXISTE
 * ═══════════════════════════════════════════════════════════════════════════
 * Le durcissement serveur a doté POST /yield/{id}/preview d'un Form Request :
 * l'aperçu renvoie désormais 422 sur une bande tarifaire incohérente
 * (min_price gt:0, base_price gte:min_price, max_price gte:base_price).
 *
 * Or les appelants consommaient le corps de la réponse SANS tester
 * `response.ok` : sur un 422, `result.preview` valait `undefined` et le
 * calendrier plantait au rendu (`preview.map(...)` sur `undefined`), sans
 * ErrorBoundary pour rattraper. Le chemin est banal : vider le champ
 * « Prix minimum » suffit (Number('') === 0, refusé par `gt:0`), tout comme
 * glisser le curseur « max » sous « base ».
 *
 * Le corps d'un 422 Laravel a toujours la forme :
 *   { message: "...", errors: { min_price: ["...", "..."], ... } }
 * On en tire UN message par champ (le premier, le plus précis) pour l'afficher
 * à côté du champ concerné, plus un message global pour l'encadré rouge.
 */

/** Un message d'erreur par champ, prêt à afficher. */
export type FieldErrors = Record<string, string>;

/** Code HTTP d'un refus de validation Laravel. */
export const VALIDATION_STATUS = 422;

/** Description d'un appel API refusé, quelle qu'en soit la cause. */
export interface ApiFailure {
    /** Code HTTP ; 0 quand la requête n'a même pas abouti (réseau coupé). */
    status: number;
    /** Messages par champ — vide si la réponse n'était pas un 422 exploitable. */
    errors: FieldErrors;
    /** Message global, TOUJOURS renseigné : l'appelant peut l'afficher tel quel. */
    message: string;
}

/** Résultat d'un appel API : soit le corps décodé, soit un échec décrit. */
export type ApiResult<T> = { ok: true; data: T } | { ok: false; failure: ApiFailure };

/**
 * Aplatit `{ errors: { champ: [msg, ...] } }` en `{ champ: msg }`.
 *
 * Tolère tout ce que le serveur peut réellement renvoyer : corps vide, HTML
 * d'une page d'erreur, `errors` absent, valeur scalaire au lieu d'un tableau,
 * tableau vide. Dans tous ces cas on rend un objet vide plutôt que de laisser
 * fuiter un `undefined` dans l'interface.
 */
export function flattenValidationErrors(payload: unknown): FieldErrors {
    if (payload == null || typeof payload !== 'object') {
        return {};
    }

    const brut = (payload as { errors?: unknown }).errors;
    if (brut == null || typeof brut !== 'object' || Array.isArray(brut)) {
        return {};
    }

    const aplati: FieldErrors = {};
    for (const [champ, messages] of Object.entries(brut as Record<string, unknown>)) {
        if (Array.isArray(messages)) {
            const premier = messages.find((m) => typeof m === 'string' && m.trim() !== '');
            if (typeof premier === 'string') {
                aplati[champ] = premier;
            }
        } else if (typeof messages === 'string' && messages.trim() !== '') {
            aplati[champ] = messages;
        }
    }

    return aplati;
}

/** Premier message affichable d'un jeu d'erreurs, ou `null` s'il n'y en a aucun. */
export function firstErrorMessage(errors: FieldErrors): string | null {
    for (const message of Object.values(errors)) {
        if (message.trim() !== '') return message;
    }
    return null;
}

/**
 * Message global d'un échec, en français.
 *
 * Ordre de préférence : le premier message de validation (le plus précis, il
 * NOMME le champ fautif) → le `message` global de Laravel → un repli selon le
 * code HTTP. Un repli générique n'est utilisé qu'en dernier recours, jamais à
 * la place d'une raison connue.
 */
export function failureMessage(status: number, payload: unknown, errors: FieldErrors): string {
    const premier = firstErrorMessage(errors);
    if (premier) return premier;

    if (payload != null && typeof payload === 'object') {
        const message = (payload as { message?: unknown }).message;
        // Laravel renvoie « The given data was invalid. » sur un 422 sans détail :
        // inutile pour l'utilisateur, on préfère notre repli français ci-dessous.
        if (typeof message === 'string' && message.trim() !== '' && status !== VALIDATION_STATUS) {
            return message;
        }
    }

    if (status === VALIDATION_STATUS) {
        return 'Les valeurs saisies sont invalides.';
    }
    if (status === 403) {
        return "Vous n'avez pas le droit d'effectuer cette action.";
    }
    if (status === 419) {
        return 'Votre session a expiré. Rechargez la page.';
    }
    if (status === 0) {
        return 'Le serveur est injoignable. Vérifiez votre connexion.';
    }
    return `Le serveur a répondu une erreur (${status}).`;
}

/** Construit l'échec complet à partir du code HTTP et du corps décodé. */
export function buildFailure(status: number, payload: unknown): ApiFailure {
    const errors = status === VALIDATION_STATUS ? flattenValidationErrors(payload) : {};
    return { status, errors, message: failureMessage(status, payload, errors) };
}

/** Les trois champs de la bande tarifaire, dans l'ordre où l'écran les présente. */
export const PRICE_BAND_FIELDS = ['min_price', 'base_price', 'max_price'] as const;

export type PriceBandField = (typeof PRICE_BAND_FIELDS)[number];

/**
 * Extrait les messages qui concernent la bande tarifaire, pour les afficher
 * SOUS le champ fautif plutôt que seulement dans l'encadré récapitulatif.
 */
export function priceBandErrors(errors: FieldErrors): Partial<Record<PriceBandField, string>> {
    const bande: Partial<Record<PriceBandField, string>> = {};
    for (const champ of PRICE_BAND_FIELDS) {
        if (errors[champ]) bande[champ] = errors[champ];
    }
    return bande;
}
