import { Link } from '@inertiajs/react';
import { ArrowDown, ArrowUp, Blocks as BlocksIcon, ChevronDown, ChevronRight, Copy, GripVertical, ImageIcon, LayoutTemplate, Paintbrush, Plus, Search, Trash2, Upload } from 'lucide-react';
import { BLOCK_META, defaultContent, isStructured, STRUCTURED_TYPES, StructuredBlockEditor, StylePanel, type BlockContent, type BlockStyle, type StructuredType } from '@/components/quote/BlockEditors';
import { useMemo, useRef, useState } from 'react';
import { AnimatedList, Reveal } from '@/components/motion';
import { MoneyInput, PercentInput, QtyInput } from '@/components/NumberInputs';
import { RichTextEditor } from '@/components/RichTextEditor';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input, Select } from '@/components/ui/input';
import { Field, FieldError, Label } from '@/components/ui/label';
import { money } from '@/lib/format';
import { xsrfToken } from '@/lib/http';
import type { TaxMode, TotalsLine, TotalsResult } from '@/lib/totals';
import { cn } from '@/lib/utils';

/**
 * The line editor shared by quotes and template sections: price, text, image and
 * subtotal lines, library lookup, template insertion, reordering, per-line options.
 */
export type LineType = 'price' | 'text' | 'subtotal' | 'image' | StructuredType;
export type ImageSize = 'full' | 'half' | 'small';

export type Line = {
    key: string;
    id?: number;
    type: LineType;
    item_id: number | null;
    code: string | null;
    heading: string | null;
    description: string | null;
    content?: BlockContent | null;
    style?: BlockStyle | null;
    /** Set when a template copied from another company referenced an item code this company doesn't have. */
    missing_item?: boolean;
    image_path?: string | null;
    image_url?: string | null;
    image_alt?: string | null;
    image_size?: ImageSize | null;
    qty: string;
    unit_price_cents: number;
    cost_cents: number | null;
    markup_bp: number | null;
    discount_bp: number;
    tax_rate_bp: number;
    optional: boolean;
    recommended: boolean;
    selected: boolean;
    choice_group: string | null;
    qty_editable: boolean;
    subscription_interval: 'week' | 'month' | 'year' | null;
    subscription_every: number | null;
    subscription_term: number | null;
};

export type LibraryItem = {
    id: number; type: 'price' | 'text' | 'image' | StructuredType; code: string | null; heading: string; description: string | null;
    image_path?: string | null; image_url?: string | null; image_alt?: string | null; image_size?: ImageSize | null;
    content?: BlockContent | null; style?: BlockStyle | null;
    unit_price_cents: number; default_qty: string; discount_bp: number; tax_rate_bp: number;
    cost_cents: number | null; markup_bp: number | null;
    subscription_interval: Line['subscription_interval']; subscription_every: number | null; subscription_term: number | null;
};

export type TemplateOption = { id: number; name: string; description: string | null; lines: Omit<Line, 'key'>[] };

let seq = 0;
export const newKey = () => `n${++seq}`;

export function blankLine(type: LineType, taxBp: number): Line {
    return {
        key: newKey(), type, item_id: null, code: null, heading: type === 'price' ? '' : null, description: null,
        content: isStructured(type) ? defaultContent(type) : null, style: null,
        image_path: null, image_url: null, image_alt: null, image_size: type === 'image' ? 'full' : null,
        qty: '1', unit_price_cents: 0, cost_cents: null, markup_bp: null, discount_bp: 0, tax_rate_bp: type === 'price' ? taxBp : 0,
        optional: false, recommended: false, selected: true, choice_group: null, qty_editable: false,
        subscription_interval: null, subscription_every: null, subscription_term: null,
    };
}

export function withKeys(lines: Omit<Line, 'key'>[]): Line[] {
    return lines.map((l) => ({ ...l, key: newKey() }));
}

export function stripKeys(lines: Line[]): Omit<Line, 'key'>[] {
    return lines.map(({ key, ...l }) => l);
}

export function toTotalsLine(l: Line): TotalsLine {
    return {
        type: l.type === 'price' || l.type === 'subtotal' ? l.type : 'text', qty: l.qty || '1', unit_price_cents: l.unit_price_cents, discount_bp: l.discount_bp, tax_rate_bp: l.tax_rate_bp,
        optional: l.optional, selected: l.selected, choice_group: l.choice_group || null, cost_cents: l.cost_cents,
        subscription: l.subscription_interval
            ? { interval: l.subscription_interval, every: l.subscription_every || 1, term: l.subscription_term || null } : null,
    };
}

const stripHtml = (html: string | null) => (html ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();

export function LinesEditor({
    lines, onChange, errors: allErrors, totals, currency, showCodes, taxMode, library, templates, canEditTemplates = false,
    defaultTaxBp, defaultMarkupBp, testId = 'quote-lines', emptyText = 'No items yet. Add one from your item library or start a blank line.', highlight,
}: {
    lines: Line[]; onChange: (lines: Line[]) => void; errors: Record<string, string>; totals: TotalsResult; currency: string;
    showCodes: boolean; taxMode: TaxMode; library: LibraryItem[]; templates?: TemplateOption[]; canEditTemplates?: boolean;
    defaultTaxBp: number; defaultMarkupBp: number; testId?: string; emptyText?: string;
    /** Line indexes to flag (e.g. unfilled smart tags). */
    highlight?: Set<number>;
}) {
    const [open, setOpen] = useState<Record<string, boolean>>({});
    const [styling, setStyling] = useState<Record<string, boolean>>({});
    const [drag, setDrag] = useState<number | null>(null);
    const [over, setOver] = useState<number | null>(null);
    /** Drag a row by its grip onto another row to move it there (the up/down buttons do the same from the keyboard). */
    const dropAt = (to: number) => {
        if (drag === null || drag === to) return;
        const next = [...lines];
        const [moved] = next.splice(drag, 1);
        next.splice(to, 0, moved);
        setLines(next);
    };
    const dnd = (i: number) => ({
        onDragOver: (e: React.DragEvent) => { if (drag !== null) { e.preventDefault(); setOver(i); } },
        onDrop: (e: React.DragEvent) => { e.preventDefault(); dropAt(i); setDrag(null); setOver(null); },
    });
    const choiceGroups = useMemo(() => [...new Set(lines.map((l) => l.choice_group).filter(Boolean) as string[])], [lines]);

    const setLines = onChange;
    const patch = (key: string, p: Partial<Line>) => setLines(lines.map((l) => (l.key === key ? { ...l, ...p } : l)));
    const add = (line: Line, openIt = false) => {
        setLines([...lines, line]);
        if (openIt) setOpen((o) => ({ ...o, [line.key]: true }));
    };
    const move = (i: number, dir: -1 | 1) => {
        const j = i + dir;
        if (j < 0 || j >= lines.length) return;
        const next = [...lines];
        [next[i], next[j]] = [next[j], next[i]];
        setLines(next);
    };
    const remove = (key: string) => setLines(lines.filter((l) => l.key !== key));
    const duplicate = (i: number) => {
        const copy = { ...lines[i], key: newKey(), id: undefined };
        setLines([...lines.slice(0, i + 1), copy, ...lines.slice(i + 1)]);
    };
    /** Picking one option in a multiple-choice group deselects the others. */
    const pick = (key: string, group: string) =>
        setLines(lines.map((l) => (l.choice_group === group ? { ...l, selected: l.key === key } : l)));

    const addFromLibrary = (item: LibraryItem) => {
        const base = blankLine(item.type, defaultTaxBp);
        add({
            ...base, item_id: item.id, code: item.code, heading: item.heading, description: item.description,
            ...(item.type === 'price' ? {
                qty: item.default_qty || '1', unit_price_cents: item.unit_price_cents, discount_bp: item.discount_bp,
                tax_rate_bp: item.tax_rate_bp, cost_cents: item.cost_cents, markup_bp: item.markup_bp,
                subscription_interval: item.subscription_interval, subscription_every: item.subscription_every, subscription_term: item.subscription_term,
            } : {}),
            ...(item.type === 'image' ? { image_path: item.image_path ?? null, image_url: item.image_url ?? null, image_alt: item.image_alt ?? null, image_size: item.image_size ?? 'full' } : {}),
            ...(item.type !== 'price' && item.type !== 'text' && item.type !== 'image' ? { content: item.content ?? base.content } : {}),
            ...(item.type !== 'price' ? { style: item.style ?? null } : {}),
        });
    };
    /** Drop a template section's lines in at the end (fresh copies; the template itself is untouched). */
    const addTemplate = (t: TemplateOption) => setLines([...lines, ...withKeys(t.lines.map(({ id, ...l }) => l))]);

    const err = (path: string) => allErrors[path];
    const lineErr = (i: number) => Object.entries(allErrors).filter(([k]) => k.startsWith(`lines.${i}.`)).map(([, v]) => v);
    const cur = currency;
    const lineResult = (i: number) => totals.lines[i];
    const subtotalFor = (i: number) => totals.subtotals.find((s) => s.index === i)?.amount_cents ?? 0;

    return (
        <>
                            <Reveal show={lines.length === 0}>
                                <p className="px-5 py-8 text-center text-sm text-ink-muted">{emptyText}</p>
                            </Reveal>
                            <AnimatedList className="divide-y divide-line" data-testid={testId}>
                                {lines.map((l, i) => {
                                    const r = lineResult(i);
                                    const errors = lineErr(i);
                                    const isOpen = !!open[l.key];
                                    const toggle = () => setOpen((o) => ({ ...o, [l.key]: !isOpen }));
                                    const rowTools = (
                                        <div className="flex items-center gap-0.5">
                                            <IconBtn label="Block style" pressed={!!styling[l.key]} onClick={() => setStyling((st) => ({ ...st, [l.key]: !st[l.key] }))}><Paintbrush className={cn('h-4 w-4', l.style && 'text-link')} /></IconBtn>
                                            <IconBtn label="Move up" onClick={() => move(i, -1)} disabled={i === 0}><ArrowUp className="h-4 w-4" /></IconBtn>
                                            <IconBtn label="Move down" onClick={() => move(i, 1)} disabled={i === lines.length - 1}><ArrowDown className="h-4 w-4" /></IconBtn>
                                            <IconBtn label="Duplicate line" onClick={() => duplicate(i)}><Copy className="h-4 w-4" /></IconBtn>
                                            <IconBtn label="Remove line" onClick={() => remove(l.key)}><Trash2 className="h-4 w-4" /></IconBtn>
                                        </div>
                                    );

                                    const grip = (
                                        <span draggable aria-hidden title="Drag to reorder" data-testid={`grip-${i}`}
                                            onDragStart={(e) => { setDrag(i); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', String(i)); }}
                                            onDragEnd={() => { setDrag(null); setOver(null); }}
                                            className="-ml-2 cursor-grab rounded p-0.5 text-ink-muted/70 hover:bg-stone-100 hover:text-ink active:cursor-grabbing">
                                            <GripVertical className="h-4 w-4" />
                                        </span>
                                    );
                                    const rowCls = cn(over === i && drag !== null && drag !== i && 'shadow-[inset_0_2px_0_var(--color-accent)]', drag === i && 'opacity-50',
                                        highlight?.has(i) && 'bg-amber-50/70 ring-2 ring-inset ring-amber-400');
                                    const stylePanel = <Reveal show={!!styling[l.key]} innerClassName="pt-3"><StylePanel id={l.key} style={l.style} onChange={(st) => patch(l.key, { style: st })} /></Reveal>;

                                    if (isStructured(l.type)) {
                                        const meta = BLOCK_META[l.type];
                                        const Icon = meta.icon;
                                        return (
                                            <div key={l.key} className={cn('space-y-3 px-5 py-4', rowCls)} data-testid={`line-${i}`} {...dnd(i)}>
                                                <div className="flex items-center gap-2">
                                                    {grip}
                                                    <Badge className="inline-flex items-center gap-1 bg-accent-soft text-link-hover"><Icon className="h-3.5 w-3.5" aria-hidden />{meta.label}</Badge>
                                                    <span className="min-w-0 flex-1 truncate text-sm text-ink-muted">{l.heading || meta.hint}</span>
                                                    {rowTools}
                                                </div>
                                                {stylePanel}
                                                <StructuredBlockEditor type={l.type} id={l.key} heading={l.heading} description={l.description} content={l.content ?? defaultContent(l.type)}
                                                    onChange={(p) => patch(l.key, p)} />
                                                <Reveal show={errors.length > 0}><div>{[...new Set(errors)].map((m) => <FieldError key={m} message={m} />)}</div></Reveal>
                                            </div>
                                        );
                                    }

                                    if (l.type === 'subtotal') {
                                        return (
                                            <div key={l.key} className={cn('flex items-center justify-between gap-3 bg-stone-50 px-5 py-2.5', rowCls)} data-testid={`line-${i}`} {...dnd(i)}>
                                                {grip}
                                                <span className="text-sm font-semibold">Subtotal</span>
                                                <span className="ml-auto text-sm font-semibold tabular-nums">{money(subtotalFor(i), cur)}</span>
                                                {rowTools}
                                            </div>
                                        );
                                    }

                                    if (l.type === 'image') {
                                        return (
                                            <div key={l.key} className={cn('space-y-3 px-5 py-4', rowCls)} data-testid={`line-${i}`} {...dnd(i)}>
                                                <div className="flex items-center gap-2">
                                                    {grip}
                                                    <Badge className="bg-stone-100 text-stone-700">Image</Badge>
                                                    <Input aria-label="Image caption" placeholder="Caption (optional)" value={l.heading ?? ''} onChange={(e) => patch(l.key, { heading: e.target.value })} />
                                                    {rowTools}
                                                </div>
                                                {stylePanel}
                                                <ImageBlock line={l} onChange={(p) => patch(l.key, p)} error={err(`lines.${i}.image_path`)} />
                                            </div>
                                        );
                                    }

                                    if (l.type === 'text') {
                                        return (
                                            <div key={l.key} className={cn('space-y-2 px-5 py-4', rowCls)} data-testid={`line-${i}`} {...dnd(i)}>
                                                <div className="flex items-center gap-2">
                                                    {grip}
                                                    <Badge className="bg-stone-100 text-stone-700">Text</Badge>
                                                    <Input aria-label="Text heading" placeholder="Heading (optional)" value={l.heading ?? ''} onChange={(e) => patch(l.key, { heading: e.target.value })} />
                                                    {rowTools}
                                                </div>
                                                {stylePanel}
                                                <RichTextEditor label="Text" value={l.description ?? ''} onChange={(h) => patch(l.key, { description: h })} />
                                            </div>
                                        );
                                    }

                                    const excluded = r && !r.included;
                                    return (
                                        <div key={l.key} className={cn('px-5 py-4', excluded && 'bg-stone-50/70', rowCls)} data-testid={`line-${i}`} {...dnd(i)}>
                                            <div className="flex items-center gap-2">
                                                {grip}
                                                <button type="button" onClick={toggle} aria-expanded={isOpen} aria-label={isOpen ? 'Hide line options' : 'Show line options'}
                                                    className="rounded p-1 text-ink-muted hover:bg-stone-100">
                                                    {isOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
                                                </button>
                                                {(l.choice_group || l.optional) && (
                                                    l.choice_group
                                                        ? <input type="radio" name={`choice-${l.choice_group}`} checked={l.selected} onChange={() => pick(l.key, l.choice_group!)}
                                                            aria-label={`Select ${l.heading || 'this option'} in ${l.choice_group}`} className="h-4 w-4 accent-[var(--color-link)]" />
                                                        : <input type="checkbox" checked={l.selected} onChange={(e) => patch(l.key, { selected: e.target.checked })}
                                                            aria-label={`Include ${l.heading || 'optional item'}`} className="h-4 w-4 accent-[var(--color-link)]" />
                                                )}
                                                <div className="min-w-0 flex-1">
                                                    <Input aria-label="Item heading" placeholder="Item heading" value={l.heading ?? ''}
                                                        onChange={(e) => patch(l.key, { heading: e.target.value })} aria-invalid={!!err(`lines.${i}.heading`) || undefined}
                                                        className={cn('font-medium', err(`lines.${i}.heading`) && 'border-danger')} />
                                                </div>
                                                {rowTools}
                                            </div>
                                            <div className="mt-2 grid grid-cols-2 items-end gap-2 pl-8 sm:grid-cols-[5.5rem_9rem_5.5rem_minmax(0,1fr)]">
                                                <div>
                                                    <Label className="text-xs text-ink-muted" htmlFor={`qty-${l.key}`}>Qty</Label>
                                                    <QtyInput id={`qty-${l.key}`} value={l.qty} onChange={(qty) => patch(l.key, { qty })} invalid={!!err(`lines.${i}.qty`)} />
                                                </div>
                                                <div>
                                                    <Label className="text-xs text-ink-muted" htmlFor={`price-${l.key}`}>Unit price</Label>
                                                    <MoneyInput id={`price-${l.key}`} required cents={l.unit_price_cents} onChange={(c) => patch(l.key, { unit_price_cents: c ?? 0 })} invalid={!!err(`lines.${i}.unit_price_cents`)} />
                                                </div>
                                                <div>
                                                    <Label className="text-xs text-ink-muted" htmlFor={`disc-${l.key}`}>Discount</Label>
                                                    <PercentInput id={`disc-${l.key}`} bp={l.discount_bp} onChange={(bp) => patch(l.key, { discount_bp: bp })} />
                                                </div>
                                                <div className="pb-2 text-right">
                                                    <div className="text-xs text-ink-muted">{l.subscription_interval ? subLabel(l) : 'Line total'}{taxMode === 'inclusive' ? ' (inc GST)' : ''}</div>
                                                    <div className={cn('text-sm font-semibold tabular-nums', excluded && 'text-ink-muted line-through')} data-testid={`line-total-${i}`}>
                                                        {money(r?.net_cents ?? 0, cur)}
                                                    </div>
                                                </div>
                                            </div>

                                            <div className="mt-2 flex flex-wrap items-center gap-1.5 pl-8">
                                                {l.code && showCodes && <Badge className="bg-stone-100 text-stone-700">{l.code}</Badge>}
                                                {l.optional && <Badge className="bg-sky-50 text-sky-800">Optional{l.recommended ? ' · recommended' : ''}</Badge>}
                                                {l.choice_group && <Badge className="bg-violet-50 text-violet-800">Choice: {l.choice_group}</Badge>}
                                                {l.qty_editable && <Badge className="bg-stone-100 text-stone-700">Client can edit qty</Badge>}
                                                {l.missing_item && <Badge className="bg-amber-50 text-amber-900">Missing in this company{l.code ? ` (${l.code})` : ''}</Badge>}
                                                {l.tax_rate_bp === 0 && taxMode !== 'none' && <Badge className="bg-stone-100 text-stone-700">GST-free</Badge>}
                                                {!isOpen && l.description && <span className="truncate text-xs text-ink-muted">{stripHtml(l.description).slice(0, 120)}</span>}
                                            </div>

                                            <div className="md:ml-8">{stylePanel}</div>
                                            <Reveal show={errors.length > 0}>
                                                <div className="pl-8">{[...new Set(errors)].map((m) => <FieldError key={m} message={m} />)}</div>
                                            </Reveal>

                                            <Reveal show={isOpen} innerClassName="pt-4">
                                                <div className="space-y-4 rounded-md border border-line bg-stone-50/60 p-4 md:ml-8">
                                                    <RichTextEditor label="Item description" value={l.description ?? ''} onChange={(h) => patch(l.key, { description: h })} />
                                                    <div className="grid gap-3 sm:grid-cols-3">
                                                        <Field label="Item code" htmlFor={`code-${l.key}`}>
                                                            <Input id={`code-${l.key}`} value={l.code ?? ''} onChange={(e) => patch(l.key, { code: e.target.value || null })} />
                                                        </Field>
                                                        <Field label="Tax" htmlFor={`tax-${l.key}`}>
                                                            <Select id={`tax-${l.key}`} value={l.tax_rate_bp === 1000 ? '1000' : l.tax_rate_bp === 0 ? '0' : 'custom'}
                                                                onChange={(e) => patch(l.key, { tax_rate_bp: e.target.value === 'custom' ? l.tax_rate_bp || 1500 : Number(e.target.value) })}>
                                                                <option value="1000">GST 10%</option>
                                                                <option value="0">GST-free</option>
                                                                <option value="custom">Other rate</option>
                                                            </Select>
                                                        </Field>
                                                        <Reveal show={l.tax_rate_bp !== 1000 && l.tax_rate_bp !== 0}>
                                                            <Field label="Tax rate" htmlFor={`taxrate-${l.key}`}>
                                                                <PercentInput id={`taxrate-${l.key}`} bp={l.tax_rate_bp} onChange={(bp) => patch(l.key, { tax_rate_bp: bp })} />
                                                            </Field>
                                                        </Reveal>
                                                    </div>

                                                    <fieldset className="grid gap-3 sm:grid-cols-3">
                                                        <legend className="mb-2 text-sm font-medium">Client options</legend>
                                                        <div className="space-y-2">
                                                            <Checkbox label="Optional item" checked={l.optional} disabled={!!l.choice_group}
                                                                onChange={(e) => patch(l.key, { optional: e.target.checked, selected: e.target.checked ? l.recommended : true })} />
                                                            <Reveal show={l.optional}>
                                                                <Checkbox label="Recommended (pre-selected)" checked={l.recommended}
                                                                    onChange={(e) => patch(l.key, { recommended: e.target.checked, selected: e.target.checked })} />
                                                            </Reveal>
                                                            <Checkbox label="Client can edit quantity" checked={l.qty_editable} onChange={(e) => patch(l.key, { qty_editable: e.target.checked })} />
                                                        </div>
                                                        <Field label="Multiple-choice group" htmlFor={`group-${l.key}`} hint="Lines sharing a group name: the client picks one." className="sm:col-span-2">
                                                            <Input id={`group-${l.key}`} list={`groups-${l.key}`} value={l.choice_group ?? ''} placeholder="e.g. Support plan"
                                                                onChange={(e) => {
                                                                    const g = e.target.value.trim() ? e.target.value : null;
                                                                    const firstInGroup = g !== null && !lines.some((x) => x.key !== l.key && x.choice_group === g);
                                                                    patch(l.key, { choice_group: g, optional: g ? false : l.optional, selected: g ? firstInGroup : true });
                                                                }} />
                                                            <datalist id={`groups-${l.key}`}>{choiceGroups.map((g) => <option key={g} value={g} />)}</datalist>
                                                        </Field>
                                                    </fieldset>

                                                    <fieldset className="grid gap-3 sm:grid-cols-3">
                                                        <legend className="mb-2 text-sm font-medium">Billing</legend>
                                                        <Field label="Repeats" htmlFor={`int-${l.key}`}>
                                                            <Select id={`int-${l.key}`} value={l.subscription_interval ?? ''}
                                                                onChange={(e) => {
                                                                    const v = (e.target.value || null) as Line['subscription_interval'];
                                                                    patch(l.key, { subscription_interval: v, subscription_every: v ? l.subscription_every || 1 : null, subscription_term: v ? l.subscription_term : null });
                                                                }}>
                                                                <option value="">One-off</option>
                                                                <option value="week">Weekly</option>
                                                                <option value="month">Monthly</option>
                                                                <option value="year">Yearly</option>
                                                            </Select>
                                                        </Field>
                                                        <Reveal show={!!l.subscription_interval} className="sm:col-span-2" innerClassName="grid gap-3 sm:grid-cols-2">
                                                            <Field label={`Every N ${l.subscription_interval ?? 'month'}s`} htmlFor={`every-${l.key}`}>
                                                                <Input id={`every-${l.key}`} type="number" min={1} max={52} value={l.subscription_every ?? 1}
                                                                    onChange={(e) => patch(l.key, { subscription_every: Math.max(1, Number(e.target.value) || 1) })} />
                                                            </Field>
                                                            <Field label="For how many periods" htmlFor={`term-${l.key}`} hint="Leave blank for ongoing.">
                                                                <Input id={`term-${l.key}`} type="number" min={1} max={999} value={l.subscription_term ?? ''}
                                                                    onChange={(e) => patch(l.key, { subscription_term: Number(e.target.value) || null })} />
                                                            </Field>
                                                        </Reveal>
                                                    </fieldset>

                                                    <fieldset className="grid gap-3 rounded-md border border-dashed border-line-strong p-3 sm:grid-cols-3">
                                                        <legend className="px-1 text-sm font-medium">Staff only <span className="font-normal text-ink-muted">· never shown to clients</span></legend>
                                                        <Field label="Cost price" htmlFor={`cost-${l.key}`}>
                                                            <MoneyInput id={`cost-${l.key}`} allowNegative={false} cents={l.cost_cents} onChange={(c) => patch(l.key, { cost_cents: c })} />
                                                        </Field>
                                                        <Field label="Markup" htmlFor={`markup-${l.key}`} hint="Sets the unit price from cost.">
                                                            <PercentInput id={`markup-${l.key}`} max={10000} bp={l.markup_bp ?? defaultMarkupBp}
                                                                onChange={(bp) => patch(l.key, { markup_bp: bp, ...(l.cost_cents !== null ? { unit_price_cents: Math.round(l.cost_cents * (1 + bp / 10000)) } : {}) })} />
                                                        </Field>
                                                        <div>
                                                            <div className="mb-1 text-sm font-medium">Line margin</div>
                                                            <div className="pt-2 text-sm tabular-nums">
                                                                {l.cost_cents === null ? '—' : lineMargin(l, r?.net_cents ?? 0, cur)}
                                                            </div>
                                                        </div>
                                                    </fieldset>
                                                </div>
                                            </Reveal>
                                        </div>
                                    );
                                })}
                            </AnimatedList>
                            <div className="flex flex-wrap items-center gap-2 border-t border-line px-5 py-4">
                                <LibraryPicker library={library} onPick={addFromLibrary} currency={cur} />
                                <Button type="button" variant="secondary" size="sm" onClick={() => add(blankLine('price', defaultTaxBp), true)}><Plus className="h-4 w-4" /> Price item</Button>
                                <Button type="button" variant="secondary" size="sm" onClick={() => add(blankLine('text', 0))}><Plus className="h-4 w-4" /> Text</Button>
                                <Button type="button" variant="secondary" size="sm" onClick={() => add(blankLine('image', 0))} data-testid="add-image"><ImageIcon className="h-4 w-4" /> Image</Button>
                                <Button type="button" variant="secondary" size="sm" onClick={() => add(blankLine('subtotal', 0))}><Plus className="h-4 w-4" /> Subtotal</Button>
                                <BlockMenu onPick={(t) => add(blankLine(t, 0))} />
                                {templates && <TemplatePicker templates={templates} onPick={addTemplate} canEdit={canEditTemplates} />}
                            </div>
        </>
    );
}

function subLabel(l: Line): string {
    const every = l.subscription_every || 1;
    const base = every === 1 ? `Per ${l.subscription_interval}` : `Every ${every} ${l.subscription_interval}s`;
    return l.subscription_term ? `${base} × ${l.subscription_term}` : base;
}

function lineMargin(l: Line, netCents: number, currency: string): string {
    const q = Number(l.qty) || 0;
    const cost = Math.round((l.cost_cents ?? 0) * q);
    const m = netCents - cost;
    return `${money(m, currency)}${netCents ? ` (${((m / netCents) * 100).toFixed(1)}%)` : ''}`;
}

function IconBtn({ label, onClick, disabled, pressed, children }: { label: string; onClick: () => void; disabled?: boolean; pressed?: boolean; children: React.ReactNode }) {
    return (
        <button type="button" onClick={onClick} disabled={disabled} aria-label={label} title={label} aria-pressed={pressed}
            className="rounded p-1.5 text-ink-muted hover:bg-stone-100 hover:text-ink disabled:opacity-30">{children}</button>
    );
}


/** Lookup-as-you-type over the company's item library (code or heading). */
function LibraryPicker({ library, onPick, currency }: { library: LibraryItem[]; onPick: (i: LibraryItem) => void; currency: string }) {
    const [q, setQ] = useState('');
    const [open, setOpen] = useState(false);
    const [active, setActive] = useState(0);
    const ref = useRef<HTMLInputElement>(null);
    const matches = useMemo(() => {
        const t = q.trim().toLowerCase();
        return (t ? library.filter((i) => (i.code ?? '').toLowerCase().includes(t) || i.heading.toLowerCase().includes(t)) : library).slice(0, 12);
    }, [q, library]);
    const choose = (i: LibraryItem) => { onPick(i); setQ(''); setOpen(false); ref.current?.focus(); };

    return (
        <div className="relative w-full sm:w-80">
            <Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-ink-muted" aria-hidden />
            <Input ref={ref} role="combobox" aria-expanded={open} aria-controls="library-list" aria-autocomplete="list"
                aria-label="Add from item library" placeholder="Add from library: code or name…" className="pl-8" value={q}
                onChange={(e) => { setQ(e.target.value); setOpen(true); setActive(0); }}
                onFocus={() => setOpen(true)}
                onBlur={() => setTimeout(() => setOpen(false), 150)}
                onKeyDown={(e) => {
                    if (e.key === 'ArrowDown') { e.preventDefault(); setActive((a) => Math.min(a + 1, matches.length - 1)); }
                    else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); }
                    else if (e.key === 'Enter') { e.preventDefault(); if (matches[active]) choose(matches[active]); }
                    else if (e.key === 'Escape') setOpen(false);
                }} />
            {open && (
                <ul id="library-list" role="listbox" className="motion-pop-up absolute bottom-full z-20 mb-1 max-h-72 w-full overflow-auto rounded-md border border-line bg-surface py-1 shadow-lg sm:w-96">
                    {matches.length === 0 && <li className="px-3 py-2 text-sm text-ink-muted">No library items match.</li>}
                    {matches.map((i, idx) => (
                        <li key={i.id} role="option" aria-selected={idx === active}
                            onMouseDown={(e) => { e.preventDefault(); choose(i); }} onMouseEnter={() => setActive(idx)}
                            className={cn('flex cursor-pointer items-baseline justify-between gap-3 px-3 py-2 text-sm', idx === active && 'bg-accent-soft')}>
                            <span className="min-w-0 truncate">{i.code && <span className="mr-1.5 font-mono text-xs text-ink-muted">{i.code}</span>}{i.heading}</span>
                            <span className="shrink-0 tabular-nums text-ink-muted">{i.type === 'price' ? money(i.unit_price_cents, currency) : i.type === 'text' ? 'Text' : i.type === 'image' ? 'Image' : BLOCK_META[i.type as StructuredType]?.label}</span>
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
}


/** Suggested pixel widths per display size (the proposal page is ~800 px wide, ≈170 mm on A4). */
export const IMAGE_GUIDE: Record<ImageSize, { label: string; px: number; hint: string }> = {
    full: { label: 'Full width', px: 1600, hint: '1600 × 900 px (16:9) or taller; fills the page width' },
    half: { label: 'Half width', px: 800, hint: '800 × 600 px (4:3); sits at half the page width' },
    small: { label: 'Small', px: 400, hint: '400 × 400 px; good for logos and badges' },
};

/** Upload / replace an image for an image line, with sizing guidance and a low-resolution warning. */
export function ImageBlock({ line, onChange, error }: { line: Line; onChange: (p: Partial<Line>) => void; error?: string }) {
    const input = useRef<HTMLInputElement>(null);
    const [busy, setBusy] = useState(false);
    const [problem, setProblem] = useState<string | null>(null);
    const [dims, setDims] = useState<{ w: number; h: number } | null>(null);
    const size: ImageSize = line.image_size ?? 'full';
    const guide = IMAGE_GUIDE[size];

    const upload = async (file: File) => {
        setBusy(true);
        setProblem(null);
        try {
            const body = new FormData();
            body.append('image', file);
            const res = await fetch('/api/uploads/images', {
                method: 'POST', body, credentials: 'same-origin',
                headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-XSRF-TOKEN': xsrfToken() },
            });
            const data = await res.json().catch(() => ({}));
            if (!res.ok) throw new Error(data?.errors?.image?.[0] ?? data?.message ?? 'Upload failed.');
            setDims(data.width ? { w: data.width, h: data.height } : null);
            onChange({ image_path: data.path, image_url: data.url, image_alt: line.image_alt || file.name.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ') });
        } catch (e) {
            setProblem(e instanceof Error ? e.message : 'Upload failed.');
        } finally {
            setBusy(false);
            if (input.current) input.current.value = '';
        }
    };

    const lowRes = dims && dims.w < guide.px;
    const width = size === 'full' ? 'w-full' : size === 'half' ? 'w-1/2' : 'w-1/4';

    return (
        <div className="space-y-3">
            <input ref={input} type="file" accept="image/jpeg,image/png,image/webp,image/gif" className="sr-only" aria-label="Upload image"
                onChange={(e) => { const f = e.target.files?.[0]; if (f) void upload(f); }} />
            {line.image_url ? (
                <div className="rounded-md border border-line bg-stone-50 p-3">
                    <div className={cn('mx-auto', width)}>
                        <img src={line.image_url} alt={line.image_alt ?? ''} className="mx-auto h-auto max-h-72 w-auto max-w-full rounded object-contain" />
                    </div>
                </div>
            ) : (
                <button type="button" onClick={() => input.current?.click()} disabled={busy}
                    className="flex w-full flex-col items-center justify-center gap-1 rounded-md border-2 border-dashed border-line-strong bg-stone-50 px-4 py-8 text-sm text-ink-muted hover:border-link hover:text-ink">
                    <Upload className="h-5 w-5" aria-hidden />
                    <span className="font-medium">{busy ? 'Uploading…' : 'Choose an image to upload'}</span>
                    <span className="text-xs">JPG, PNG, WebP or GIF · up to 10 MB</span>
                </button>
            )}
            <div className="grid gap-3 sm:grid-cols-3">
                <Field label="Display size" htmlFor={`imgsize-${line.key}`}>
                    <Select id={`imgsize-${line.key}`} value={size} onChange={(e) => onChange({ image_size: e.target.value as ImageSize })}>
                        {(Object.keys(IMAGE_GUIDE) as ImageSize[]).map((k) => <option key={k} value={k}>{IMAGE_GUIDE[k].label}</option>)}
                    </Select>
                </Field>
                <Field label="Alt text" htmlFor={`imgalt-${line.key}`} hint="Describes the image for screen readers." className="sm:col-span-2">
                    <Input id={`imgalt-${line.key}`} value={line.image_alt ?? ''} onChange={(e) => onChange({ image_alt: e.target.value })} />
                </Field>
            </div>
            <p className="text-xs text-ink-muted" data-testid="image-guide">
                <strong className="font-semibold text-ink">Suggested size:</strong> {guide.hint}. Images are scaled to fit, so larger is fine; A4 PDFs print them at up to ~170 mm wide.
            </p>
            <Reveal show={!!lowRes}>
                <p className="rounded-md bg-amber-50 px-3 py-2 text-sm text-amber-900">
                    This image is {dims?.w} px wide, so it may look blurry at {guide.label.toLowerCase()}. Use one at least {guide.px} px wide, or choose a smaller display size.
                </p>
            </Reveal>
            <Reveal show={!!(problem || error)}><FieldError message={problem ?? error} /></Reveal>
            {line.image_url && (
                <div className="flex gap-2">
                    <Button type="button" size="sm" variant="secondary" onClick={() => input.current?.click()} disabled={busy}><Upload className="h-4 w-4" /> {busy ? 'Uploading…' : 'Replace image'}</Button>
                    <Button type="button" size="sm" variant="ghost" onClick={() => { setDims(null); onChange({ image_path: null, image_url: null }); }}>Remove image</Button>
                </div>
            )}
        </div>
    );
}

/** "Add template" menu listing the company's template sections. */
function TemplatePicker({ templates, onPick, canEdit }: { templates: TemplateOption[]; onPick: (t: TemplateOption) => void; canEdit: boolean }) {
    const [open, setOpen] = useState(false);
    return (
        <div className="relative" onBlur={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setOpen(false); }}>
            <Button type="button" variant="secondary" size="sm" aria-expanded={open} aria-haspopup="listbox" onClick={() => setOpen(!open)} data-testid="add-template">
                <LayoutTemplate className="h-4 w-4" /> Template <ChevronDown className="h-3.5 w-3.5" />
            </Button>
            {open && (
                <div className="motion-pop-up absolute bottom-full right-0 z-20 mb-1 w-80 overflow-hidden rounded-md border border-line bg-surface shadow-lg" role="listbox" aria-label="Template sections">
                    {templates.length === 0 && (
                        <p className="px-3 py-3 text-sm text-ink-muted">
                            No template sections yet.{canEdit && <> <Link href="/templates" className="link">Create one</Link>.</>}
                        </p>
                    )}
                    <ul className="max-h-72 overflow-auto py-1">
                        {templates.map((t) => (
                            <li key={t.id}>
                                <button type="button" role="option" aria-selected={false} className="block w-full px-3 py-2 text-left hover:bg-accent-soft focus:bg-accent-soft focus:outline-none"
                                    onClick={() => { onPick(t); setOpen(false); }}>
                                    <span className="block text-sm font-medium">{t.name}</span>
                                    <span className="block truncate text-xs text-ink-muted">{t.description || `${t.lines.length} ${t.lines.length === 1 ? 'block' : 'blocks'}`}</span>
                                </button>
                            </li>
                        ))}
                    </ul>
                </div>
            )}
        </div>
    );
}

/** "More blocks" menu: cover, video, testimonial, team, timeline, case study, T&Cs, acceptance. */
export function BlockMenu({ onPick, below = false, testId = 'add-block' }: { onPick: (t: StructuredType) => void; below?: boolean; testId?: string }) {
    const [open, setOpen] = useState(false);
    return (
        <div className="relative" onBlur={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setOpen(false); }}>
            <Button type="button" variant="secondary" size="sm" aria-expanded={open} aria-haspopup="menu" onClick={() => setOpen(!open)} data-testid={testId}>
                <BlocksIcon className="h-4 w-4" /> More blocks <ChevronDown className="h-3.5 w-3.5" />
            </Button>
            {open && (
                <div role="menu" className={cn('motion-pop-up absolute z-20 grid w-[22rem] grid-cols-2 gap-1 rounded-md border border-line bg-surface p-1.5 shadow-lg', below ? 'right-0 top-full mt-1' : 'bottom-full left-0 mb-1')}>
                    {STRUCTURED_TYPES.map((t) => {
                        const m = BLOCK_META[t];
                        const Icon = m.icon;
                        return (
                            <button key={t} type="button" role="menuitem" onClick={() => { onPick(t); setOpen(false); }}
                                className="flex items-start gap-2 rounded px-2 py-2 text-left hover:bg-accent-soft focus:bg-accent-soft focus:outline-none">
                                <Icon className="mt-0.5 h-4 w-4 shrink-0 text-link" aria-hidden />
                                <span><span className="block text-sm font-medium">{m.label}</span><span className="block text-[11px] leading-tight text-ink-muted">{m.hint}</span></span>
                            </button>
                        );
                    })}
                </div>
            )}
        </div>
    );
}
