import { Head, Link } from '@inertiajs/react';
import { ArrowLeft, Download, Eye, FileText, Monitor, Printer, ScrollText, Smartphone, UserRound } from 'lucide-react';
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import { buildBlocks, type Block, type Layout, type Signed } from '@/components/proposal/buildBlocks';
import { Brand, Eyebrow, Heading, NumberTile, pad2, SHADOW, telHref, webHref, type Align } from '@/components/proposal/elements';
import { buildPalette, sansFont, type Palette } from '@/components/proposal/palette';
import { toTotalsLine, withKeys, type Line } from '@/components/quote/LinesEditor';
import { date, money } from '@/lib/format';
import { fillDeep, fillHtml, fillText } from '@/lib/smartTags';
import { calculateTotals, type TaxMode } from '@/lib/totals';
import { cn } from '@/lib/utils';
import { Flash } from '@/components/Flash';
import { AcceptDialog, ClientBanner, ClientBar, DeclineDialog, QuestionsSection, useProposalTracking, type ClientMode } from '@/components/proposal/ClientPanel';
import type { AcceptAction } from '@/components/proposal/elements';

type Company = {
    name: string; abn: string | null; phone: string | null; website: string | null; address: string; logo_url: string | null;
    colours: { primary: string; secondary: string; background: string; text: string; button: string };
    heading_font: string; body_font: string; terms_html: string | null;
    reviews?: { rating: number; comment: string | null; name: string | null; role: string | null }[];
};
type Props = {
    quote: {
        id: number | null; number: string; title: string; title_label: string; status: string; currency: string; tax_mode: TaxMode;
        discount_bp: number; deposit_bp: number; pricing_visibility: 'all' | 'totals_only' | 'quantities_only' | 'hide_all'; show_codes: boolean;
        quote_date: string; expiry_date: string | null; lines: Omit<Line, 'key'>[]; theme: { layout: Layout; align: Align };
        cover_image?: { url: string; alt: string } | null;
    };
    company: Company;
    client: { name: string; company: string | null; email: string | null } | null;
    owner: { name: string; email: string; phone: string | null; job_title: string | null } | null;
    tags: Record<string, string | null>;
    showMoney: boolean;
    paper: 'a4' | 'letter';
    view: 'pages' | 'web';
    render: boolean;
    editUrl: string | null;
    pdfUrl: string | null;
    kind: 'quote' | 'template';
    /** Set on the client's own page (/p/{token}); null for staff previews. */
    clientMode: ClientMode | null;
    signed: Signed | null;
};

/* Page sizes at 96 dpi. Everything is laid out in these units and scaled to fit on screen. */
const PAPER = {
    a4: { w: 794, h: 1123, css: 'A4', mmW: '210mm', mmH: '297mm', label: 'A4' },
    letter: { w: 816, h: 1056, css: 'letter', mmW: '8.5in', mmH: '11in', label: 'US Letter' },
};
const PAD_X = 52;
const BODY_TOP = 112;
const BODY_BOTTOM = 76;

type Mode = 'pages' | 'web' | 'mobile';

/** Greedy page packing; a block marked keepWithNext moves to the next page with its follower. */
function paginate(heights: number[], keep: boolean[], max: number): number[][] {
    const pages: number[][] = [[]];
    let used = 0;
    heights.forEach((h, i) => {
        const withNext = keep[i] && i + 1 < heights.length ? h + heights[i + 1] : h;
        const need = withNext <= max ? withNext : h;
        if (used > 0 && used + need > max) { pages.push([]); used = 0; }
        pages[pages.length - 1].push(i);
        used += h;
    });
    return pages;
}

/**
 * Client proposal. "pages": portrait A4 / US Letter sheets (cover, flowing content, closing) —
 * what prints and becomes the PDF. "web": the same blocks as a responsive page (desktop / mobile).
 * Staff preview: optional items and choices can be toggled; nothing is saved or tracked.
 */
export default function QuotePreview(props: Props) {
    const { quote, company, client, owner, tags, showMoney, editUrl, pdfUrl, kind } = props;
    const embed = typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('embed') === '1';
    const cm = props.clientMode && !props.clientMode.render ? props.clientMode : null;
    const chrome = !props.render && !embed && !cm;
    const [mode, setMode] = useState<Mode>(props.view === 'web' ? 'web' : 'pages');
    const [paperKey, setPaperKey] = useState(props.paper);
    const paper = PAPER[paperKey];
    const [lines, setLines] = useState(() => withKeys(quote.lines));
    const p = useMemo(() => buildPalette(company.colours), [company.colours]);
    const headingFont = sansFont(company.heading_font);
    const bodyFont = sansFont(company.body_font);
    const vis = showMoney ? quote.pricing_visibility : 'hide_all';
    const flags = { showMoney, showLinePrices: vis === 'all', showQty: vis === 'all' || vis === 'quantities_only', showTotals: vis === 'all' || vis === 'totals_only' };
    const totals = useMemo(() => calculateTotals({ tax_mode: quote.tax_mode, discount_bp: quote.discount_bp, lines: lines.map(toTotalsLine) }), [lines, quote]);
    const toggle = useCallback((key: string, group: string | null, value: boolean) =>
        setLines((ls) => ls.map((l) => (group ? (l.choice_group === group ? { ...l, selected: l.key === key } : l) : l.key === key ? { ...l, selected: value } : l))), []);
    const setQty = useCallback((key: string, qty: string) => setLines((ls) => ls.map((l) => (l.key === key ? { ...l, qty } : l))), []);
    const [accepting, setAccepting] = useState(false);
    const [declining, setDeclining] = useState(false);
    useProposalTracking(cm?.track ? cm.base : null, true);
    const acceptAction: AcceptAction | undefined = cm ? {
        onClick: () => setAccepting(true), disabled: !!cm.blocked,
        text: cm.decision?.decision === 'accepted' ? 'Accepted' : cm.decision?.decision === 'declined' ? 'Declined' : undefined,
        title: cm.blocked ?? undefined,
    } : undefined;
    const fonts = [...new Set([headingFont, bodyFont])].map((f) => `family=${encodeURIComponent(f)}:wght@400;500;600;700;800`).join('&');
    const hf: CSSProperties = useMemo(() => ({ fontFamily: `'${headingFont}', sans-serif` }), [headingFont]);
    const title = fillText(quote.title, tags) || `${quote.title_label} ${quote.number}`;

    // Smart tags are filled once and appear everywhere.
    const filled = useMemo(() => lines.map((l) => ({
        ...l, heading: fillText(l.heading, tags), description: fillHtml(l.description, tags, !props.render), content: l.content ? fillDeep(l.content, tags) : l.content,
    })), [lines, tags, props.render]);
    const coverBlock = filled.find((l) => l.type === 'cover') ?? null;
    // Cover hero: the quote's cover image, else a legacy Cover block's image. Images in the body always
    // stay exactly where they were placed (they are never lifted onto the cover).
    const heroImage = quote.cover_image ?? (coverBlock?.content?.image_url ? { url: coverBlock.content.image_url, alt: '' } : null);
    const heroKey = null;

    const oneOff = totals.groups.find((g) => g.key === 'one_off');
    const headline = flags.showTotals ? (oneOff ?? totals.groups[0] ?? null) : null;
    const subsExist = totals.groups.some((g) => g.key !== 'one_off');

    const blocks = useMemo<Block[]>(() => buildBlocks({
        lines: filled, totals, p, hf, layout: quote.theme?.layout ?? 'single', align: quote.theme?.align ?? 'left',
        quote, company: { ...company, terms_html: fillHtml(company.terms_html, tags, false) }, owner, ...flags, heroKey, toggle,
        accept: acceptAction, setQty: cm ? setQty : undefined, locked: !!cm?.blocked, signed: props.signed,
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }), [filled, totals, p, hf, quote, company, owner, heroKey, toggle, showMoney, vis, tags, cm?.blocked, cm?.decision?.decision]);

    /* ---------- Measure blocks off-screen, then pack them into pages ---------- */
    const contentW = paper.w - PAD_X * 2;
    const bodyH = paper.h - BODY_TOP - BODY_BOTTOM;
    const measureRef = useRef<HTMLDivElement>(null);
    const [heights, setHeights] = useState<number[]>([]);
    const measure = useCallback(() => {
        const box = measureRef.current;
        if (!box) return;
        const hs = [...box.children].map((n) => (n as HTMLElement).offsetHeight);
        setHeights((prev) => (prev.length === hs.length && prev.every((v, i) => v === hs[i]) ? prev : hs));
    }, []);
    useLayoutEffect(() => {
        const box = measureRef.current;
        if (!box) return;
        measure();
        const ro = new ResizeObserver(measure);
        [...box.children].forEach((c) => ro.observe(c));
        document.fonts?.ready.then(measure);
        return () => ro.disconnect();
    }, [blocks, measure, paperKey, mode]);

    const pages = useMemo(
        () => (heights.length === blocks.length ? paginate(heights, blocks.map((b) => !!b.keepWithNext), bodyH) : [blocks.map((_, i) => i)]),
        [heights, blocks, bodyH],
    );
    const totalPages = pages.length + 1;
    const sections = useMemo(() => {
        const out: { title: string; page: number }[] = [];
        pages.forEach((idx, pi) => idx.forEach((bi) => { const b = blocks[bi]; if (b?.section !== undefined && b.title) out[b.section] = { title: b.title, page: pi + 2 }; }));
        return out.filter(Boolean);
    }, [pages, blocks]);

    /* ---------- Fit the sheets to the screen ---------- */
    const [scale, setScale] = useState(1);
    useEffect(() => {
        const fit = () => setScale(Math.min(1, (window.innerWidth - 24) / paper.w));
        fit();
        window.addEventListener('resize', fit);
        return () => window.removeEventListener('resize', fit);
    }, [paper.w]);
    useEffect(() => {
        if (!chrome) return;
        const u = new URL(window.location.href);
        if (mode === 'web') u.searchParams.set('view', 'web'); else u.searchParams.delete('view');
        if (paperKey !== props.paper) u.searchParams.set('paper', paperKey); else u.searchParams.delete('paper');
        window.history.replaceState(window.history.state, '', u.toString());
    }, [mode, paperKey, chrome, props.paper]);
    // PDF renderer waits for this flag (fonts loaded + pages packed).
    useEffect(() => {
        if (heights.length !== blocks.length) return;
        // Don't wait forever on web fonts (e.g. a server without internet access): fall back after 4s.
        const fonts = document.fonts?.ready ?? Promise.resolve();
        Promise.race([fonts, new Promise((r) => setTimeout(r, 4000))]).then(() => requestAnimationFrame(() => { document.documentElement.dataset.proposalReady = '1'; }));
    }, [heights, blocks]);

    const rootStyle = {
        fontFamily: `'${bodyFont}', sans-serif`, color: p.text, '--color-link': p.accentInk, '--color-link-hover': p.text, '--kn-dot': p.accent, '--kn-ink': p.text,
        '--sheet-w': paper.mmW, '--sheet-h': paper.mmH,
    } as CSSProperties;
    const chromeProps = { p, hf, company, quote, owner, totalPages, paper };
    const coverProps = {
        p, hf, quote, title, company, client, owner, hero: heroImage, subtitle: coverBlock?.content?.subtitle ?? null,
        total: headline ? money(headline.total_cents, quote.currency) : null, totalNote: headline ? (subsExist && headline === oneOff ? 'One-off total' : 'Total investment') : '',
        sections, accept: acceptAction,
    };
    const selections = lines.filter((l) => l.id).map((l) => ({ id: l.id as number, selected: !!l.selected, qty: String(l.qty) }));
    const summary = lines.filter((l) => l.type === 'price' && (l.optional || l.choice_group) && l.selected).map((l) => `Includes ${l.heading ?? 'option'}`);
    const clientTotal = headline ? money(headline.total_cents, quote.currency) : null;

    return (
        <div className="proposal-screen min-h-screen" style={{ ...rootStyle, background: chrome ? '#d9dbe3' : p.page }}>
            <Head title={cm ? `${quote.title_label} ${quote.number} · ${company.name}` : `${kind === 'template' ? 'Template preview' : 'Preview'} · ${kind === 'template' ? title : quote.number}`}>
                <link rel="stylesheet" href={`https://fonts.googleapis.com/css2?${fonts}&display=swap`} />
            </Head>
            <style>{`@page { size: ${paper.css} portrait; margin: 0; }`}</style>
            {chrome && (
                <div className="sticky top-0 z-30 flex flex-wrap items-center gap-3 bg-stone-900 px-4 py-2.5 text-sm text-white print:hidden" role="note" style={{ fontFamily: 'var(--font-sans)' }}>
                    <Eye className="h-4 w-4 shrink-0" aria-hidden />
                    <span className="font-medium">{kind === 'template' ? 'Template preview' : 'Preview'}</span>
                    <span className="hidden text-stone-300 md:inline">
                        {kind === 'template' ? 'Sample client details; smart tags are filled when the template is used in a quote.' : `This is how your client will see ${quote.number}. Nothing here is saved or tracked.`}
                    </span>
                    <div className="ml-auto flex flex-wrap items-center gap-2">
                        <div className="flex rounded bg-white/10 p-0.5" role="radiogroup" aria-label="Preview as">
                            {([['pages', 'Pages', FileText], ['web', 'Desktop', Monitor], ['mobile', 'Mobile', Smartphone]] as const).map(([m, lbl, Icon]) => (
                                <button key={m} type="button" role="radio" aria-checked={mode === m} onClick={() => setMode(m)} data-testid={`preview-mode-${m}`}
                                    className={cn('inline-flex items-center gap-1.5 rounded px-2.5 py-1 font-medium', mode === m ? 'bg-white text-stone-900' : 'hover:bg-white/10')}>
                                    <Icon className="h-4 w-4" aria-hidden />{lbl}
                                </button>
                            ))}
                        </div>
                        {mode === 'pages' && (
                            <select aria-label="Paper size" value={paperKey} onChange={(e) => setPaperKey(e.target.value as 'a4' | 'letter')} className="rounded bg-white/10 px-2 py-1 text-sm text-white [&>option]:text-stone-900">
                                <option value="a4">A4</option><option value="letter">US Letter</option>
                            </select>
                        )}
                        {pdfUrl && <a href={`${pdfUrl}?paper=${paperKey}`} className="inline-flex items-center gap-1.5 rounded bg-white/10 px-3 py-1 font-medium hover:bg-white/20" data-testid="download-pdf"><Download className="h-4 w-4" aria-hidden /> PDF</a>}
                        <button type="button" onClick={() => { setMode('pages'); setTimeout(() => window.print(), 50); }} className="inline-flex items-center gap-1.5 rounded bg-white/10 px-3 py-1 font-medium hover:bg-white/20">
                            <Printer className="h-4 w-4" aria-hidden /> Print
                        </button>
                        {editUrl && <Link href={editUrl} className="inline-flex items-center gap-1.5 rounded bg-white/10 px-3 py-1 font-medium hover:bg-white/20"><ArrowLeft className="h-4 w-4" /> Back to editing</Link>}
                    </div>
                </div>
            )}

            {chrome && <div className="mx-auto max-w-3xl px-4 print:hidden [&>div]:mt-4" style={{ fontFamily: 'var(--font-sans)' }}><Flash /></div>}
            {cm && <ClientBanner p={p} hf={hf} mode={cm} label={quote.title_label} />}
            {cm && mode === 'pages' && (
                <div className="mx-auto flex max-w-[820px] justify-end gap-2 px-4 pt-4 print:hidden">
                    <button type="button" onClick={() => window.print()} className="rounded-full px-4 py-2 text-[13px] font-semibold" style={{ background: p.card, boxShadow: SHADOW }}><Printer className="mr-1.5 inline h-4 w-4" />Print / Save as PDF</button>
                    <button type="button" onClick={() => setMode('web')} className="rounded-full px-4 py-2 text-[13px] font-semibold" style={{ background: p.grad, color: p.onAccent }}>Back to the web view</button>
                </div>
            )}
            {mode === 'mobile' && chrome ? (
                <div className="flex justify-center px-4 py-8">
                    <div className="overflow-hidden rounded-[38px] border-[10px] border-stone-900 bg-stone-900 shadow-2xl">
                        <iframe title="Mobile preview" src={mobileSrc()} className="block h-[760px] w-[390px] rounded-[28px] bg-white" data-testid="mobile-frame" />
                    </div>
                </div>
            ) : mode === 'web' ? (
                <WebView {...coverProps} blocks={blocks} after={cm ? <QuestionsSection p={p} hf={hf} mode={cm} /> : null} />
            ) : (
                <>
                    {/* Off-screen measuring copy, same width as a page body. */}
                    <div ref={measureRef} inert aria-hidden className="@container pointer-events-none fixed left-[-10000px] top-0 h-0 overflow-hidden" style={{ width: contentW, visibility: 'hidden' }}>
                        {blocks.map((b) => <div key={b.id} style={{ display: 'flow-root' }}>{b.render(false)}</div>)}
                    </div>
                    <div className={cn('motion-page flex flex-col items-center gap-6 print:block print:p-0', chrome && 'py-6 sm:py-10')} data-testid="preview">
                        <Sheet scale={chrome ? scale : 1} paper={paper}>
                            <PageChrome {...chromeProps} page={1}><Cover {...coverProps} /></PageChrome>
                        </Sheet>
                        {pages.map((idx, pi) => (
                            <Sheet key={pi} scale={chrome ? scale : 1} paper={paper}>
                                <PageChrome {...chromeProps} page={pi + 2}>
                                    <div className="@container">{idx.map((bi) => <div key={blocks[bi].id} style={{ display: 'flow-root' }}>{blocks[bi].render(true)}</div>)}</div>
                                </PageChrome>
                            </Sheet>
                        ))}
                    </div>
                </>
            )}
            {cm && mode === 'web' && (
                <ClientBar p={p} hf={hf} mode={cm} label={quote.title_label} total={clientTotal} onAccept={() => setAccepting(true)} onDecline={() => setDeclining(true)}
                    pdfUrl={pdfUrl} paper={paperKey} onPrint={() => { setMode('pages'); setTimeout(() => window.print(), 300); }} />
            )}
            {cm && !cm.blocked && (
                <>
                    <AcceptDialog p={p} hf={hf} open={accepting} onOpenChange={setAccepting} mode={cm} label={quote.title_label} total={clientTotal}
                        selections={selections} expectedTotal={showMoney && flags.showTotals ? totals.total_cents : null} summary={summary} />
                    <DeclineDialog p={p} hf={hf} open={declining} onOpenChange={setDeclining} mode={cm} label={quote.title_label} />
                </>
            )}
        </div>
    );
}

function mobileSrc() {
    const u = new URL(window.location.href);
    u.searchParams.set('view', 'web');
    u.searchParams.set('embed', '1');
    return u.pathname + u.search;
}

/* ---------- Page furniture ---------- */

function Sheet({ scale, paper, children }: { scale: number; paper: (typeof PAPER)['a4']; children: ReactNode }) {
    return (
        <div className="proposal-sheet-wrap shrink-0" style={{ width: paper.w * scale, height: paper.h * scale }}>
            <div className="proposal-sheet origin-top-left shadow-[0_8px_30px_rgb(30_35_70/0.14)]" style={{ width: paper.w, height: paper.h, transform: scale < 1 ? `scale(${scale})` : undefined }}>
                {children}
            </div>
        </div>
    );
}

function contactHref(owner: Props['owner'], company: Company) {
    return owner?.email ? `mailto:${owner.email}` : company.phone ? `tel:${company.phone.replace(/\s+/g, '')}` : null;
}

function PageChrome({ p, hf, company, quote, owner, page, totalPages, children }: {
    p: Palette; hf: CSSProperties; company: Company; quote: Props['quote']; owner: Props['owner']; page: number; totalPages: number; paper: (typeof PAPER)['a4']; children: ReactNode;
}) {
    const contact = contactHref(owner, company);
    return (
        <section className="relative h-full w-full overflow-hidden" style={{ background: p.page, color: p.text }} aria-label={`Page ${page} of ${totalPages}`}>
            <header className="absolute flex items-center justify-between" style={{ top: 26, left: PAD_X, right: PAD_X, height: 76 }}>
                <Brand p={p} hf={hf} company={company} />
                <span className="flex items-center gap-4 text-[12px]">
                    <Eyebrow p={p} hf={hf}>{quote.title_label} {quote.number}</Eyebrow>
                    {contact && <a href={contact} className="rounded-full px-3 py-1 font-medium" style={{ ...hf, background: p.dark, color: p.onDark }}>Contact us</a>}
                </span>
            </header>
            <div className="absolute" style={{ top: BODY_TOP, left: PAD_X, right: PAD_X, bottom: BODY_BOTTOM }}>{children}</div>
            <footer className="absolute flex items-center justify-between text-[10.5px]" style={{ bottom: 32, left: PAD_X, right: PAD_X, color: p.muted }}>
                <span className="min-w-0 truncate pr-6">{[company.address || null, company.phone ? <a key="tel" href={telHref(company.phone)} className="hover:underline">{company.phone}</a> : null].filter(Boolean).map((x, i) => <span key={i}>{i > 0 && ' · '}{x}</span>)}</span>
                <span className="flex shrink-0 items-center gap-2" style={hf}>
                    <span className="font-medium tabular-nums" style={{ color: p.text }}>{pad2(page)}</span>
                    <span className="h-px w-8" style={{ background: p.cardLine }} aria-hidden />
                    <span className="tabular-nums">{pad2(totalPages)}</span>
                </span>
            </footer>
        </section>
    );
}

/* ---------- Cover ---------- */

type CoverProps = {
    p: Palette; hf: CSSProperties; quote: Props['quote']; title: string; company: Company; client: Props['client']; owner: Props['owner'];
    hero: { url: string; alt: string } | null; subtitle: string | null; total: string | null; totalNote: string; sections: { title: string; page: number }[];
    accept?: AcceptAction;
};

/** Cover visual: the hero image (when there is one) beside the "Prepared for" and "Valid until" cards. */
function HeroPanel({ p, hf, hero, client, company, owner, quote, className }: CoverProps & { className?: string }) {
    const who = client ? client.name : company.name;
    const cards = (
        <>
            <ForCard p={p} hf={hf} who={who} sub={client?.company ?? null} owner={owner} client={!!client} tall={!hero} />
            <div className="flex min-h-0 flex-col justify-between rounded-[18px] px-5 py-4" style={{ background: p.card }}>
                <span className="text-[11px]" style={{ color: p.muted }}>Valid until</span>
                <span className="text-[24px] font-medium leading-none tabular-nums" style={{ ...hf, letterSpacing: '-0.02em' }}>{quote.expiry_date ? date(quote.expiry_date) : '—'}</span>
            </div>
        </>
    );
    return hero ? (
        <div className={cn('grid grid-cols-[1.6fr_1fr] gap-3', className)}>
            <div className="overflow-hidden rounded-[18px]" style={{ background: p.card }}><img src={hero.url} alt={hero.alt} className="h-full w-full object-cover" /></div>
            <div className="grid grid-rows-2 gap-3">{cards}</div>
        </div>
    ) : (
        <div className={cn('grid grid-cols-[1.35fr_1fr] gap-3', className)}>{cards}</div>
    );
}

function ForCard({ p, hf, who, sub, owner, client, tall = false }: { p: Palette; hf: CSSProperties; who: string; sub: string | null; owner: Props['owner']; client: boolean; tall?: boolean }) {
    return (
        <div className={cn('flex min-h-0 flex-col justify-between rounded-[18px] px-5 py-5', tall && 'py-6')} style={{ background: p.dark, color: p.onDark }}>
            <span className="flex h-9 w-9 items-center justify-center rounded-[9px]" style={{ background: p.accent, color: p.onAccent }} aria-hidden><UserRound className="h-4 w-4" strokeWidth={2} /></span>
            <div className="min-w-0">
                <div className="text-[11px] opacity-70">{client ? 'Prepared for' : 'Prepared by'}</div>
                <div className={cn('truncate font-medium leading-tight', tall ? 'text-[22px]' : 'text-[16px]')} style={{ ...hf, letterSpacing: '-0.01em' }}>{who}</div>
                {sub && <div className="truncate text-[11.5px] opacity-70">{sub}</div>}
                {tall && owner && <div className="mt-4 border-t pt-3 text-[11.5px] opacity-80" style={{ borderColor: 'rgb(255 255 255 / 0.18)' }}>By {owner.name}{owner.job_title ? `, ${owner.job_title}` : ''}</div>}
            </div>
        </div>
    );
}

function CoverText({ p, hf, quote, title, subtitle, big }: CoverProps & { big: number }) {
    return (
        <>
            <Eyebrow p={p} hf={hf}>{quote.title_label} {quote.number} · {date(quote.quote_date)}</Eyebrow>
            <div className="mt-4"><Heading p={p} hf={hf} text={title} size={title.length > 60 ? big * 0.78 : big} as="h1" /></div>
            {subtitle && <p className="mt-4 max-w-[520px] text-[13.5px] leading-[1.65]" style={{ color: p.muted }}>{subtitle}</p>}
        </>
    );
}

function Contents({ p, hf, sections }: Pick<CoverProps, 'p' | 'hf' | 'sections'>) {
    if (sections.length < 2) return null;
    return (
        <div>
            <Eyebrow p={p} hf={hf}>Contents</Eyebrow>
            <div className="mt-3 grid grid-cols-2 gap-x-6 gap-y-3 @lg:grid-cols-3">
                {sections.slice(0, 9).map((s, i) => (
                    <div key={i} className="flex items-center gap-3">
                        <NumberTile p={p} hf={hf} n={i + 1} size={34} />
                        <div className="min-w-0">
                            <div className="truncate text-[12.5px] font-medium" style={hf}>{s.title}</div>
                            <div className="text-[10.5px]" style={{ color: p.muted }}>Page {s.page}</div>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
}

function Cover(props: CoverProps) {
    return (
        <div className="@container relative flex h-full flex-col">
            <CoverText {...props} big={46} />
            <HeroPanel {...props} className="mt-9 h-[380px] shrink-0" />
            <div className="mt-auto pb-1"><Contents {...props} /></div>
        </div>
    );
}

/* ---------- Web view (desktop + mobile) ---------- */

/** Tracking key for each block: the section it sits in (by heading), or a fixed name for the furniture. */
function sectionKeys(blocks: Block[]): string[] {
    let current = 'Introduction';
    return blocks.map((b) => {
        if (b.section !== undefined && b.title) current = b.title;
        if (b.id === 'totals' || b.id === 'no-money') return 'Pricing';
        if (b.id.startsWith('terms')) return 'Terms & conditions';
        if (b.id === 'closing') return 'Closing';
        return current.slice(0, 80);
    });
}

function WebView(props: CoverProps & { blocks: Block[]; after?: ReactNode }) {
    const { p, hf, company, owner, quote, blocks, after } = props;
    const keys = useMemo(() => sectionKeys(blocks), [blocks]);
    const contact = contactHref(owner, company);
    return (
        <div className="motion-page min-h-screen pb-16" style={{ background: p.page }} data-testid="preview-web">
            <header className="mx-auto flex max-w-[1080px] items-center justify-between gap-4 px-4 py-6 sm:px-8">
                <Brand p={p} hf={hf} company={company} />
                {contact && <a href={contact} className="text-[14px] font-bold underline decoration-2 underline-offset-4" style={{ ...hf, color: p.text }}>Contact us</a>}
            </header>
            <section className="mx-auto grid max-w-[1080px] items-center gap-10 px-4 pb-8 sm:px-8 md:grid-cols-[1fr_1.05fr]" data-sk="Cover">
                <div className="pt-4 md:order-first md:pt-0"><CoverText {...props} big={40} /></div>
                <HeroPanel {...props} className="h-[300px] md:aspect-[690/380] md:h-auto" />
            </section>
            <main className="@container mx-auto max-w-[820px] px-4 sm:px-8">
                {blocks.map((b, i) => <div key={b.id} style={{ display: 'flow-root' }} data-sk={keys[i]}>{b.render(true)}</div>)}
                {after}
            </main>
            <footer className="mx-auto mt-10 flex max-w-[820px] items-center justify-between gap-4 px-4 text-[11px] sm:px-8" style={{ color: p.muted }}>
                <span>{quote.title_label} {quote.number}</span>
                <span className="inline-flex items-center gap-1.5"><ScrollText className="h-3.5 w-3.5" aria-hidden />{company.name}{company.abn ? ` · ABN ${company.abn}` : ''}</span>
            </footer>
        </div>
    );
}
