import { Star, CheckCircle2, Clapperboard, FileSignature, FileText, Globe, MapPin, MessageSquareQuote, Phone, Play, Trophy, Users, Wallet } from 'lucide-react';
import type { CSSProperties, ReactNode } from 'react';
import { embedUrl, videoThumb } from '@/components/quote/BlockEditors';
import type { Line } from '@/components/quote/LinesEditor';
import { badge, type Palette } from '@/components/proposal/palette';
import {
    AcceptButton, type AcceptAction, Amount, Avatar, Chip, ContactItem, Eyebrow, Heading, IconTile, initials, mailHref, MiniStat, NumberTile, pad2, Prose, Row, SHADOW, Step, Tag, telHref, webHref, type Align,
} from '@/components/proposal/elements';
import { date, dateTime, money } from '@/lib/format';
import type { TotalsResult } from '@/lib/totals';
import { cn } from '@/lib/utils';

export type Block = { id: string; keepWithNext?: boolean; section?: number; title?: string; render: (live: boolean) => ReactNode };
export type Layout = 'single' | 'three' | 'right';

type Ctx = {
    lines: Line[]; totals: TotalsResult; p: Palette; hf: CSSProperties; layout: Layout; align: Align;
    quote: { title_label: string; currency: string; tax_mode: string; show_codes: boolean; deposit_bp: number; expiry_date: string | null };
    company: { name: string; phone: string | null; website: string | null; address: string; terms_html: string | null; reviews?: { rating: number; comment: string | null; name: string | null; role: string | null }[] };
    owner: { name: string; email: string; phone: string | null; job_title: string | null } | null;
    showMoney: boolean; showTotals: boolean; showLinePrices: boolean; showQty: boolean;
    heroKey: string | null; toggle: (key: string, group: string | null, value: boolean) => void;
    /** Client page: the live Accept button, editable quantities, and a lock once decided. */
    accept?: AcceptAction; setQty?: (key: string, qty: string) => void; locked?: boolean;
    signed?: Signed | null;
};
export type Signed = { name: string | null; date: string; signature_type: string | null; signature: string | null; on_behalf: boolean; at?: string; when?: string; email?: string | null; recorded_by?: string | null };

/** Splits rich text into its top-level paragraphs / lists so long sections can flow across pages. */
export function splitHtml(html: string | null | undefined): string[] {
    if (!html || !html.replace(/<[^>]*>/g, '').trim()) return [];
    const doc = new DOMParser().parseFromString(`<div>${html}</div>`, 'text/html');
    const out: string[] = [];
    doc.body.firstElementChild!.childNodes.forEach((n) => {
        // Loose text is re-wrapped with the DOM (never string-concatenated: textContent is decoded, so "&lt;img…&gt;" would become a tag).
        if (n.nodeType === Node.TEXT_NODE) { if (n.textContent?.trim()) { const para = doc.createElement('p'); para.textContent = n.textContent; out.push(para.outerHTML); } return; }
        if (n instanceof Element) out.push(n.outerHTML);
    });
    return out;
}

const SPACE = { none: '0px', sm: '8px', md: '20px', lg: '40px' } as const;

/** Applies a block's style overrides (spacing, alignment, background, colour). */
function Styled({ l, p, children, fallbackAlign }: { l: Line; p: Palette; children: ReactNode; fallbackAlign?: Align }) {
    const s = l.style;
    if (!s && !fallbackAlign) return <>{children}</>;
    const pad = s?.spacing ? SPACE[s.spacing] : undefined;
    const align = s?.align ?? fallbackAlign;
    return (
        <div style={{ paddingTop: pad, paddingBottom: pad, textAlign: align }}>
            {s?.background
                ? <div className="rounded-[18px] px-6 py-5" style={{ background: s.background, color: s.color ?? undefined, boxShadow: SHADOW }}>{children}</div>
                : <div style={{ color: s?.color ?? undefined }}>{children}</div>}
        </div>
    );
}

const periodOf = (l: Line) => l.subscription_interval
    ? `${l.subscription_every && l.subscription_every > 1 ? `every ${l.subscription_every} ${l.subscription_interval}s` : `per ${l.subscription_interval}`}${l.subscription_term ? ` for ${l.subscription_term}` : ''}`
    : null;

export function buildBlocks(c: Ctx): Block[] {
    const { lines, totals, p, hf, quote, company, owner, layout, align } = c;
    const cur = quote.currency;
    const out: Block[] = [];
    let section = 0;
    let item = 0;
    const oneOff = totals.groups.find((g) => g.key === 'one_off');
    const groups = [oneOff, ...totals.groups.filter((g) => g.key !== 'one_off')].filter((g): g is NonNullable<typeof g> => !!g);
    const taxWord = quote.tax_mode === 'inclusive' ? 'incl. GST' : quote.tax_mode === 'none' ? '' : quote.tax_mode === 'exclusive' ? '+ GST' : 'incl. GST';
    const deposit = quote.deposit_bp > 0 ? Math.round(totals.total_cents * quote.deposit_bp / 10000) : null;
    const label = quote.title_label;
    const hasTermsBlock = lines.some((l) => l.type === 'terms');
    const blockAlign = (l: Line) => l.style?.align ?? align;
    let acceptLine: Line | null = null;

    const sectionHeading = (l: Line, text: string, size = 30, icon?: typeof FileText): Block => {
        const n = section++;
        return {
            id: `${l.key}-h`, keepWithNext: true, section: n, title: text,
            render: () => (
                <Styled l={{ ...l, style: l.style?.background ? null : l.style }} p={p}>
                    <div className="pb-4 pt-8" style={{ textAlign: blockAlign(l) }}>
                        <Eyebrow p={p} hf={hf} className="mb-2.5">{pad2(n + 1)}</Eyebrow>
                        <Heading p={p} hf={hf} text={text} size={size} icon={icon} align={blockAlign(l)} color={l.style?.color ?? undefined} />
                    </div>
                </Styled>
            ),
        };
    };
    const richChunks = (l: Line, html: string | null, cls = 'text-[13px] leading-[1.75]', color?: string) => {
        const chunks = splitHtml(html);
        chunks.forEach((h, ci) => out.push({
            id: `${l.key}-c${ci}`,
            render: () => <Styled l={{ ...l, style: l.style ? { ...l.style, background: null } : null }} p={p}><Prose p={p} html={h} align={blockAlign(l)} color={l.style?.color ?? color} className={cn(ci === chunks.length - 1 ? 'pb-4' : 'pb-2.5', cls)} /></Styled>,
        }));
        return chunks.length;
    };

    lines.forEach((l, i) => {
        const r = totals.lines[i];

        switch (l.type) {
            case 'cover': return; // shown on the cover page
            case 'text': {
                if (l.style?.background) {
                    // A coloured panel stays in one piece.
                    const n = l.heading?.trim() ? section++ : undefined;
                    out.push({
                        id: l.key, section: n, title: l.heading?.trim() || undefined,
                        render: () => (
                            <div className="pb-4 pt-5">
                                <Styled l={l} p={p} fallbackAlign={align}>
                                    {l.heading?.trim() && <div className="pb-3"><Heading p={p} hf={hf} text={l.heading.trim()} size={24} align={blockAlign(l)} color={l.style?.color ?? undefined} /></div>}
                                    {l.description && <Prose p={p} html={l.description} color={l.style?.color ?? undefined} align={blockAlign(l)} className="text-[13px] leading-[1.75]" />}
                                </Styled>
                            </div>
                        ),
                    });
                    return;
                }
                if (l.heading?.trim()) out.push(sectionHeading(l, l.heading.trim()));
                const n = richChunks(l, l.description);
                if (n === 0 && out.length && out[out.length - 1].id === `${l.key}-h`) out[out.length - 1].keepWithNext = false;
                return;
            }
            case 'image': {
                if (!l.image_url || l.key === c.heroKey) return;
                out.push({
                    id: l.key,
                    render: () => (
                        <Styled l={l} p={p}>
                            <figure className={cn('pb-5 pt-3', l.image_size === 'half' ? 'mx-auto w-1/2' : l.image_size === 'small' ? 'mx-auto w-1/4' : 'w-full')}>
                                <img src={l.image_url!} alt={l.image_alt ?? ''} className="h-auto max-h-[760px] w-full rounded-[14px] object-cover" />
                                {l.heading && <figcaption className="mt-2 text-center text-[11px]" style={{ color: p.muted }}>{l.heading}</figcaption>}
                            </figure>
                        </Styled>
                    ),
                });
                return;
            }
            case 'subtotal': {
                if (!c.showTotals) return;
                const amount = totals.subtotals.find((s) => s.index === i)?.amount_cents ?? 0;
                out.push({
                    id: l.key,
                    render: () => (
                        <Styled l={l} p={p}>
                            <div className="flex justify-end pb-5 pt-1">
                                <div className="flex items-center gap-5 rounded-full py-1.5 pl-5 pr-1.5" style={{ background: p.card }}>
                                    <span className="text-[12px] font-medium" style={{ color: p.muted }}>Subtotal</span>
                                    <span className="rounded-full px-4 py-1.5 text-[14px] font-semibold tabular-nums" style={{ background: p.bg, color: p.charcoal, ...hf }}>{money(amount, cur)}</span>
                                </div>
                            </div>
                        </Styled>
                    ),
                });
                return;
            }
            case 'video': {
                const src = embedUrl(l.content?.url);
                const thumb = videoThumb(l.content?.url);
                if (l.heading?.trim()) out.push(sectionHeading(l, l.heading.trim(), 22, Clapperboard));
                out.push({
                    id: l.key,
                    render: (live) => (
                        <Styled l={l} p={p}>
                            <div className="pb-5 pt-2">
                                {src && live && <div className="aspect-video w-full overflow-hidden rounded-[14px] bg-black print:hidden" style={{ boxShadow: SHADOW }}><iframe src={src} title={l.heading || 'Video'} className="h-full w-full" allow="encrypted-media; picture-in-picture" allowFullScreen loading="lazy" /></div>}
                                {/* Print / PDF (and the measuring copy): a thumbnail card with the link. */}
                                <a href={l.content?.url ?? undefined} className={cn('relative block aspect-video w-full overflow-hidden rounded-[14px]', src && live && 'hidden print:block')}
                                    style={{ background: thumb ? `center / cover no-repeat url(${thumb})` : p.dark, boxShadow: SHADOW }}>
                                    <span className="absolute inset-0 flex flex-col items-center justify-center gap-3" style={{ background: thumb ? 'rgb(0 0 0 / 0.25)' : undefined, color: '#fff' }}>
                                        <span className="flex h-16 w-16 items-center justify-center rounded-full" style={{ background: p.accent, color: p.onAccent }}><Play className="ml-1 h-7 w-7" fill="currentColor" /></span>
                                        <span className="max-w-[85%] truncate rounded-full bg-black/45 px-3 py-1 text-[11px]">{l.content?.url || 'Add a video link'}</span>
                                    </span>
                                </a>
                            </div>
                        </Styled>
                    ),
                });
                return;
            }
            case 'testimonial': {
                const t = l.content ?? {};
                if (t.source === 'reviews') {
                    const reviews = company.reviews ?? [];
                    if (!reviews.length) return;
                    if (l.heading?.trim()) out.push(sectionHeading(l, l.heading.trim(), 24, MessageSquareQuote));
                    out.push({
                        id: l.key,
                        render: () => (
                            <Styled l={l} p={p}>
                                <div className={cn('grid gap-3 pb-5 pt-3', reviews.length > 1 && 'sm:grid-cols-2', reviews.length > 2 && 'lg:grid-cols-3 print:grid-cols-3')} data-testid="featured-reviews">
                                    {reviews.map((r, i) => (
                                        <figure key={i} className="flex flex-col rounded-[18px] px-5 py-5" style={{ background: i === 0 ? p.dark : p.card, color: i === 0 ? p.onDark : p.text }}>
                                            <span className="flex gap-0.5" role="img" aria-label={`${r.rating} out of 5 stars`}>
                                                {[1, 2, 3, 4, 5].map((n) => <Star key={n} className="h-4 w-4" style={{ color: p.accent }} fill={n <= r.rating ? p.accent : 'none'} strokeWidth={1.5} aria-hidden />)}
                                            </span>
                                            {r.comment && <blockquote className="mt-3 flex-1 text-[14px] leading-[1.55]" style={hf}>“{r.comment}”</blockquote>}
                                            <figcaption className="mt-4 text-[12px] leading-tight"><span className="block font-medium" style={hf}>{r.name}</span>{r.role && <span className="opacity-70">{r.role}</span>}</figcaption>
                                        </figure>
                                    ))}
                                </div>
                            </Styled>
                        ),
                    });
                    return;
                }
                out.push({
                    id: l.key,
                    render: () => (
                        <Styled l={l} p={p}>
                            <div className="pb-5 pt-3">
                                <figure className="relative rounded-[18px] px-7 py-6" style={{ background: p.dark, color: p.onDark, textAlign: blockAlign(l) }}>
                                    <IconTile p={p} icon={MessageSquareQuote} size={36} />
                                    <blockquote className="mt-4 text-[17px] font-medium leading-[1.5]" style={{ ...hf, letterSpacing: '-0.01em', color: l.style?.color ?? p.onDark }}>“{t.quote}”</blockquote>
                                    {(t.name || t.role) && (
                                        <figcaption className={cn('mt-5 flex items-center gap-3 border-t pt-4', blockAlign(l) === 'center' && 'justify-center', blockAlign(l) === 'right' && 'justify-end')} style={{ borderColor: 'rgb(255 255 255 / 0.14)' }}>
                                            <Avatar p={p} hf={hf} text={initials(t.name || '?')} accent size={34} />
                                            <span className="text-left text-[12px] leading-tight"><span className="block font-medium" style={hf}>{t.name}</span><span className="opacity-70">{[t.role, t.client].filter(Boolean).join(', ')}</span></span>
                                        </figcaption>
                                    )}
                                </figure>
                            </div>
                        </Styled>
                    ),
                });
                return;
            }
            case 'team': {
                const members = l.content?.members ?? [];
                if (l.heading?.trim()) out.push(sectionHeading(l, l.heading.trim(), 24, Users));
                // Rows of three flow across pages independently.
                for (let k = 0; k < members.length; k += 3) {
                    const row = members.slice(k, k + 3);
                    out.push({
                        id: `${l.key}-r${k}`,
                        render: () => (
                            <Styled l={l} p={p}>
                                <div className="grid grid-cols-1 gap-3 pb-3 pt-1 @lg:grid-cols-3">
                                    {row.map((m, j) => (
                                        <div key={j} className="rounded-[18px] px-5 py-5" style={{ background: p.card }}>
                                            <Avatar p={p} hf={hf} text={initials(m.name || '?')} photo={m.photo_url} accent={!m.photo_url && (k + j) % 3 === 0} light={!m.photo_url && (k + j) % 3 === 2} size={56} />
                                            <div className="mt-4 text-[14px] font-medium" style={hf}>{m.name}</div>
                                            {m.role && <div className="text-[11.5px]" style={{ color: p.accentInk }}>{m.role}</div>}
                                            {m.bio && <p className="mt-2 text-[11.5px] leading-[1.6]" style={{ color: p.muted }}>{m.bio}</p>}
                                        </div>
                                    ))}
                                </div>
                            </Styled>
                        ),
                    });
                }
                return;
            }
            case 'timeline': {
                const steps = l.content?.steps ?? [];
                if (l.heading?.trim()) out.push(sectionHeading(l, l.heading.trim(), 24));
                if (l.content?.display === 'down') {
                    // Rows down the page: one step per row (number, title + when, description), soft divider between.
                    steps.forEach((st, k) => {
                        out.push({
                            id: `${l.key}-d${k}`,
                            render: () => (
                                <Styled l={l} p={p}>
                                    <div className="flex gap-4 py-3.5" style={{ borderTop: k ? `1px solid ${p.cardLine}` : undefined }} data-testid="timeline-row">
                                        <div className="shrink-0"><NumberTile p={p} hf={hf} n={k + 1} /></div>
                                        <div className="min-w-0 flex-1">
                                            <div className="flex flex-wrap items-center justify-between gap-2">
                                                <div className="text-[13.5px] font-medium" style={hf}>{st.title}</div>
                                                {st.when && <span className="rounded-full px-2.5 py-0.5 text-[10.5px] font-medium" style={{ background: p.card, color: p.text }}>{st.when}</span>}
                                            </div>
                                            {st.description && <p className="mt-1 text-[11.5px] leading-[1.6]" style={{ color: p.muted }}>{st.description}</p>}
                                        </div>
                                    </div>
                                </Styled>
                            ),
                        });
                    });
                    return;
                }
                for (let k = 0; k < steps.length; k += 3) {
                    const row = steps.slice(k, k + 3);
                    out.push({
                        id: `${l.key}-r${k}`,
                        render: () => (
                            <Styled l={l} p={p}>
                                <div className="grid grid-cols-1 gap-x-6 gap-y-5 pb-4 pt-1 @lg:grid-cols-3">
                                    {row.map((st, j) => (
                                        <div key={j} className="min-w-0">
                                            <div className="flex items-center justify-between gap-2">
                                                <NumberTile p={p} hf={hf} n={k + j + 1} />
                                                {st.when && <span className="rounded-full px-2.5 py-0.5 text-[10.5px] font-medium" style={{ background: p.card, color: p.text }}>{st.when}</span>}
                                            </div>
                                            <div className="mt-3 text-[13.5px] font-medium" style={hf}>{st.title}</div>
                                            {st.description && <p className="mt-1 text-[11.5px] leading-[1.6]" style={{ color: p.muted }}>{st.description}</p>}
                                        </div>
                                    ))}
                                </div>
                            </Styled>
                        ),
                    });
                }
                return;
            }
            case 'case_study': {
                const cs = l.content ?? {};
                out.push({
                    id: l.key,
                    render: () => (
                        <Styled l={l} p={p}>
                            <div className="pb-5 pt-5">
                                <div className="grid grid-cols-1 gap-3 @lg:grid-cols-[0.9fr_1.1fr]">
                                    <div className="relative flex min-h-[190px] flex-col justify-between overflow-hidden rounded-[18px] p-5" style={{ background: cs.image_url ? p.card : p.accent, color: p.onAccent }}>
                                        {cs.image_url && <img src={cs.image_url} alt="" className="absolute inset-0 h-full w-full object-cover" />}
                                        {!cs.image_url && <>
                                            <span className="flex h-9 w-9 items-center justify-center rounded-[9px]" style={{ background: p.dark, color: p.onDark }} aria-hidden><Trophy className="h-4 w-4" /></span>
                                            {cs.result && <div className="text-[30px] font-medium leading-none" style={{ ...hf, letterSpacing: '-0.03em' }}>{cs.result}</div>}
                                        </>}
                                    </div>
                                    <div className="min-w-0 rounded-[18px] px-6 py-5" style={{ background: p.card }}>
                                        {cs.client && <Chip p={p} hf={hf}>{cs.client}</Chip>}
                                        {l.heading && <h3 className="mt-3 text-[22px] font-medium leading-tight" style={{ ...hf, letterSpacing: '-0.02em' }}>{l.heading}</h3>}
                                        {cs.result && cs.image_url && <div className="mt-2 text-[24px] font-medium leading-none" style={{ ...hf, color: p.accentInk }}>{cs.result}</div>}
                                        {l.description && <Prose p={p} html={l.description} className="mt-3 text-[12.5px] leading-[1.65]" />}
                                    </div>
                                </div>
                            </div>
                        </Styled>
                    ),
                });
                return;
            }
            case 'terms': {
                out.push({ ...sectionHeading(l, l.heading?.trim() || 'Terms & conditions', 20, FileText) });
                richChunks(l, l.content?.source === 'custom' ? l.description : company.terms_html, 'text-[11px] leading-[1.65]', p.muted);
                return;
            }
            case 'acceptance': {
                // One acceptance section only, at the very end of the proposal (rendered after the closing).
                acceptLine = l;
                return;
            }
        }

        // ----- Price item -----
        const n = ++item;
        const choosable = l.optional || !!l.choice_group;
        const excluded = r && !r.included;
        const featured = !!l.recommended && !excluded && layout === 'single';
        const period = periodOf(l);
        const first = lines[i - 1]?.type !== 'price';
        const last = lines[i + 1]?.type !== 'price';
        const pick = (live: boolean, onAccent = false) => choosable && (
            <input type={l.choice_group ? 'radio' : 'checkbox'} name={l.choice_group ? `pv-${live ? '' : 'm-'}${l.choice_group}` : undefined}
                checked={l.selected} onChange={(e) => c.toggle(l.key, l.choice_group, e.target.checked)} tabIndex={live ? undefined : -1} disabled={c.locked}
                className="h-4 w-4 shrink-0 cursor-pointer" style={{ accentColor: onAccent ? p.card : p.charcoal }}
                aria-label={`${l.choice_group ? 'Choose' : 'Include'} ${l.heading ?? 'item'}`} />
        );
        const tags = (on: boolean) => <>
            {l.recommended && <Tag p={p} on={on}>Recommended</Tag>}
            {l.optional && <Tag p={p} on={on} soft>Optional</Tag>}
            {l.choice_group && !l.optional && <Tag p={p} on={on} soft>Choose one</Tag>}
        </>;
        const editQty = !!c.setQty && !!l.qty_editable && !c.locked;
        const qtyBox = (live: boolean) => (
            <input type="text" inputMode="decimal" value={l.qty} tabIndex={live ? undefined : -1} aria-label={`Quantity for ${l.heading ?? 'item'}`}
                data-testid={live ? `qty-${l.id ?? l.key}` : undefined}
                onChange={(e) => c.setQty?.(l.key, e.target.value.replace(/[^\d.]/g, '').slice(0, 10))}
                className="w-16 rounded-md border px-2 py-0.5 text-right text-[12px] tabular-nums print:border-0" style={{ borderColor: p.cardLine, background: p.card, color: p.text }} />
        );
        const meta = (color: string) => (c.showQty || c.showLinePrices || editQty) && (
            <div className="mt-1.5 text-[11.5px]" style={{ color }}>
                {(c.showQty || editQty) && (editQty ? <label className="inline-flex items-center gap-1.5">Qty {qtyBox(true)}</label> : <>Qty {l.qty}</>)}
                {c.showLinePrices && <> × {money(l.unit_price_cents, cur)}{l.discount_bp ? ` less ${l.discount_bp / 100}%` : ''}</>}
                {period && <> · {period}</>}
            </div>
        );

        if (layout !== 'single' && first && (c.showLinePrices || c.showQty)) {
            out.push({
                id: `${l.key}-head`, keepWithNext: true,
                render: () => layout === 'three' ? (
                    <div className="mt-2 hidden grid-cols-[1fr_4.5rem_6.5rem_7rem] gap-3 rounded-t-[14px] border px-5 py-2.5 text-[10.5px] font-bold uppercase tracking-[0.08em] @lg:grid"
                        style={{ background: p.dark, borderColor: p.dark, color: p.onDark, ...hf }}>
                        <span>Item</span><span className="text-right">{c.showQty ? 'Qty' : ''}</span><span className="text-right">{c.showLinePrices ? 'Unit price' : ''}</span><span className="text-right">{c.showLinePrices ? 'Amount' : ''}</span>
                    </div>
                ) : (
                    <div className="mt-2 flex justify-between border-b-2 pb-2 text-[10.5px] font-bold uppercase tracking-[0.08em]" style={{ borderColor: p.accent, color: p.accentInk, ...hf }}>
                        <span>Description</span>{c.showLinePrices && <span>Amount</span>}
                    </div>
                ),
            });
        }

        if (layout === 'three') {
            out.push({
                id: l.key,
                render: (live) => (
                    <div className={cn(last && 'pb-4')}>
                        <div className={cn('grid grid-cols-1 gap-x-3 gap-y-1 border-x border-b px-5 py-3.5 @lg:grid-cols-[1fr_4.5rem_6.5rem_7rem]', excluded && 'opacity-55', last && 'rounded-b-[14px]',
                            first && !(c.showLinePrices || c.showQty) && 'rounded-t-[14px] border-t')}
                            style={{ background: p.bg, borderColor: p.cardLine }}>
                            <div className="min-w-0">
                                <div className="flex flex-wrap items-center gap-2">
                                    {pick(live)}
                                    {quote.show_codes && l.code && <span className="font-mono text-[11px]" style={{ color: p.muted }}>{l.code}</span>}
                                    <span className="text-[13.5px] font-bold" style={hf}>{l.heading}</span>{tags(false)}
                                </div>
                                {l.description && <Prose p={p} html={l.description} className="mt-1 text-[12px] leading-[1.6]" />}
                                {period && <div className="mt-1 text-[11px]" style={{ color: p.muted }}>{period}</div>}
                            </div>
                            <div className="text-[12.5px] tabular-nums @lg:pt-0.5 @lg:text-right">{(c.showQty || editQty) && <><span className="@lg:hidden" style={{ color: p.muted }}>Qty </span>{editQty ? qtyBox(live) : l.qty}</>}</div>
                            <div className="text-[12.5px] tabular-nums @lg:pt-0.5 @lg:text-right">{c.showLinePrices && <><span className="@lg:hidden" style={{ color: p.muted }}>× </span>{money(l.unit_price_cents, cur)}{l.discount_bp ? <span className="block text-[10.5px]" style={{ color: p.muted }}>less {l.discount_bp / 100}%</span> : null}</>}</div>
                            <div className="@lg:pt-0.5 @lg:text-right">{c.showLinePrices && <Amount p={p} hf={hf} value={money(r?.net_cents ?? 0, cur)} size={14} />}</div>
                        </div>
                    </div>
                ),
            });
            return;
        }

        if (layout === 'right') {
            out.push({
                id: l.key,
                render: (live) => (
                    <div className={cn(last && 'pb-4')}>
                        <div className={cn('flex gap-5 border-b py-4', excluded && 'opacity-55')} style={{ borderColor: p.cardLine }}>
                            <div className="min-w-0 flex-1">
                                <div className="flex flex-wrap items-center gap-2">
                                    {pick(live)}
                                    {quote.show_codes && l.code && <span className="font-mono text-[11px]" style={{ color: p.muted }}>{l.code}</span>}
                                    <span className="text-[14px] font-bold" style={hf}>{l.heading}</span>{tags(false)}
                                </div>
                                {l.description && <Prose p={p} html={l.description} className="mt-1 text-[12.5px] leading-[1.65]" />}
                                {meta(p.muted)}
                            </div>
                            {c.showLinePrices && (
                                <div className="w-32 shrink-0 border-l-2 pl-4 text-right" style={{ borderColor: p.accent }}>
                                    <Amount p={p} hf={hf} value={money(r?.net_cents ?? 0, cur)} size={15} />
                                    {period && <div className="text-[10.5px]" style={{ color: p.muted }}>{period}</div>}
                                </div>
                            )}
                        </div>
                    </div>
                ),
            });
            return;
        }

        const fg = p.text;
        const sub = p.muted;
        out.push({
            id: l.key,
            render: (live) => (
                <Styled l={l} p={p}>
                    <div className="pb-3">
                        <div className="flex items-start gap-4 rounded-[18px] px-5 py-4"
                            style={{ background: excluded ? p.bg : p.card, color: fg, outline: excluded ? `1.5px dashed ${p.cardLine}` : featured ? `2px solid ${p.accent}` : undefined, outlineOffset: excluded ? -1.5 : undefined }}>
                            <div className="flex shrink-0 flex-col items-center gap-2">
                                <NumberTile p={p} hf={hf} n={n} />
                            </div>
                            <div className="min-w-0 flex-1 pt-0.5">
                                <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
                                    {quote.show_codes && l.code && <span className="font-mono text-[11px]" style={{ color: sub }}>{l.code}</span>}
                                    <span className="text-[15px] font-medium leading-snug" style={{ ...hf, letterSpacing: '-0.01em' }}>{l.heading}</span>
                                    {tags(false)}
                                </div>
                                {l.description && <Prose p={p} html={l.description} className="mt-1 text-[12.5px] leading-[1.65]" color={sub} />}
                                {meta(sub)}
                                {choosable && <OptionPick p={p} hf={hf} live={live} included={!excluded} group={!!l.choice_group} input={pick(live, !excluded)} locked={!!c.locked} />}
                            </div>
                            {c.showLinePrices && (
                                <div className="shrink-0 pt-0.5 text-right">
                                    <Amount p={p} hf={hf} value={money(r?.net_cents ?? 0, cur)} size={16} />
                                    {period && <div className="text-[10.5px]" style={{ color: sub }}>{period}</div>}
                                </div>
                            )}
                        </div>
                    </div>
                </Styled>
            ),
        });
    });

    if (c.showTotals && groups.length) {
        const headline = oneOff ?? groups[0];
        const extras = groups.filter((g) => g !== headline);
        out.push({
            id: 'totals',
            render: (live) => (
                <div className="pb-4 pt-8" data-testid={live ? 'preview-totals' : undefined}>
                    <Eyebrow p={p} hf={hf} className="mb-2.5">Pricing</Eyebrow>
                    <Heading p={p} hf={hf} text="Investment summary" size={30} icon={Wallet} align={align} />
                    <p className="mt-3 text-[12.5px]" style={{ color: p.muted, textAlign: align }}>
                        All prices in {cur}{taxWord ? `, ${taxWord}` : ''}. Valid until {quote.expiry_date ? date(quote.expiry_date) : 'further notice'}.
                    </p>
                    <div className="mt-5 grid grid-cols-1 gap-3 @lg:grid-cols-[1fr_1.15fr]">
                        <div className="flex flex-col justify-between rounded-[18px] px-6 py-6" style={{ background: p.card, color: p.text }}>
                            <div className="flex items-center gap-3 text-[12.5px] font-medium" style={hf}>
                                <IconTile p={p} icon={Wallet} size={34} />
                                {extras.length ? headline.label : 'Total investment'}
                            </div>
                            <div className="mt-8">
                                <div className="break-words leading-none"><Amount p={p} hf={hf} value={money(headline.total_cents, cur)} size={40} /></div>
                                <div className="mt-2.5 text-[12px]" style={{ color: p.muted }}>{headline.term_total_cents !== null ? `${money(headline.term_total_cents, cur)} over ${headline.term} periods` : taxWord || 'Total'}</div>
                            </div>
                        </div>
                        <div className="rounded-[18px] border px-6 py-5" style={{ background: p.bg, borderColor: p.cardLine }}>
                            {groups.map((g, gi) => (
                                <div key={g.key} className={cn('space-y-2 text-[12.5px]', gi > 0 && 'mt-4 border-t pt-4')} style={{ borderColor: p.cardLine }}>
                                    {groups.length > 1 && <div className="text-[11px] font-medium" style={{ color: p.accentInk, ...hf }}>{g.label}</div>}
                                    {g.discount_cents !== 0 && <Row p={p} label="Discounts" value={`−${money(g.discount_cents, cur)}`} />}
                                    <Row p={p} label={`Subtotal${quote.tax_mode === 'none' ? '' : ' (ex GST)'}`} value={money(g.subtotal_cents, cur)} />
                                    {quote.tax_mode !== 'none' && quote.tax_mode !== 'exclusive' && <Row p={p} label="GST" value={money(g.tax_cents, cur)} />}
                                    <div className="flex justify-between border-t pt-2 text-[14px] font-medium" style={{ borderColor: p.cardLine, ...hf }}>
                                        <span>Total</span><span className="font-semibold tabular-nums" style={{ color: p.charcoal }}>{money(g.total_cents, cur)}</span>
                                    </div>
                                </div>
                            ))}
                        </div>
                    </div>
                    {(extras.length > 0 || deposit !== null) && (
                        <div className="mt-3 flex flex-col rounded-[18px] px-2 py-5 @lg:flex-row" style={{ background: p.card }}>
                            {extras.map((g) => (
                                <Step key={g.key} p={p} hf={hf} title={g.label} value={money(g.total_cents, cur)}
                                    note={g.term_total_cents !== null ? `${money(g.term_total_cents, cur)} over ${g.term} periods` : taxWord} />
                            ))}
                            {deposit !== null && <Step p={p} hf={hf} title={`Deposit (${quote.deposit_bp / 100}%)`} value={money(deposit, cur)} note="Due on acceptance" />}
                        </div>
                    )}
                </div>
            ),
        });
    }
    if (!c.showMoney) {
        out.push({
            id: 'no-money',
            render: () => <p className="mt-6 rounded-[14px] px-4 py-3 text-[12px]" style={{ background: p.card }}>Prices are hidden in this preview because your role can't view $ values.</p>,
        });
    }

    // Company T&Cs close the proposal unless the quote placed its own T&Cs block.
    const terms = hasTermsBlock ? [] : splitHtml(company.terms_html);
    if (terms.length) {
        out.push({ id: 'terms-h', keepWithNext: true, render: () => <div className="pb-2 pt-9"><Heading p={p} hf={hf} text="Terms & conditions" size={20} icon={FileText} align={align} /></div> });
        terms.forEach((html, i) => out.push({ id: `terms-${i}`, render: () => <Prose p={p} html={html} className="text-[11px] leading-[1.65]" color={p.muted} align={align} /> }));
    }

    out.push({
        id: 'closing', keepWithNext: true,
        render: () => (
            <div className="pt-12">
                <Eyebrow p={p} hf={hf}>Ready when you are</Eyebrow>
                <div className="mt-3"><Heading p={p} hf={hf} text="Thank you for your time" size={40} /></div>
                <p className="mt-4 max-w-[480px] text-[13px] leading-[1.7]" style={{ color: p.muted }}>
                    We'd love to work with you. If anything in this {label.toLowerCase()} needs a tweak, just let us know.
                </p>
                <div className="mt-7 grid grid-cols-1 gap-x-6 gap-y-4 @lg:grid-cols-2">
                    {(owner?.phone || company.phone) && <ContactItem p={p} hf={hf} icon={Phone} title="Phone" text={(owner?.phone || company.phone)!} href={telHref((owner?.phone || company.phone)!)} n={0} />}
                    {owner && <ContactItem p={p} hf={hf} title={owner.name} text={owner.email} href={mailHref(owner.email)} n={1} />}
                    {company.website && <ContactItem p={p} hf={hf} icon={Globe} title="Website" text={company.website.replace(/^https?:\/\//, '')} href={webHref(company.website)} n={2} />}
                    {company.address && <ContactItem p={p} hf={hf} icon={MapPin} title="Address" text={company.address} n={0} />}
                </div>
            </div>
        ),
    });

    // The one acceptance section, at the very end: statement, signature lines and the Accept button.
    const al = acceptLine as Line | null;
    const a = al?.content ?? {};
    const signature = a.signature ?? 'typed';
    const statement = a.statement?.trim() || `I accept this ${label.toLowerCase()} and its terms and conditions.`;
    out.push({
        id: al?.key ?? 'acceptance',
        render: () => (
            <div className="pb-2 pt-10" data-testid="acceptance-section">
                <div className="rounded-[18px] px-7 py-6" style={{ background: p.card }}>
                    <div className="flex items-center gap-3">
                        <IconTile p={p} icon={FileSignature} />
                        <h3 className="text-[22px] font-medium" style={{ ...hf, letterSpacing: '-0.015em' }}>{al?.heading?.trim() || `Accept this ${label.toLowerCase()}`}</h3>
                    </div>
                    {/* Before acceptance, the statement, signature, name and date are all in the Accept pop-up; once signed, they're shown here as the record. */}
                    {!c.signed && <p className="mt-3 text-[12.5px] leading-[1.6]" style={{ color: p.muted }}>Ready to go ahead? Click Accept to confirm and sign.</p>}
                    {c.signed && <>
                    <p className="mt-4 flex items-start gap-2.5 text-[12.5px] leading-[1.6]">
                        <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" style={{ color: p.accentInk }} aria-hidden />
                        {statement}
                    </p>
                    <div className="mt-5 grid grid-cols-1 gap-5 @lg:grid-cols-[1.4fr_1fr_1fr]" data-testid={c.signed ? 'signed-block' : undefined}>
                        {signature !== 'none' && <SignLine p={p} label={signature === 'drawn' ? 'Signature' : 'Signature (typed)'} tall={signature === 'drawn'}
                            value={c.signed ? (c.signed.signature_type === 'drawn' && c.signed.signature
                                ? <img src={c.signed.signature} alt={`Signature of ${c.signed.name}`} className="h-14 w-auto" />
                                : c.signed.signature ? <span className="text-[24px]" style={{ fontFamily: "'Segoe Script', 'Brush Script MT', 'Snell Roundhand', cursive" }}>{c.signed.signature}</span>
                                    : <span className="text-[12px]" style={{ color: p.muted }}>{c.signed.on_behalf ? 'Accepted by phone / email' : 'Accepted online'}</span>) : null} />}
                        <SignLine p={p} label="Name" value={c.signed?.name ?? null} />
                        <SignLine p={p} label="Date and time" value={c.signed ? (c.signed.at ? dateTime(c.signed.at) : date(c.signed.date)) : null} />
                    </div>
                    <p className="mt-4 text-[11.5px] leading-[1.6]" style={{ color: p.muted }} data-testid="signed-record">
                        {c.signed.on_behalf
                            ? <>Accepted by <b style={{ color: p.text }}>{c.signed.name}</b> by phone or email{c.signed.when ? `, recorded on ${c.signed.when}` : ''}{c.signed.recorded_by ? ` by ${c.signed.recorded_by}` : ''}.</>
                            : <>Accepted online by <b style={{ color: p.text }}>{c.signed.name}</b>{c.signed.email ? ` (${c.signed.email})` : ''}{c.signed.when ? ` on ${c.signed.when}` : ''}.</>}
                    </p>
                    </>}
                    <div className="mt-7 flex flex-wrap items-center gap-x-8 gap-y-4 border-t pt-5" style={{ borderColor: p.cardLine }}>
                        {!c.signed && <AcceptButton p={p} label={label} action={c.accept} />}
                        {c.showTotals && groups[0] && <MiniStat p={p} hf={hf} value={money((oneOff ?? groups[0]).total_cents, cur)} label="Total investment" />}
                        {quote.expiry_date && <MiniStat p={p} hf={hf} value={date(quote.expiry_date)} label="Valid until" plain />}
                    </div>
                </div>
            </div>
        ),
    });
    return out;
}

/**
 * The tick box on an optional item / choice: an obvious "tick to add" control on screen, and a printed
 * box (ticked when included) in the PDF.
 */
function OptionPick({ p, hf, live, included, group, input, locked }: { p: Palette; hf: CSSProperties; live: boolean; included: boolean; group: boolean; input: ReactNode; locked: boolean }) {
    const text = included ? (group ? 'Selected option' : 'Added to your quote') : (group ? 'Choose this option' : 'Optional: tick to add it to your quote');
    return (
        <label className={cn('mt-3 inline-flex items-center gap-2.5 rounded-full border py-1.5 pl-2.5 pr-4 text-[11.5px] font-medium', !locked && live && 'cursor-pointer')}
            style={{ ...hf, borderColor: included ? p.charcoal : p.cardLine, background: included ? p.charcoal : p.bg, color: included ? '#ffffff' : p.text }}>
            <span className="print:hidden">{input}</span>
            <span className={cn('hidden h-4 w-4 shrink-0 items-center justify-center rounded-[4px] border-2 print:inline-flex', group && 'rounded-full')}
                style={{ borderColor: included ? '#ffffff' : p.charcoal }} aria-hidden>
                {included && <CheckCircle2 className="h-3 w-3" />}
            </span>
            {text}
        </label>
    );
}

function SignLine({ p, label, tall = false, value = null }: { p: Palette; label: string; tall?: boolean; value?: ReactNode }) {
    return (
        <div>
            <div className={cn('flex items-end border-b pb-1 text-[13.5px] font-medium', tall ? 'h-16' : 'h-9')} style={{ borderColor: p.muted }}>{value}</div>
            <div className="mt-1.5 text-[10.5px] font-medium" style={{ color: p.muted }}>{label}</div>
        </div>
    );
}
