/**
 * Interrupteur « Règle active » de l'éditeur — logique PURE (aucun DOM, aucun
 * appel réseau), testée par rule-sections.test.ts.
 *
 * ═══════════════════════════════════════════════════════════════════════════
 * TROIS ÉTATS, À NE JAMAIS CONFONDRE
 * ═══════════════════════════════════════════════════════════════════════════
 *  - ABSENT : colonne NULL en base (lignes anciennes) → le serveur sert le
 *    défaut ; l'éditeur ne voit donc jamais cet état, il reçoit les valeurs.
 *  - DÉSACTIVÉ : la forme NEUTRE, fournie par le serveur (`sectionDisabled`) :
 *    aucune saison et socle à 0, aucun palier et prime à 0, aucune fenêtre OBA,
 *    week-end à ×1. Elle donne ×1,00 sur toutes les nuits.
 *  - ACTIF : n'importe quelle autre valeur.
 *
 * Le week-end fait exception à la ligne ABSENT : son défaut serveur (colonne
 * NULL) est lui-même NEUTRE (×1). L'éditeur le reçoit donc « désactivé », et
 * c'est vrai : il ne touche à aucun prix.
 *
 * Couper une section écrit sa forme neutre. La rallumer restaure la dernière
 * valeur connue DANS LA SESSION (mémorisée au moment de la couper) si elle a
 * un effet, sinon le défaut fourni par le serveur (`sectionDefaults`) :
 * rallumer ne doit jamais donner une section vide qui ressemblerait à une
 * section active.
 */
import type { FarOutPremium, RuleSectionKey, RuleSectionValues, WeekendRule, YieldSeason, YieldSeasonality } from '../types';
import { MOVABLE_HOLIDAY_KEYS } from './season-calendar';

/** Onglet de l'éditeur qui porte une règle. Le last-minute porte AUSSI la prime long terme. */
export type UiSection = 'seasonality' | 'lastminute' | 'oba' | 'weekend';

/** Onglet → colonnes qu'il pilote (mêmes clés que les groupes et rule_overrides). */
export const SECTION_KEYS: Record<UiSection, RuleSectionKey[]> = {
    seasonality: ['seasonality'],
    lastminute: ['last_minute_rules', 'far_out_premium'],
    oba: ['oba_rules'],
    weekend: ['weekend_rule'],
};

/** Les sept jours ISO, dans l'ordre d'affichage (lundi d'abord). */
export const ISO_DAYS = [1, 2, 3, 4, 5, 6, 7] as const;

/** Nuits par défaut — les mêmes que YieldConfig::WEEKEND_DEFAULT_DAYS : vendredi et samedi. */
export const WEEKEND_DEFAULT_DAYS: readonly number[] = [5, 6];

/** Seuil de repli d'une prime sans seuil — le même que YieldConfig::disabledFarOutPremium(). */
const NEUTRAL_FAR_OUT_THRESHOLD = 60;

/**
 * Ramène une saisonnalité reçue à la forme PAR DATES que l'éditeur sait
 * afficher. L'ancienne forme par mois n'a pas de clé `seasons` : elle devient
 * une saisonnalité vide (socle 0, aucune saison).
 *
 * Les cinq clés FACULTATIVES d'une saison — `repeat`, `layer`, `holiday`,
 * `offset_start`, `offset_end` (récurrence, priorité, fête mobile) — sont
 * recopiées quand elles sont valides, et seulement alors. Sans elles, ouvrir
 * puis enregistrer une configuration ferait perdre à ses saisons leur
 * récurrence et à l'Aïd sa date mobile : l'aperçu et l'enregistrement partent
 * de cette valeur. Une valeur invalide (`layer: 'x'`) est retirée — le serveur
 * la refuserait.
 */
export function normalizeSeasonality(value: unknown): YieldSeasonality {
    if (value && typeof value === 'object' && Array.isArray((value as { seasons?: unknown }).seasons)) {
        const v = value as YieldSeasonality;
        return {
            base_pct: typeof v.base_pct === 'number' ? v.base_pct : 0,
            seasons: v.seasons.map((s) => normalizeSeason(s)),
        };
    }
    return { base_pct: 0, seasons: [] };
}

/** Une saison reçue : les quatre clés de base, et les clés facultatives valides. */
function normalizeSeason(s: YieldSeason): YieldSeason {
    const season: YieldSeason = {
        name: s.name ?? '',
        start_date: s.start_date ?? '',
        end_date: s.end_date ?? '',
        pct: typeof s.pct === 'number' ? s.pct : 0,
    };
    if (s.repeat === 'yearly') season.repeat = 'yearly';
    if (s.layer === 'season' || s.layer === 'event') season.layer = s.layer;
    if (typeof s.holiday === 'string' && (MOVABLE_HOLIDAY_KEYS as readonly string[]).includes(s.holiday)) season.holiday = s.holiday;
    if (Number.isInteger(s.offset_start)) season.offset_start = s.offset_start;
    if (Number.isInteger(s.offset_end)) season.offset_end = s.offset_end;
    return season;
}

/**
 * Ramène toute prime long terme reçue à la forme que l'éditeur sait afficher.
 *
 * Une prime sans seuil — `[]` venu de PHP, `{}`, `undefined` — ne rapporte
 * rien au calculateur : elle devient la forme neutre {60 j, 0}. Sans cela le
 * champ lisait `undefined` et l'aide affichait « ×NaN ».
 */
export function normalizeFarOut(value: unknown): FarOutPremium {
    if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
        const v = value as { threshold_days?: unknown; premium_pct?: unknown };
        const threshold = Number(v.threshold_days);
        if (v.threshold_days !== undefined && v.threshold_days !== null && Number.isFinite(threshold)) {
            const premium = Number(v.premium_pct);
            return {
                threshold_days: threshold,
                premium_pct: v.premium_pct !== undefined && v.premium_pct !== null && Number.isFinite(premium) ? premium : 0,
            };
        }
    }

    return { threshold_days: NEUTRAL_FAR_OUT_THRESHOLD, premium_pct: 0 };
}

/**
 * Ramène toute règle week-end reçue à la forme {multiplier, days} — même
 * contrat que YieldConfig::normalizeWeekendRule() côté serveur.
 *
 *  - rien du tout (`undefined`, `null` : clé absente d'un ancien payload) →
 *    le défaut du serveur, ×1 sur vendredi et samedi : aucun effet ;
 *  - une partie illisible ne produit AUCUN effet, jamais un effet deviné :
 *    multiplicateur illisible → ×1, liste de jours illisible → aucune nuit
 *    (et non vendredi-samedi, qui appliquerait un multiplicateur à des nuits
 *    que personne n'a cochées) ;
 *  - jours gardés s'ils sont entiers de 1 à 7, sans doublon, triés.
 */
export function normalizeWeekend(value: unknown): WeekendRule {
    if (value === undefined || value === null) {
        return { multiplier: 1, days: [...WEEKEND_DEFAULT_DAYS] };
    }
    if (typeof value !== 'object' || Array.isArray(value)) {
        return { multiplier: 1, days: [] };
    }

    const v = value as { multiplier?: unknown; days?: unknown };
    const multiplier = typeof v.multiplier === 'number' || typeof v.multiplier === 'string' ? Number(v.multiplier) : Number.NaN;
    const days = Array.isArray(v.days)
        ? Array.from(new Set(v.days.map(Number).filter((d) => Number.isInteger(d) && d >= 1 && d <= 7))).sort((a, b) => a - b)
        : [];

    return { multiplier: Number.isFinite(multiplier) && multiplier > 0 ? multiplier : 1, days };
}

/**
 * La règle week-end telle que l'ÉDITEUR la tient : jours normalisés comme
 * ci-dessus, mais multiplicateur GARDÉ TEL QUEL — 0, négatif, ou NaN d'un
 * champ vidé compris.
 *
 * normalizeWeekend() sert à LIRE une valeur stockée (×1 plutôt qu'un effet
 * deviné). Appliquée à la saisie en cours, elle réécrivait « 0 » en « 1 »
 * sous le curseur : taper « 0.9 » donnait ×1,9 (+90 % au lieu de −10 %),
 * dans les bornes, donc enregistré sans refus. Une saisie hors bornes doit au
 * contraire partir telle quelle : le serveur la refuse et l'écran le dit.
 */
export function editableWeekend(value: unknown): WeekendRule {
    const normalized = normalizeWeekend(value);
    const typed = value !== null && typeof value === 'object' && !Array.isArray(value) ? (value as { multiplier?: unknown }).multiplier : undefined;
    return { multiplier: typeof typed === 'number' ? typed : normalized.multiplier, days: normalized.days };
}

/**
 * Coche ou décoche une nuit : la liste reste triée et sans doublon, pour que
 * ouvrir puis enregistrer sans toucher renvoie exactement la valeur stockée.
 */
export function toggleWeekendDay(rule: WeekendRule, day: number, checked: boolean): WeekendRule {
    const others = rule.days.filter((d) => d !== day);
    return { ...rule, days: (checked ? [...others, day] : others).sort((a, b) => a - b) };
}

/**
 * La section a-t-elle un effet possible sur un prix ?
 *  - saisons : au moins une saison, ou un socle ≠ 0 ;
 *  - last-minute : au moins un palier, ou une prime long terme ≠ 0 ;
 *  - OBA : au moins une fenêtre ;
 *  - week-end : un multiplicateur ≠ 1 ET au moins une nuit cochée.
 */
export function isSectionEnabled(ui: UiSection, values: RuleSectionValues): boolean {
    switch (ui) {
        case 'seasonality':
            return (values.seasonality?.seasons?.length ?? 0) > 0 || Number(values.seasonality?.base_pct ?? 0) !== 0;
        case 'lastminute':
            return (values.last_minute_rules?.length ?? 0) > 0 || normalizeFarOut(values.far_out_premium).premium_pct !== 0;
        case 'oba':
            return (values.oba_rules?.length ?? 0) > 0;
        case 'weekend': {
            const weekend = normalizeWeekend(values.weekend_rule);
            return weekend.multiplier !== 1 && weekend.days.length > 0;
        }
    }
}

/** Les seules valeurs que l'onglet pilote — ce qu'on mémorise avant de le couper. */
export function pickSection(ui: UiSection, values: RuleSectionValues): Partial<RuleSectionValues> {
    const picked: Partial<RuleSectionValues> = {};
    for (const key of SECTION_KEYS[ui]) {
        assign(picked, key, clone(values[key]));
    }
    return picked;
}

/** Coupe l'onglet : ses colonnes prennent la forme neutre (le last-minute coupe aussi la prime). */
export function disableSection(ui: UiSection, values: RuleSectionValues, disabled: RuleSectionValues): RuleSectionValues {
    const next: RuleSectionValues = { ...values };
    for (const key of SECTION_KEYS[ui]) {
        assign(next, key, clone(disabled[key]));
    }
    return next;
}

/**
 * Rallume l'onglet : la valeur mémorisée à la coupure si elle existe ET a un
 * effet, sinon le défaut du serveur — colonne par colonne.
 *
 * Une valeur mémorisée NEUTRE est ignorée : on peut vider l'onglet à la main
 * (supprimer toutes les saisons) sans qu'il se coupe, puis le couper et le
 * rallumer. Rendre alors la forme vide afficherait un onglet « actif » qui ne
 * touche à aucun prix — exactement ce que l'interrupteur doit empêcher.
 */
export function enableSection(
    ui: UiSection,
    values: RuleSectionValues,
    remembered: Partial<RuleSectionValues> | undefined,
    defaults: RuleSectionValues,
): RuleSectionValues {
    const usable = remembered !== undefined && isSectionEnabled(ui, { ...values, ...remembered });
    const next: RuleSectionValues = { ...values };
    for (const key of SECTION_KEYS[ui]) {
        const source = usable && remembered[key] !== undefined ? remembered[key] : defaults[key];
        assign(next, key, clone(source));
    }
    // Une prime mémorisée sous une forme incomplète redevient affichable.
    if (ui === 'lastminute') {
        next.far_out_premium = normalizeFarOut(next.far_out_premium);
    }
    if (ui === 'weekend') {
        next.weekend_rule = normalizeWeekend(next.weekend_rule);
    }
    return next;
}

/** Les sections d'un groupe lié, telles que le serveur les sert : `null` = section non portée. */
export type GroupSectionValues = { [K in RuleSectionKey]?: RuleSectionValues[K] | null };

/**
 * « Revenir au groupe » : l'onglet suit de nouveau le groupe. Ses colonnes
 * sortent de `rule_overrides` et reprennent la valeur du GROUPE, lue avec les
 * mêmes normalisations qu'à l'ouverture de l'éditeur — sans quoi l'aperçu et
 * l'enregistrement partiraient avec la valeur personnalisée pendant que
 * l'écran annonce « suit le groupe ».
 *
 * Une colonne que le groupe ne porte pas (null) garde la valeur du formulaire.
 */
export function reattachSection<F extends RuleSectionValues & { rule_overrides: RuleSectionKey[] }>(
    ui: UiSection,
    form: F,
    group: GroupSectionValues | null | undefined,
): F {
    const keys = SECTION_KEYS[ui];
    const next: F = { ...form, rule_overrides: form.rule_overrides.filter((k) => !keys.includes(k)) };
    for (const key of keys) {
        const groupValue = group?.[key];
        if (groupValue === null || groupValue === undefined) continue;
        assign(next, key, normalizeSectionValue(key, clone(groupValue)));
    }
    return next;
}

/** La normalisation de lecture propre à chaque colonne (identité pour les listes). */
function normalizeSectionValue(key: RuleSectionKey, value: unknown): unknown {
    switch (key) {
        case 'seasonality':
            return normalizeSeasonality(value);
        case 'far_out_premium':
            return normalizeFarOut(value);
        case 'weekend_rule':
            return normalizeWeekend(value);
        default:
            return value;
    }
}

/** Affectation typée clé par clé (TypeScript ne relie pas `key` au type de la valeur). */
function assign<K extends RuleSectionKey>(target: Partial<RuleSectionValues>, key: K, value: unknown): void {
    target[key] = value as RuleSectionValues[K];
}

/** Copie profonde : les éditeurs ne mutent rien, mais une valeur partagée avec les props ne doit jamais l'être. */
function clone<T>(value: T): T {
    return value === undefined ? value : (JSON.parse(JSON.stringify(value)) as T);
}
