import DOMPurify from 'dompurify';
import { Building2, ExternalLink, Loader2, TriangleAlert, Users } from 'lucide-react';
import { Component, useState, type ReactNode } from 'react';
import {
    Bar,
    BarChart,
    CartesianGrid,
    Cell,
    Legend,
    Line,
    LineChart,
    Pie,
    PieChart,
    ResponsiveContainer,
    Tooltip,
    XAxis,
    YAxis,
} from 'recharts';

import { apiFetch } from '@/lib/loading/api-fetch';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';

import { ReservationsTable } from './reservations-table';
import type {
    ActionBlock,
    ChartBlock,
    ChatbotBlock,
    ClarificationBlock,
    ComparisonBlock,
    DashboardBlock,
    MetricsBlock,
    ProfileBlock,
    PropertiesBlock,
    StatBlock,
    TableBlock,
    TextBlock,
} from './types';

/** Callback a block calls to send a follow-up question (drill-down). */
export type DrillFn = (question: string) => void;

// The Landlord palette (deep teal → mint) — saturated, distinct shades so no
// slice/bar washes out against a white card.
const PALETTE = ['#1b4e4d', '#31b08f', '#2c8f76', '#5ec6a8', '#0f3433', '#26786a', '#7bd0b6', '#154a44'];

// Fixed brand colours for known reservation channels, so a slice/bar reads the
// same on every chart: Direct = TLL green, Airbnb red, Booking blue, Expedia
// gold, and distinct hues for the rest. Keyed by the *friendly* channel label
// produced server-side (ChatbotToolRunner::friendlyChannel), matched
// case-insensitively; any unknown label falls back to the palette by position.
const CHANNEL_COLORS: Record<string, string> = {
    direct: '#1b9c5d', // The Landlord (vente directe)
    airbnb: '#ff5a5f',
    'booking.com': '#003b95',
    expedia: '#f2a900',
    agoda: '#7048e8',
    travelstaytion: '#f06595',
    hometogo: '#e8590c',
    'trip.com': '#1098ad',
    ctrip: '#1098ad',
    google: '#5f6d7e',
};

/**
 * Colour for a chart item: a channel's brand colour when the label is a known
 * channel (so "Mix par canal" is always Direct-green / Airbnb-red / …),
 * otherwise a stable palette shade by position (leaves non-channel charts such
 * as "Top 5 logements" untouched).
 */
const colorFor = (label: string, index: number): string =>
    CHANNEL_COLORS[label.trim().toLowerCase()] ?? PALETTE[index % PALETTE.length];

const numberFmt = new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 2 });
// Compact axis ticks: 27435 → "27,4 k", 613742 → "614 k".
const compactFmt = new Intl.NumberFormat('fr-FR', { notation: 'compact', maximumFractionDigits: 1 });

function formatCell(value: unknown): string {
    if (typeof value === 'number') {
        return numberFmt.format(value);
    }
    return value === null || value === undefined ? '' : String(value);
}

/** Shorten long category labels on an axis; the full name still shows in the tooltip. */
const truncateLabel = (value: unknown, max = 12): string => {
    const s = String(value ?? '');
    return s.length > max ? `${s.slice(0, max - 1)}…` : s;
};

type ChartTooltipProps = {
    active?: boolean;
    payload?: Array<{ value?: number; name?: string; payload?: { label?: string } }>;
    label?: string | number;
    valueLabel: string;
    total?: number;
};

/**
 * Readable tooltip shared by bar/line/pie: the category (full, un-truncated) plus
 * the friendly metric label and a formatted value — never a raw SQL column name.
 */
function ChartTooltip({ active, payload, label, valueLabel, total }: ChartTooltipProps) {
    if (!active || !payload?.length) {
        return null;
    }
    const point = payload[0];
    const value = typeof point?.value === 'number' ? point.value : 0;
    // Bar/line carry the x-axis `label`; a pie slice carries its name instead.
    const heading = label !== undefined && label !== '' ? String(label) : (point?.name ?? point?.payload?.label ?? '');
    const pct = total && total > 0 ? ` (${Math.round((value / total) * 100)} %)` : '';

    return (
        <div className="rounded-lg border border-border bg-background px-3 py-2 text-xs shadow-md">
            <div className="max-w-[220px] font-medium break-words text-foreground">{heading}</div>
            <div className="mt-0.5 text-muted-foreground">
                {valueLabel} : <span className="font-semibold text-foreground">{numberFmt.format(value)}</span>
                {pct}
            </div>
        </div>
    );
}

function StatCard({ block, onDrill }: { block: StatBlock; onDrill?: DrillFn }) {
    const clickable = !!(block.drill && onDrill);
    return (
        <button
            type="button"
            disabled={!clickable}
            onClick={() => block.drill && onDrill?.(block.drill)}
            className={cn(
                'w-full rounded-xl border border-border bg-gradient-to-br from-primary to-[#0f3433] p-4 text-left text-primary-foreground shadow-sm',
                clickable && 'cursor-pointer transition hover:brightness-110',
            )}
        >
            <div className="text-xs tracking-wide uppercase opacity-80">{block.label}</div>
            <div className="mt-1 text-2xl font-semibold">{formatCell(block.value)}</div>
        </button>
    );
}

function DataTable({ block, onDrill }: { block: TableBlock; onDrill?: DrillFn }) {
    return (
        <div className="overflow-hidden rounded-xl border border-border">
            {block.title && (
                <div className="bg-muted px-3 py-2 text-xs font-medium text-muted-foreground">{block.title}</div>
            )}
            <div className="max-h-96 overflow-y-auto">
                <table className="w-full border-collapse text-sm">
                    <thead className="bg-muted/80 sticky top-0 backdrop-blur">
                        <tr>
                            {block.columns.map((col) => (
                                <th key={col} className="px-3 py-2 text-left font-medium text-muted-foreground">
                                    {col}
                                </th>
                            ))}
                        </tr>
                    </thead>
                    <tbody>
                        {block.rows.map((row, i) => {
                            const drill = block.rowDrills?.[i];
                            const clickable = !!(drill && onDrill);
                            return (
                                <tr
                                    key={i}
                                    onClick={clickable ? () => onDrill?.(drill!) : undefined}
                                    className={cn(
                                        'border-t border-border/60 align-top hover:bg-accent/50',
                                        clickable && 'cursor-pointer',
                                    )}
                                >
                                    {block.columns.map((col) => (
                                        <td key={col} className="px-3 py-2">
                                            {formatCell(row[col])}
                                        </td>
                                    ))}
                                </tr>
                            );
                        })}
                    </tbody>
                </table>
            </div>
        </div>
    );
}

function ChartCard({ block, onDrill }: { block: ChartBlock; onDrill?: DrillFn }) {
    const { data, chartType } = block;
    const tall = chartType === 'pie';
    const height = tall ? 260 : Math.max(200, Math.min(data.length, 12) * 32);
    const rotated = chartType === 'bar' && data.length > 4;
    const valueLabel = block.valueLabel ?? 'Valeur';
    const total = data.reduce((sum, d) => sum + (typeof d.value === 'number' ? d.value : 0), 0);
    const hasDrill = !!onDrill && data.some((d) => d.drill);
    const drillAt = (index: number) => {
        const d = data[index];
        if (d?.drill) onDrill?.(d.drill);
    };

    return (
        <div className="rounded-xl border border-border p-3">
            {block.title && <div className="mb-2 text-xs font-medium text-muted-foreground">{block.title}</div>}
            <ResponsiveContainer width="100%" height={height}>
                {chartType === 'pie' ? (
                    <PieChart>
                        <Pie
                            data={data}
                            dataKey="value"
                            nameKey="label"
                            outerRadius="78%"
                            onClick={hasDrill ? (_, index) => drillAt(index) : undefined}
                            cursor={hasDrill ? 'pointer' : undefined}
                        >
                            {data.map((d, i) => (
                                <Cell key={i} fill={colorFor(d.label, i)} />
                            ))}
                        </Pie>
                        <Tooltip content={<ChartTooltip valueLabel={valueLabel} total={total} />} />
                        <Legend wrapperStyle={{ fontSize: 11 }} />
                    </PieChart>
                ) : chartType === 'line' ? (
                    <LineChart data={data} margin={{ top: 8, right: 16, bottom: 8, left: 0 }}>
                        <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
                        <XAxis dataKey="label" tick={{ fontSize: 11 }} tickFormatter={(v) => truncateLabel(v, 10)} minTickGap={24} />
                        <YAxis tick={{ fontSize: 11 }} width={44} tickFormatter={(v) => compactFmt.format(Number(v))} />
                        <Tooltip content={<ChartTooltip valueLabel={valueLabel} />} />
                        <Line type="monotone" dataKey="value" stroke="#1b4e4d" strokeWidth={2} dot={false} name={valueLabel} />
                    </LineChart>
                ) : (
                    <BarChart data={data} margin={{ top: 8, right: 16, bottom: 8, left: 0 }}>
                        <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
                        <XAxis
                            dataKey="label"
                            tick={{ fontSize: 11 }}
                            interval={0}
                            tickFormatter={(v) => truncateLabel(v, 10)}
                            angle={rotated ? -30 : 0}
                            textAnchor={rotated ? 'end' : 'middle'}
                            height={rotated ? 70 : 30}
                        />
                        <YAxis tick={{ fontSize: 11 }} width={44} tickFormatter={(v) => compactFmt.format(Number(v))} />
                        <Tooltip cursor={{ fill: 'rgba(27,78,77,0.06)' }} content={<ChartTooltip valueLabel={valueLabel} />} />
                        <Bar
                            dataKey="value"
                            radius={[4, 4, 0, 0]}
                            name={valueLabel}
                            onClick={hasDrill ? (_, index) => drillAt(index) : undefined}
                            cursor={hasDrill ? 'pointer' : undefined}
                        >
                            {data.map((d, i) => (
                                <Cell key={i} fill={colorFor(d.label, i)} />
                            ))}
                        </Bar>
                    </BarChart>
                )}
            </ResponsiveContainer>
            {hasDrill && <div className="mt-1 text-center text-[10px] text-muted-foreground">Astuce : cliquez un élément pour l'explorer</div>}
        </div>
    );
}

function PropertyCards({ block, onDrill }: { block: PropertiesBlock; onDrill?: DrillFn }) {
    if (block.items.length === 0) {
        return <div className="rounded-xl border border-border bg-muted/40 px-3 py-2 text-sm text-muted-foreground">Aucun logement trouvé.</div>;
    }
    return (
        <div className="space-y-2">
            {block.title && <div className="text-xs font-medium text-muted-foreground">{block.title}</div>}
            <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
                {block.items.map((p) => {
                    const clickable = !!(p.drill && onDrill);
                    const Wrapper = clickable ? 'button' : p.url ? 'a' : 'div';
                    const wrapperProps = clickable
                        ? { type: 'button' as const, onClick: () => p.drill && onDrill?.(p.drill) }
                        : p.url
                          ? { href: p.url, target: '_blank', rel: 'noopener noreferrer' as const }
                          : {};
                    return (
                        <Wrapper
                            key={p.id}
                            {...wrapperProps}
                            title={p.name}
                            className="group block overflow-hidden rounded-xl border border-border bg-card text-left shadow-sm transition hover:border-secondary hover:shadow-md"
                        >
                            <div className="relative h-28 w-full overflow-hidden bg-muted">
                                {p.image ? (
                                    <img
                                        src={p.image}
                                        alt={p.name}
                                        loading="lazy"
                                        className="h-full w-full object-cover transition group-hover:scale-105"
                                    />
                                ) : (
                                    <div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-primary/15 to-secondary/15">
                                        <Building2 className="size-8 text-primary/50" />
                                    </div>
                                )}
                                <span className="absolute top-2 right-2 rounded-md bg-primary/90 px-2 py-0.5 text-xs font-semibold text-primary-foreground">
                                    {formatCell(p.price)} {p.currency}
                                </span>
                            </div>
                            <div className="p-3">
                                <div className="truncate text-sm font-medium group-hover:text-secondary">{p.name}</div>
                                <div className="mt-1 flex items-center justify-between text-xs text-muted-foreground">
                                    <span className="truncate">{p.state ?? '—'}</span>
                                    <span className="inline-flex items-center gap-1">
                                        <Users className="size-3" />
                                        {p.capacity}
                                    </span>
                                </div>
                            </div>
                        </Wrapper>
                    );
                })}
            </div>
        </div>
    );
}

function MetricsCards({ block }: { block: MetricsBlock }) {
    return (
        <div className="space-y-2">
            {block.title && <div className="text-xs font-medium text-muted-foreground">{block.title}</div>}
            <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
                {block.items.map((m, i) => (
                    <div key={i} className="rounded-xl border border-border bg-card p-3">
                        <div className="text-xs text-muted-foreground">{m.label}</div>
                        <div className="mt-0.5 text-lg font-semibold text-primary">{m.value}</div>
                        {m.hint && <div className="mt-0.5 text-[10px] text-muted-foreground">{m.hint}</div>}
                    </div>
                ))}
            </div>
        </div>
    );
}

/**
 * Multi-entity comparison table (e.g. occupancy per property, this year vs last).
 * Always used instead of one metrics card per property: a single scrollable card
 * with an occupancy bar per row, a coloured Δ chip when a compare period exists,
 * and extra right-aligned columns (revenue / reservations). Rows are clickable
 * to drill into a single property.
 */
function ComparisonCard({ block, onDrill }: { block: ComparisonBlock; onDrill?: DrillFn }) {
    const unit = block.unit ? ` ${block.unit}` : '';
    const hasCompare = block.rows.some((r) => typeof r.previous === 'number');
    const extraCols = block.rows[0]?.columns?.map((c) => c.label) ?? [];
    const fmtMetric = (v: number) => `${numberFmt.format(v)}${unit}`;
    // A non-percentage metric (revenue) scales bars to the largest value; occupancy
    // has no barMax and stays absolute against 100.
    const barMax = block.barMax && block.barMax > 0 ? block.barMax : null;
    const barPct = (v: number) => (barMax ? (v / barMax) * 100 : v);
    // Δ is in "pts" for a percentage metric, otherwise in the metric's own unit.
    const deltaUnit = block.unit && block.unit !== '%' ? ` ${block.unit}` : ' pts';
    const sortLabel = block.sortLabel ?? 'occupation';

    const Bar = ({ value, strong }: { value: number; strong: boolean }) => (
        <div className="flex items-center gap-2">
            <div className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
                <div
                    className={cn('h-full rounded-full', strong ? 'bg-[#2c8f76]' : 'bg-muted-foreground/40')}
                    style={{ width: `${Math.max(0, Math.min(100, barPct(value)))}%` }}
                />
            </div>
            <span className={cn('min-w-[46px] text-right tabular-nums', strong ? 'font-medium' : 'text-muted-foreground')}>
                {fmtMetric(value)}
            </span>
        </div>
    );

    const Delta = ({ cur, prev }: { cur: number; prev: number }) => {
        const d = Math.round((cur - prev) * 10) / 10;
        if (d === 0) return <span className="text-muted-foreground">—</span>;
        const up = d > 0;
        return (
            <span
                className={cn(
                    'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium tabular-nums',
                    up ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-700',
                )}
            >
                {up ? '▲' : '▼'} {numberFmt.format(Math.abs(d))}{deltaUnit}
            </span>
        );
    };

    return (
        <div className="overflow-hidden rounded-xl border border-border">
            {(block.title || block.periodLabel) && (
                <div className="border-b border-border bg-muted/60 px-3 py-2">
                    {block.title && <div className="text-xs font-medium">{block.title}</div>}
                    {block.periodLabel && (
                        <div className="mt-0.5 text-[11px] text-muted-foreground">
                            {block.periodLabel}
                            {hasCompare && block.comparePeriodLabel ? ` vs ${block.comparePeriodLabel}` : ''}
                            {` · ${block.rows.length} logements · trié par ${sortLabel}`}
                        </div>
                    )}
                </div>
            )}
            <div className="max-h-80 overflow-y-auto">
                <table className="w-full border-collapse text-sm">
                    <thead className="bg-muted/80 sticky top-0 backdrop-blur">
                        <tr>
                            <th className="px-3 py-2 text-left font-medium text-muted-foreground">Logement</th>
                            <th className="px-2 py-2 text-left font-medium text-muted-foreground">
                                {hasCompare ? 'Cette année' : (block.metricLabel ?? 'Occupation')}
                            </th>
                            {hasCompare && <th className="px-2 py-2 text-left font-medium text-muted-foreground">L'an dernier</th>}
                            {hasCompare && <th className="px-2 py-2 text-right font-medium text-muted-foreground">Δ</th>}
                            {extraCols.map((c) => (
                                <th key={c} className="px-3 py-2 text-right font-medium text-muted-foreground">
                                    {c}
                                </th>
                            ))}
                        </tr>
                    </thead>
                    <tbody>
                        {block.rows.map((r, i) => {
                            const clickable = !!(r.drill && onDrill);
                            return (
                                <tr
                                    key={i}
                                    onClick={clickable ? () => onDrill?.(r.drill!) : undefined}
                                    className={cn('border-t border-border/60 hover:bg-accent/50', clickable && 'cursor-pointer')}
                                >
                                    <td className="max-w-[220px] px-3 py-2">
                                        <div className="truncate font-medium" title={r.label}>
                                            {r.label}
                                        </div>
                                        {r.sublabel && <div className="text-[11px] text-muted-foreground">{r.sublabel}</div>}
                                    </td>
                                    <td className="w-40 px-2 py-2">
                                        <Bar value={r.current} strong />
                                    </td>
                                    {hasCompare && (
                                        <td className="w-32 px-2 py-2">
                                            {typeof r.previous === 'number' ? <Bar value={r.previous} strong={false} /> : <span className="text-muted-foreground">—</span>}
                                        </td>
                                    )}
                                    {hasCompare && (
                                        <td className="px-2 py-2 text-right">
                                            {typeof r.previous === 'number' ? <Delta cur={r.current} prev={r.previous} /> : null}
                                        </td>
                                    )}
                                    {(r.columns ?? []).map((c) => (
                                        <td key={c.label} className="px-3 py-2 text-right tabular-nums text-muted-foreground">
                                            {formatCell(c.value)}
                                        </td>
                                    ))}
                                </tr>
                            );
                        })}
                    </tbody>
                </table>
            </div>
            {block.rows.some((r) => r.drill) && onDrill && (
                <div className="border-t border-border/60 py-1 text-center text-[10px] text-muted-foreground">
                    Astuce : cliquez une ligne pour l'explorer
                </div>
            )}
        </div>
    );
}

/**
 * Clarification request: the question as plain text plus suggested replies as
 * chips. A chip click sends its text as the next user message through the same
 * `onDrill` path as the follow-up chips / drills (so the loading guard in
 * `send` applies). Options are deduped and capped at 6 (contract).
 */
function ClarificationCard({ block, onDrill }: { block: ClarificationBlock; onDrill?: DrillFn }) {
    const options = Array.from(new Set(block.options.filter((o) => o.trim() !== ''))).slice(0, 6);
    return (
        <div className="space-y-2 rounded-xl border border-secondary/40 bg-secondary/5 p-3">
            <div className="text-sm leading-relaxed">{block.question}</div>
            {options.length > 0 && onDrill && (
                <div className="flex flex-wrap gap-2">
                    {options.map((o) => (
                        <button
                            key={o}
                            type="button"
                            onClick={() => onDrill(o)}
                            className="rounded-full border border-border bg-card px-2.5 py-1 text-xs text-muted-foreground transition hover:border-secondary hover:text-foreground"
                        >
                            {o}
                        </button>
                    ))}
                </div>
            )}
        </div>
    );
}

function HtmlText({ block }: { block: TextBlock }) {
    return (
        <div
            className="text-sm leading-relaxed"
            dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(block.html) }}
        />
    );
}

function ProfileCard({ block, onDrill }: { block: ProfileBlock; onDrill?: DrillFn }) {
    return (
        <div className="overflow-hidden rounded-2xl border border-border bg-card">
            {block.image && (
                <div className="h-36 w-full overflow-hidden bg-muted">
                    <img src={block.image} alt={block.name} loading="lazy" className="h-full w-full object-cover" />
                </div>
            )}
            <div className="space-y-4 p-4">
                <div>
                    <div className="text-lg font-semibold">{block.name}</div>
                    {block.subtitle && <div className="text-xs text-muted-foreground">{block.subtitle}</div>}
                </div>
                {block.sections.map((sec, i) => (
                    <div key={i}>
                        <div className="text-xs font-medium tracking-wide uppercase text-muted-foreground">{sec.title}</div>
                        <div className="mt-1.5 grid grid-cols-2 gap-x-4 gap-y-1">
                            {sec.items.map((it, j) => (
                                <div key={j} className="flex items-baseline justify-between gap-2 text-sm">
                                    <span className="text-muted-foreground">{it.label}</span>
                                    <span className="text-right font-medium">{it.value}</span>
                                </div>
                            ))}
                        </div>
                    </div>
                ))}
                {block.blocks && block.blocks.length > 0 && (
                    <div className="space-y-3">
                        {block.blocks.map((b, i) => (
                            <BlockRenderer key={i} block={b} onDrill={onDrill} />
                        ))}
                    </div>
                )}
                {block.links && block.links.length > 0 && (
                    <div className="flex flex-wrap gap-3 border-t border-border pt-3">
                        {block.links.map((l) => (
                            <a
                                key={l.url}
                                href={l.url}
                                target="_blank"
                                rel="noopener noreferrer"
                                className="text-xs font-medium text-secondary underline underline-offset-2 hover:opacity-80"
                            >
                                {l.label}
                            </a>
                        ))}
                    </div>
                )}
            </div>
        </div>
    );
}

const csrfToken = (): string =>
    document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? '';

function ActionConfirmation({ block }: { block: ActionBlock }) {
    const [state, setState] = useState<'idle' | 'loading' | 'done' | 'error' | 'cancelled'>('idle');
    const [result, setResult] = useState<{ form_url?: string; message?: string; error?: string }>({});

    const call = async (path: string): Promise<{ ok: boolean; data: Record<string, unknown> }> => {
        const r = await apiFetch(path, {
            method: 'POST',
            headers: { Accept: 'application/json', 'X-CSRF-TOKEN': csrfToken(), 'X-Requested-With': 'XMLHttpRequest' },
        });
        const data = (await r.json().catch(() => ({}))) as Record<string, unknown>;
        return { ok: r.ok, data };
    };

    const confirm = async () => {
        setState('loading');
        try {
            const { ok, data } = await call(`/chatbot/actions/${block.actionId}/confirm`);
            if (!ok || data.ok === false) {
                setResult({ error: (data.error as string) ?? 'Échec de l’action.' });
                setState('error');
                return;
            }
            setResult({ form_url: data.form_url as string, message: data.message as string });
            setState('done');
        } catch {
            setResult({ error: 'Erreur réseau.' });
            setState('error');
        }
    };

    const cancel = async () => {
        setState('loading');
        try {
            await call(`/chatbot/actions/${block.actionId}/cancel`);
        } catch {
            /* ignore */
        }
        setState('cancelled');
    };

    if (state === 'done') {
        return (
            <div className="rounded-xl border border-emerald-500/30 bg-emerald-50/50 p-4 text-sm">
                <div className="font-medium text-emerald-700">✓ {result.message ?? 'Action effectuée'}</div>
                {result.form_url && (
                    <a
                        href={result.form_url}
                        target="_blank"
                        rel="noopener noreferrer"
                        className="mt-2 inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground"
                    >
                        <ExternalLink className="size-3.5" /> Ouvrir le lien de paiement
                    </a>
                )}
            </div>
        );
    }

    if (state === 'cancelled') {
        return <div className="rounded-xl border border-border bg-muted/40 p-3 text-sm text-muted-foreground">Action annulée.</div>;
    }

    return (
        <div className={cn('rounded-xl border p-4', block.destructive ? 'border-destructive/40 bg-destructive/5' : 'border-primary/30 bg-primary/5')}>
            <div className="text-sm font-medium">{block.title}</div>
            <div className="mt-1 text-sm text-muted-foreground">{block.message}</div>
            {state === 'error' && <div className="mt-2 text-xs text-destructive">{result.error}</div>}
            <div className="mt-3 flex gap-2">
                <Button
                    size="sm"
                    onClick={confirm}
                    disabled={state === 'loading'}
                    className={cn(block.destructive && 'bg-destructive hover:bg-destructive/90')}
                >
                    {state === 'loading' ? <Loader2 className="size-4 animate-spin" /> : block.actionLabel}
                </Button>
                <Button size="sm" variant="ghost" onClick={cancel} disabled={state === 'loading'}>
                    Annuler
                </Button>
            </div>
        </div>
    );
}

function DashboardRenderer({ block, onDrill }: { block: DashboardBlock; onDrill?: DrillFn }) {
    const grid = block.layout === 'grid-2' || block.layout === 'grid-3';
    const cols = block.layout === 'grid-3' ? 'lg:grid-cols-3' : 'lg:grid-cols-2';
    const span = block.layout === 'grid-3' ? 'lg:col-span-3' : 'lg:col-span-2';

    return (
        <div className="space-y-3 rounded-2xl border border-border bg-muted/20 p-3">
            {block.title && <div className="text-sm font-semibold">{block.title}</div>}
            {grid ? (
                <div className={cn('grid grid-cols-1 gap-3', cols)}>
                    {block.blocks.map((b, i) => (
                        // Charts sit 2-up; everything else spans full width.
                        <div key={i} className={cn(b.type !== 'chart' && span)}>
                            <BlockRenderer block={b} onDrill={onDrill} />
                        </div>
                    ))}
                </div>
            ) : (
                <div className="space-y-3">
                    {block.blocks.map((b, i) => (
                        <BlockRenderer key={i} block={b} onDrill={onDrill} />
                    ))}
                </div>
            )}
        </div>
    );
}

/**
 * Isolates a single block's render. A block that throws (e.g. a charting library
 * hiccup) must degrade to an inline notice — never unmount the whole app. Because
 * the widget is mounted on every page and replays the persisted conversation, an
 * unguarded throw here would blank out unrelated pages too.
 */
class BlockBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
    state = { failed: false };

    static getDerivedStateFromError() {
        return { failed: true };
    }

    componentDidCatch(error: unknown) {
        console.error('Chatbot: échec du rendu d’un bloc', error);
    }

    render() {
        if (this.state.failed) {
            return (
                <div className="flex items-center gap-2 rounded-xl border border-amber-500/30 bg-amber-50/50 px-3 py-2 text-xs text-amber-700">
                    <TriangleAlert className="size-4 shrink-0" />
                    Impossible d’afficher cet élément.
                </div>
            );
        }
        return this.props.children;
    }
}

export function BlockRenderer(props: { block: ChatbotBlock; onDrill?: DrillFn }) {
    return (
        <BlockBoundary>
            <BlockSwitch {...props} />
        </BlockBoundary>
    );
}

function BlockSwitch({ block, onDrill }: { block: ChatbotBlock; onDrill?: DrillFn }) {
    switch (block.type) {
        case 'stat':
            return <StatCard block={block} onDrill={onDrill} />;
        case 'table':
            return <DataTable block={block} onDrill={onDrill} />;
        case 'chart':
            return <ChartCard block={block} onDrill={onDrill} />;
        case 'properties':
            return <PropertyCards block={block} onDrill={onDrill} />;
        case 'reservations':
            return <ReservationsTable block={block} onDrill={onDrill} />;
        case 'metrics':
            return <MetricsCards block={block} />;
        case 'comparison':
            return <ComparisonCard block={block} onDrill={onDrill} />;
        case 'profile':
            return <ProfileCard block={block} onDrill={onDrill} />;
        case 'dashboard':
            return <DashboardRenderer block={block} onDrill={onDrill} />;
        case 'action':
            return <ActionConfirmation block={block} />;
        case 'clarification':
            return <ClarificationCard block={block} onDrill={onDrill} />;
        case 'text':
            return <HtmlText block={block} />;
        default:
            return null;
    }
}
