import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useTranslation } from '@/hooks/use-translation';
import { useForm } from '@inertiajs/react';
import { Calendar, CalendarDays, Clock, Eye, Link2, Lock, Percent, Save, Settings, TriangleAlert } from 'lucide-react';
import { useEffect, useId, useRef, useState } from 'react';
import { toast } from 'sonner';
import type { SeasonTemplate } from '../../logements/season-range-utils';
import { useCanManageYield, useReadOnlyReason } from '../hooks/useYieldPermissions';
import { firstErrorMessage, priceBandErrors, type FieldErrors } from '../lib/api-errors';
import { previewKey, previewPayload } from '../lib/rule-payloads';
import {
    disableSection,
    enableSection,
    isSectionEnabled,
    normalizeFarOut,
    normalizeSeasonality,
    normalizeWeekend,
    pickSection,
    reattachSection,
    SECTION_KEYS,
    type UiSection,
} from '../lib/rule-sections';
import { postYieldJson } from '../lib/yield-api';
import type {
    FarOutPremium,
    LastMinuteRule,
    MovableHolidays,
    ObaRule,
    PriceDay,
    RuleSectionKey,
    RuleSectionValues,
    WeekendRule,
    YieldSeasonality,
} from '../types';
import HelpHint from './HelpHint';
import PriceSliders from './PriceSliders';
import LastMinuteEditor from './rules/LastMinuteEditor';
import ObaEditor from './rules/ObaEditor';
import SeasonsEditor from './rules/SeasonsEditor';
import WeekendEditor from './rules/WeekendEditor';

const NS = 'yield.index';

/** The group's own section values, exposed so a re-attached section can be reset to them. */
export interface LinkedRuleGroup {
    id: number;
    name: string;
    sections: RuleSectionKey[];
    seasonality: YieldSeasonality | null;
    last_minute_rules: LastMinuteRule[] | null;
    far_out_premium: FarOutPremium | null;
    oba_rules: ObaRule[] | null;
    weekend_rule: WeekendRule | null;
}

export interface YieldConfigData {
    id: number;
    property_id: string;
    internal_id: number;
    property_name: string;
    is_active: boolean;
    min_price: number;
    base_price: number;
    max_price: number;
    seasonality: YieldSeasonality | Record<string, { label: string; multiplier: number }>;
    last_minute_rules: LastMinuteRule[];
    oba_rules: ObaRule[];
    far_out_premium: FarOutPremium;
    /** Toujours complète côté serveur (défaut ×1 = aucun effet) ; normalisée quand même à la lecture. */
    weekend_rule: WeekendRule;
    rule_group_id: number | null;
    rule_overrides: RuleSectionKey[];
    rule_group: LinkedRuleGroup | null;
    last_pushed_at: string | null;
    last_push_status: string | null;
}


type YieldFormData = {
    min_price: number;
    base_price: number;
    max_price: number;
    // Le type partagé, et non une copie à quatre clés : une saison porte aussi
    // sa récurrence, son niveau et sa fête mobile (types.ts).
    seasonality: YieldSeasonality;
    last_minute_rules: { days_before: number; multiplier: number; label: string }[];
    oba_rules: { window: [number, number]; thresholds: { occ: number; mult: number }[] }[];
    far_out_premium: { threshold_days: number; premium_pct: number };
    weekend_rule: { multiplier: number; days: number[] };
    rule_overrides: RuleSectionKey[];
};

/** Les cinq sections de règles du formulaire (ce que pilotent les interrupteurs « Règle active »). */
const sectionValues = (d: YieldFormData): RuleSectionValues => ({
    seasonality: d.seasonality,
    last_minute_rules: d.last_minute_rules,
    far_out_premium: d.far_out_premium,
    oba_rules: d.oba_rules,
    weekend_rule: d.weekend_rule,
});

interface YieldRuleEditorProps {
    config: YieldConfigData;
    obaPresets: Record<string, ObaRule[]>;
    exchangeRate: number;
    seasonTemplates?: SeasonTemplate[];
    /** Dates des fêtes mobiles (config/yield-holidays.php), pour l'onglet Saisons. */
    movableHolidays?: MovableHolidays;
    onPreview?: (preview: PriceDay[]) => void;
    /**
     * Signale que l'aperçu affiché n'est plus le reflet de la saisie (celle-ci
     * est refusée par le serveur). Le parent garde la dernière grille valide à
     * l'écran et peut la marquer comme figée.
     */
    onPreviewStale?: (stale: boolean) => void;
    onBasePriceChange?: (basePrice: number) => void;
    onSaved?: () => void;
    hideSaveButton?: boolean;
    /** Valeurs par défaut du serveur (YieldConfig::sectionDefaults) : rallumer une section sans valeur mémorisée. */
    sectionDefaults: RuleSectionValues;
    /** Formes neutres du serveur (YieldConfig::sectionDisabledValues) : couper une section. */
    sectionDisabled: RuleSectionValues;
}

/**
 * Single source of truth for editing a logement's yield rules. Owns the Inertia
 * form, the debounced preview, and PUT yield.update on save.
 *
 * When the config is LIVE-linked to a rule group, each rule section is locked
 * (read-only, served by the group) until the user "personalises" it — detaching
 * just that section into rule_overrides. Re-attaching resets the section to the
 * group's value so preview and save stay consistent.
 *
 * Chaque onglet de règle porte un interrupteur « Règle active ». Coupé, la
 * section prend sa forme NEUTRE (fournie par le serveur, ×1,00 sur toutes les
 * nuits) et l'éditeur dit « Règle désactivée ». Rallumé, elle reprend la
 * dernière valeur connue DANS LA SESSION (mémorisée à la coupure) si elle a un
 * effet, sinon le défaut du serveur — jamais une section vide qui aurait
 * l'air active.
 * L'interrupteur est grisé quand la section suit le groupe (c'est le groupe
 * qui décide) et sans `yield.manage`.
 */
export default function YieldRuleEditor({
    config,
    obaPresets,
    exchangeRate,
    seasonTemplates = [],
    movableHolidays,
    onPreview,
    onPreviewStale,
    onBasePriceChange,
    onSaved,
    hideSaveButton = false,
    sectionDefaults,
    sectionDisabled,
}: YieldRuleEditorProps) {
    const { t } = useTranslation();
    const canManage = useCanManageYield();
    const readOnlyReason = useReadOnlyReason();
    const { data, setData, put, processing } = useForm<YieldFormData>({
        min_price: config.min_price,
        base_price: config.base_price,
        max_price: config.max_price,
        seasonality: normalizeSeasonality(config.seasonality),
        last_minute_rules: config.last_minute_rules,
        oba_rules: config.oba_rules,
        // Une prime reçue sans ses clés ([] de PHP) devient la forme neutre
        // affichable ; le calculateur n'y applique déjà aucune prime.
        far_out_premium: normalizeFarOut(config.far_out_premium),
        // Clé absente (réponse d'un serveur pas encore à jour) = défaut ×1, sans effet.
        weekend_rule: normalizeWeekend(config.weekend_rule),
        rule_overrides: config.rule_overrides ?? [],
    });

    /**
     * État de l'interrupteur de chaque onglet, tenu À PART des valeurs : sinon
     * supprimer la dernière saison à la main ferait basculer l'onglet sur
     * « Règle désactivée » et escamoterait l'éditeur sous le curseur.
     */
    const [sectionOn, setSectionOn] = useState<Record<UiSection, boolean>>(() => ({
        seasonality: isSectionEnabled('seasonality', sectionValues(data)),
        lastminute: isSectionEnabled('lastminute', sectionValues(data)),
        oba: isSectionEnabled('oba', sectionValues(data)),
        weekend: isSectionEnabled('weekend', sectionValues(data)),
    }));
    /** Valeurs d'un onglet au moment où on l'a coupé, pour les rendre s'il est rallumé. */
    const remembered = useRef<Partial<Record<UiSection, Partial<RuleSectionValues>>>>({});
    const switchId = useId();

    const [previewLoading, setPreviewLoading] = useState(false);
    /** Messages de validation du DERNIER aperçu refusé (vide = saisie valide). */
    const [previewErrors, setPreviewErrors] = useState<FieldErrors>({});
    /** Message global du refus, affiché en tête de l'encadré rouge. */
    const [previewMessage, setPreviewMessage] = useState<string | null>(null);

    const group = config.rule_group;

    /** Whether a UI section is currently served by (locked to) the group. */
    const followsGroup = (ui: UiSection): boolean => {
        if (!group) return false;
        const keys = SECTION_KEYS[ui];
        const inGroup = keys.some((k) => group.sections.includes(k));
        const overridden = keys.some((k) => data.rule_overrides.includes(k));
        return inGroup && !overridden;
    };

    /**
     * Une section est éteinte soit parce qu'elle suit le groupe, soit parce que
     * l'utilisateur n'a pas `yield.manage` (lecture seule).
     */
    const sectionLocked = (ui: UiSection): boolean => !canManage || followsGroup(ui);

    /**
     * L'onglet est-il actif ? Quand il suit le groupe, c'est la valeur du
     * groupe qui le dit (un groupe peut porter une section neutre) ; sinon,
     * l'interrupteur.
     */
    const isOn = (ui: UiSection): boolean => (followsGroup(ui) ? isSectionEnabled(ui, sectionValues(data)) : sectionOn[ui]);

    /** Coupe (forme neutre) ou rallume (valeur mémorisée, sinon défaut) un onglet. */
    const toggleSection = (ui: UiSection, on: boolean) => {
        if (sectionLocked(ui)) return;
        if (on) {
            setData((prev) => ({ ...prev, ...enableSection(ui, sectionValues(prev), remembered.current[ui], sectionDefaults) }));
        } else {
            remembered.current[ui] = pickSection(ui, sectionValues(data));
            setData((prev) => ({ ...prev, ...disableSection(ui, sectionValues(prev), sectionDisabled) }));
        }
        setSectionOn((prev) => ({ ...prev, [ui]: on }));
    };

    /** Titre de l'interrupteur quand il est grisé : pourquoi on ne peut pas y toucher. */
    const switchLockReason = (ui: UiSection): string | undefined => {
        if (!canManage) return readOnlyReason;
        if (followsGroup(ui)) return t(NS, 'section_follows_group_hint', 'Réglée par le groupe lié : « Personnaliser » pour la modifier sur ce logement.');
        return undefined;
    };

    /** Detach a section to customise it on this house (keeps the current group value as the starting point). */
    const detach = (ui: UiSection) => {
        const keys = SECTION_KEYS[ui];
        setData('rule_overrides', Array.from(new Set([...data.rule_overrides, ...keys])));
        // Détachée, la section repart de la valeur du groupe : l'interrupteur la reflète.
        setSectionOn((prev) => ({ ...prev, [ui]: isSectionEnabled(ui, sectionValues(data)) }));
    };

    /** Re-attach a section: reset its value(s) to the group's and follow the group again (reattachSection, testée). */
    const reattach = (ui: UiSection) => {
        setData((prev) => reattachSection(ui, prev, group));
    };

    const onPreviewRef = useRef(onPreview);
    useEffect(() => {
        onPreviewRef.current = onPreview;
    }, [onPreview]);

    const onPreviewStaleRef = useRef(onPreviewStale);
    useEffect(() => {
        onPreviewStaleRef.current = onPreviewStale;
    }, [onPreviewStale]);

    // Debounced preview — recompute on any rule change (preview always reflects the
    // exact rules in the editor; the server detaches the group for the preview calc).
    // La dépendance est le corps POSTÉ lui-même (previewKey) : une liste écrite à
    // la main à côté du corps pouvait oublier une section — postée, mais dont la
    // modification ne relançait jamais l'aperçu.
    const previewDependency = previewKey(data);
    useEffect(() => {
        const timer = setTimeout(() => refreshPreview(), 800);
        return () => clearTimeout(timer);
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [previewDependency]);

    /**
     * Recalcule l'aperçu.
     *
     * Le serveur valide désormais la requête (PreviewYieldPricesRequest) et
     * répond 422 sur une bande incohérente. Deux règles ici :
     *
     *  1. on teste `ok` AVANT de toucher au corps — un 422 ne porte pas de
     *     `preview`, et le passer au calendrier faisait planter l'écran ;
     *  2. on NE VIDE PAS l'aperçu sur refus : la dernière grille valide reste
     *     affichée (l'utilisateur doit voir ce qu'il compare) et on se contente
     *     de la marquer comme figée en expliquant pourquoi.
     */
    async function refreshPreview() {
        setPreviewLoading(true);

        const result = await postYieldJson<{ preview: PriceDay[] }>(route('yield.preview', config.id), previewPayload(data));

        setPreviewLoading(false);

        if (!result.ok) {
            setPreviewErrors(result.failure.errors);
            setPreviewMessage(result.failure.message);
            onPreviewStaleRef.current?.(true);
            return;
        }

        setPreviewErrors({});
        setPreviewMessage(null);
        onPreviewStaleRef.current?.(false);
        // Le serveur renvoie toujours un tableau, mais on ne le suppose pas :
        // c'est exactement cette supposition qui a cassé l'écran.
        onPreviewRef.current?.(Array.isArray(result.data?.preview) ? result.data.preview : []);
    }

    function handleSave() {
        // Garde-fou : le bouton est déjà éteint, mais l'enregistrement ne doit
        // pas pouvoir partir en 403 par un autre chemin (raccourci, focus).
        if (!canManage) return;

        put(route('yield.update', config.id), {
            preserveState: true,
            onSuccess: () => {
                toast.success(t(NS, 'toast_saved', 'Configuration sauvegardée'));
                onSaved?.();
            },
            // Un refus à l'enregistrement (422) était SILENCIEUX : le bouton
            // redevenait cliquable et rien ne disait que rien n'était sauvé.
            onError: (errors) => {
                toast.error(firstErrorMessage(errors) ?? t(NS, 'toast_save_error', 'Configuration non enregistrée : vérifiez les règles.'));
            },
        });
    }

    function handlePriceChange(field: 'min_price' | 'base_price' | 'max_price', value: number) {
        setData(field, value);
        if (field === 'base_price') onBasePriceChange?.(value);
    }

    const previewErrorList = Object.values(previewErrors);

    return (
        <div className="space-y-4">
            {!canManage && (
                <div className="flex items-center gap-2 rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:border-amber-900/40 dark:bg-amber-950/30 dark:text-amber-200">
                    <Eye className="h-4 w-4 shrink-0" />
                    <span>{readOnlyReason}</span>
                </div>
            )}

            {/* Refus de l'aperçu — même langage visuel que le slide-over des
                groupes de règles : encadré rouge + liste des raisons. */}
            {(previewMessage || previewErrorList.length > 0) && (
                <div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900/40 dark:bg-red-950/30 dark:text-red-300">
                    <p className="flex items-center gap-1.5 font-medium">
                        <TriangleAlert className="h-3.5 w-3.5 shrink-0" />
                        {t(NS, 'preview_errors_title', 'Aperçu impossible à calculer :')}
                    </p>
                    <ul className="mt-1 list-disc pl-4">
                        {(previewErrorList.length > 0 ? previewErrorList : [previewMessage as string]).map((m, i) => (
                            <li key={i}>{m}</li>
                        ))}
                    </ul>
                    <p className="mt-1">{t(NS, 'preview_stale_hint', 'La grille affichée reste le dernier calcul valide.')}</p>
                </div>
            )}

            {group && (
                <div className="flex items-center gap-2 rounded-lg border border-violet-200 bg-violet-50 px-3 py-2 text-xs text-violet-800 dark:border-violet-900/40 dark:bg-violet-900/20 dark:text-violet-200">
                    <Link2 className="h-4 w-4 shrink-0" />
                    <span>
                        {t(NS, 'linked_banner', 'Lié au groupe « :name ». Les sections suivies se mettent à jour avec le groupe.').replace(':name', group.name)}
                    </span>
                </div>
            )}

            {!hideSaveButton && (
                <div className="flex items-center justify-end gap-2">
                    {previewLoading && <span className="text-muted-foreground animate-pulse text-xs">{t(NS, 'computing', 'Calcul...')}</span>}
                    <Button onClick={handleSave} disabled={processing || !canManage} title={canManage ? undefined : readOnlyReason}>
                        <Save className="mr-2 h-4 w-4" />
                        {t(NS, 'save', 'Sauvegarder')}
                    </Button>
                </div>
            )}

            <Tabs defaultValue="pricing" className="w-full">
                {/* Cinq onglets : sur une ligne quand ils tiennent, sinon ils passent à
                    la ligne (flex-wrap) — une grille à cinq colonnes égales faisait
                    chevaucher « Last-minute » et ses voisins sous ~670 px (téléphone,
                    colonne de la page yield/edit à côté du menu). */}
                <TabsList className="flex h-auto w-full flex-wrap">
                    <TabsTrigger value="pricing" className="flex h-9 items-center gap-1">
                        <Settings className="h-3.5 w-3.5" />
                        {t(NS, 'tab_pricing', 'Prix')}
                    </TabsTrigger>
                    {/* Ancre de l'étape « Saisons » de la visite de yield/edit
                        (yield-edit.tour.ts), posée sur le DÉCLENCHEUR : le contenu
                        d'un onglet inactif n'est pas monté, les étapes sont
                        résolues au lancement, et une ancre dans l'onglet Saisons
                        ne serait donc jamais trouvée (TOURS.md §6.4). Sans
                        openOnEnter : l'étape dit d'ouvrir l'onglet, elle ne
                        prétend pas l'ouvrir. */}
                    <TabsTrigger value="seasonality" className="flex h-9 items-center gap-1" data-tour="yield-edit-seasons">
                        <Calendar className="h-3.5 w-3.5" />
                        {t(NS, 'tab_seasons', 'Saisons')}
                    </TabsTrigger>
                    {/* Ancre de l'étape « Règle active » : l'interrupteur existe
                        dans chacun des quatre onglets de règle ; l'onglet Saisons
                        porte désormais l'étape des saisons (une ancre par étape). */}
                    <TabsTrigger value="lastminute" className="flex h-9 items-center gap-1" data-tour="yield-editor-section-switch">
                        <Clock className="h-3.5 w-3.5" />
                        {t(NS, 'tab_lastminute', 'Last-minute')}
                    </TabsTrigger>
                    {/* Ancre de l'étape OBA de la visite de yield/edit : le DÉCLENCHEUR,
                        monté au lancement (TOURS.md §6.4), comme le week-end. */}
                    <TabsTrigger value="oba" className="flex h-9 items-center gap-1" data-tour="yield-edit-oba">
                        <Percent className="h-3.5 w-3.5" />
                        {t(NS, 'tab_oba', 'OBA')}
                    </TabsTrigger>
                    {/* Ancre de la visite de la page yield/edit : le DÉCLENCHEUR,
                        monté au lancement, et non le contenu de l'onglet, qui ne
                        l'est pas (TOURS.md §6.4). */}
                    <TabsTrigger value="weekend" className="flex h-9 items-center gap-1" data-tour="yield-edit-weekend">
                        <CalendarDays className="h-3.5 w-3.5" />
                        {t(NS, 'tab_weekend', 'Week-end')}
                    </TabsTrigger>
                </TabsList>

                {/* Pricing — never part of a group, always editable. */}
                <TabsContent value="pricing">
                    <Card>
                        <CardHeader>
                            <CardTitle>{t(NS, 'pricing_title', 'Prix de base')}</CardTitle>
                        </CardHeader>
                        <CardContent>
                            <PriceSliders
                                minPrice={data.min_price}
                                basePrice={data.base_price}
                                maxPrice={data.max_price}
                                exchangeRate={exchangeRate}
                                onChange={handlePriceChange}
                                disabled={!canManage}
                                disabledReason={readOnlyReason}
                                errors={priceBandErrors(previewErrors)}
                            />
                        </CardContent>
                    </Card>
                </TabsContent>

                {/* Seasonality */}
                <TabsContent value="seasonality">
                    <Card>
                        <CardHeader>
                            <CardTitle className="flex flex-wrap items-center justify-between gap-2">
                                <span>
                                    {t(NS, 'seasons_title', 'Saisons')}
                                    <HelpHint text={t(NS, 'seasons_help', 'Ajustez le prix sur des plages de dates précises. Exemple base 100 : été +40 % (×1,40) = 140 ; basse saison −15 % (×0,85) = 85. Les dates non couvertes utilisent le % de base.')} />
                                </span>
                                <div className="flex items-center gap-3">
                                    <SectionSwitch id={`${switchId}-seasonality`} checked={isOn('seasonality')} disabled={sectionLocked('seasonality')} lockReason={switchLockReason('seasonality')} onChange={(on) => toggleSection('seasonality', on)} />
                                    <SectionLink locked={followsGroup('seasonality')} canFollow={!!group && group.sections.includes('seasonality')} canManage={canManage} readOnlyReason={readOnlyReason} onDetach={() => detach('seasonality')} onReattach={() => reattach('seasonality')} />
                                </div>
                            </CardTitle>
                        </CardHeader>
                        <CardContent>
                            {isOn('seasonality') ? (
                                <fieldset disabled={sectionLocked('seasonality')} title={canManage ? undefined : readOnlyReason} className={sectionLocked('seasonality') ? 'opacity-70' : ''}>
                                    <SeasonsEditor value={data.seasonality} onChange={(s) => setData('seasonality', s)} seasonTemplates={seasonTemplates} holidays={movableHolidays} />
                                </fieldset>
                            ) : (
                                <SectionDisabledNotice />
                            )}
                        </CardContent>
                    </Card>
                </TabsContent>

                {/* Last-minute / Far-out — full width for clarity */}
                <TabsContent value="lastminute">
                    <Card>
                        <CardHeader>
                            <CardTitle className="flex flex-wrap items-center justify-between gap-2 text-base">
                                <span>
                                    {t(NS, 'lastminute_card', 'Last-minute & far-out')}
                                    <HelpHint text={t(NS, 'lastminute_card_help', 'Rabais sur les nuits proches encore libres, prime sur les réservations très anticipées. Une nuit déjà réservée n’est jamais bradée.')} />
                                </span>
                                <div className="flex items-center gap-3">
                                    <SectionSwitch id={`${switchId}-lastminute`} checked={isOn('lastminute')} disabled={sectionLocked('lastminute')} lockReason={switchLockReason('lastminute')} onChange={(on) => toggleSection('lastminute', on)} />
                                    <SectionLink locked={followsGroup('lastminute')} canFollow={!!group && group.sections.includes('last_minute_rules')} canManage={canManage} readOnlyReason={readOnlyReason} onDetach={() => detach('lastminute')} onReattach={() => reattach('lastminute')} />
                                </div>
                            </CardTitle>
                        </CardHeader>
                        <CardContent>
                            {isOn('lastminute') ? (
                                <fieldset disabled={sectionLocked('lastminute')} title={canManage ? undefined : readOnlyReason} className={sectionLocked('lastminute') ? 'opacity-70' : ''}>
                                    <LastMinuteEditor
                                        lastMinute={data.last_minute_rules}
                                        farOut={data.far_out_premium}
                                        onChangeLastMinute={(r) => setData('last_minute_rules', r)}
                                        onChangeFarOut={(f) => setData('far_out_premium', f)}
                                    />
                                </fieldset>
                            ) : (
                                <SectionDisabledNotice />
                            )}
                        </CardContent>
                    </Card>
                </TabsContent>

                {/* OBA — full width for clarity */}
                <TabsContent value="oba">
                    <Card>
                        <CardHeader>
                            <CardTitle className="flex flex-wrap items-center justify-between gap-2 text-base">
                                <span>
                                    {t(NS, 'oba_card', 'OBA — ajustement par occupation')}
                                    <HelpHint text={t(NS, 'oba_card_help', 'Monte le prix quand la période se remplit, jamais l’inverse : « ≥ 80 % → ×1,05 » = +5 % sur les nuits encore libres dès que 80 % des nuits vendables de l’échéance sont vendues. Tant qu’aucun autre palier n’est atteint, la ligne « Dès 0 % » s’applique (×1 dans les presets). Les baisses viennent de la règle dernière minute. Charge un preset puis personnalise chaque seuil.')} />
                                </span>
                                <div className="flex items-center gap-3">
                                    <SectionSwitch id={`${switchId}-oba`} checked={isOn('oba')} disabled={sectionLocked('oba')} lockReason={switchLockReason('oba')} onChange={(on) => toggleSection('oba', on)} />
                                    <SectionLink locked={followsGroup('oba')} canFollow={!!group && group.sections.includes('oba_rules')} canManage={canManage} readOnlyReason={readOnlyReason} onDetach={() => detach('oba')} onReattach={() => reattach('oba')} />
                                </div>
                            </CardTitle>
                        </CardHeader>
                        <CardContent>
                            {isOn('oba') ? (
                                <fieldset disabled={sectionLocked('oba')} title={canManage ? undefined : readOnlyReason} className={sectionLocked('oba') ? 'opacity-70' : ''}>
                                    <ObaEditor value={data.oba_rules} onChange={(r) => setData('oba_rules', r)} presets={obaPresets} />
                                </fieldset>
                            ) : (
                                <SectionDisabledNotice />
                            )}
                        </CardContent>
                    </Card>
                </TabsContent>

                {/* Week-end — un multiplicateur sur les nuits choisies. Coupé =
                    ×1 (la forme neutre du serveur), le défaut de tout logement
                    qui ne l'a jamais réglé. */}
                <TabsContent value="weekend">
                    <Card>
                        <CardHeader>
                            <CardTitle className="flex flex-wrap items-center justify-between gap-2 text-base">
                                <span>
                                    {t(NS, 'weekend_card', 'Week-end')}
                                    <HelpHint text={t(NS, 'weekend_card_help', 'Un multiplicateur appliqué aux nuits que vous choisissez — par défaut celles du vendredi et du samedi, qui se vendent le mieux. Il s’applique aussi quand la nuit est déjà réservée, mais une nuit réservée n’est jamais renvoyée.')} />
                                </span>
                                <div className="flex items-center gap-3">
                                    <SectionSwitch id={`${switchId}-weekend`} checked={isOn('weekend')} disabled={sectionLocked('weekend')} lockReason={switchLockReason('weekend')} onChange={(on) => toggleSection('weekend', on)} />
                                    <SectionLink locked={followsGroup('weekend')} canFollow={!!group && group.sections.includes('weekend_rule')} canManage={canManage} readOnlyReason={readOnlyReason} onDetach={() => detach('weekend')} onReattach={() => reattach('weekend')} />
                                </div>
                            </CardTitle>
                        </CardHeader>
                        <CardContent>
                            {isOn('weekend') ? (
                                <fieldset disabled={sectionLocked('weekend')} title={canManage ? undefined : readOnlyReason} className={sectionLocked('weekend') ? 'opacity-70' : ''}>
                                    <WeekendEditor value={data.weekend_rule} onChange={(r) => setData('weekend_rule', r)} />
                                </fieldset>
                            ) : (
                                <SectionDisabledNotice />
                            )}
                        </CardContent>
                    </Card>
                </TabsContent>

            </Tabs>
        </div>
    );
}

/**
 * Interrupteur « Règle active » d'un onglet. Le `title` est posé sur
 * l'enveloppe : un bouton désactivé n'émet pas de survol.
 */
function SectionSwitch({
    id,
    checked,
    disabled,
    lockReason,
    onChange,
}: {
    id: string;
    checked: boolean;
    disabled: boolean;
    lockReason?: string;
    onChange: (on: boolean) => void;
}) {
    const { t } = useTranslation();
    return (
        <span className="flex items-center gap-1.5 text-xs font-normal" title={disabled ? lockReason : undefined}>
            <Switch id={id} checked={checked} disabled={disabled} onCheckedChange={onChange} />
            <label htmlFor={id} className={disabled ? 'text-muted-foreground' : 'cursor-pointer'}>
                {t(NS, 'section_active', 'Règle active')}
            </label>
        </span>
    );
}

/** Ce qu'affiche un onglet coupé : la règle existe, mais ne touche à aucun prix. */
function SectionDisabledNotice() {
    const { t } = useTranslation();
    return (
        <p className="text-muted-foreground rounded-md border border-dashed px-3 py-4 text-sm">
            {t(NS, 'section_disabled_hint', 'Règle désactivée : elle ne modifie aucun prix.')}
        </p>
    );
}

/**
 * Link control for a rule section: when the section follows the group it shows a
 * "personnaliser" (detach) button; once detached it shows "revenir au groupe".
 * Hidden entirely when the group does not carry the section.
 */
function SectionLink({
    locked,
    canFollow,
    canManage,
    readOnlyReason,
    onDetach,
    onReattach,
}: {
    locked: boolean;
    canFollow: boolean;
    /** Sans `yield.manage`, détacher / rattacher est une écriture : éteint + expliqué. */
    canManage: boolean;
    readOnlyReason: string;
    onDetach: () => void;
    onReattach: () => void;
}) {
    const { t } = useTranslation();
    if (!canFollow) return null;
    const title = canManage ? undefined : readOnlyReason;
    return locked ? (
        <Button type="button" variant="outline" size="sm" className="h-7 gap-1 text-xs" onClick={onDetach} disabled={!canManage} title={title}>
            <Lock className="h-3 w-3" />
            {t(NS, 'detach_section', 'Personnaliser')}
        </Button>
    ) : (
        <Button type="button" variant="ghost" size="sm" className="h-7 gap-1 text-xs text-violet-700" onClick={onReattach} disabled={!canManage} title={title}>
            <Link2 className="h-3 w-3" />
            {t(NS, 'reattach_section', 'Revenir au groupe')}
        </Button>
    );
}
