// PhotoGallery — an entity's images as an ORDERED gallery (migration 0056):
// the first photo is the cover, drag to reorder, click a label to caption.
// Industry shape (MLS/Zillow) chosen 2026-08-20 over fixed slots ("Bedroom 1",
// "Bathroom 1"…) and over filename conventions — see the migration header.
//
// Core component, not pack-only: ordering/captions are generic attachment
// features; the realtor property modal is simply the first host. Hosts that
// mount this should mount AttachmentsPanel with exclude="images" beside it
// so a photo never appears in two lists.
//
// Images load through api.attachmentImageUrl (fetch + object URL) because an
// <img src> can't carry the bearer header. Object URLs are revoked on unmount.
//
// Every button is type="button" ON PURPOSE — this renders inside host <form>s
// (the property modal), where a bare <button> would submit the form.
const PhotoGallery = ({ entityType, entityId, title = 'Photos' }) => {
    const [photos, setPhotos]       = React.useState([]);
    const [loading, setLoading]     = React.useState(true);
    const [uploading, setUploading] = React.useState(false);
    const [dragOver, setDragOver]   = React.useState(false);   // file drop zone
    const [error, setError]         = React.useState(null);
    const [urls, setUrls]           = React.useState({});      // id → object URL
    // Tile drag state (HTML5 DnD, no library): which tile is lifted, and where
    // it would land (index + before/after) so the drop marker can render.
    const [dragId, setDragId]       = React.useState(null);
    const [dropAt, setDropAt]       = React.useState(null);    // { id, side }
    // Caption being edited: a text input inside a draggable tile can't select
    // text (the browser starts a drag instead), so the tile stops being
    // draggable while its caption has focus.
    const [editing, setEditing]     = React.useState(null);
    const fileInputRef = React.useRef(null);
    const urlsRef      = React.useRef({});

    const isImage = (a) => (a.mime_type || '').startsWith('image/');

    const load = React.useCallback(() => {
        return api.getAttachments(entityType, entityId)
            .then(list => setPhotos(list.filter(isImage)))
            .catch(err => setError('Could not load photos: ' + err.message))
            .finally(() => setLoading(false));
    }, [entityType, entityId]);

    React.useEffect(() => { setLoading(true); load(); }, [load]);

    // Fetch thumbnails for any photo we don't have a URL for yet. Sequential
    // on purpose: a 60-photo listing shouldn't open 60 parallel requests.
    React.useEffect(() => {
        let cancelled = false;
        (async () => {
            for (const p of photos) {
                if (cancelled) return;
                if (urlsRef.current[p.id]) continue;
                try {
                    const u = await api.attachmentImageUrl(p.id);
                    if (cancelled) { URL.revokeObjectURL(u); return; }
                    urlsRef.current = { ...urlsRef.current, [p.id]: u };
                    setUrls(urlsRef.current);
                } catch { /* tile shows a placeholder; the list still works */ }
            }
        })();
        return () => { cancelled = true; };
    }, [photos]);

    // Release every object URL when the gallery unmounts (memory, not correctness).
    React.useEffect(() => () => {
        Object.values(urlsRef.current).forEach(u => URL.revokeObjectURL(u));
    }, []);

    const handleFiles = async (files) => {
        if (!files?.length) return;
        setUploading(true); setError(null);
        try {
            for (const file of Array.from(files)) {
                if (!(file.type || '').startsWith('image/')) {
                    throw new Error(`"${file.name}" isn't an image — documents go in the Documents list below.`);
                }
                await api.uploadAttachment(entityType, entityId, file);
            }
            await load();
        } catch (err) {
            setError('Upload failed: ' + err.message);
        } finally {
            setUploading(false);
        }
    };

    // Persist a new order: optimistic local update, server is the authority —
    // on failure reload so the grid shows what's actually saved.
    const saveOrder = async (next) => {
        setPhotos(next);
        try { await api.reorderAttachments(entityType, entityId, next.map(p => p.id)); }
        catch (err) { setError('Could not save order: ' + err.message); load(); }
    };

    const makeCover = (id) => {
        const idx = photos.findIndex(p => p.id === id);
        if (idx <= 0) return;
        const next = [...photos];
        const [p] = next.splice(idx, 1);
        next.unshift(p);
        saveOrder(next);
    };

    const moveTo = (fromId, toId, side) => {
        if (fromId === toId) return;
        const next = [...photos];
        const from = next.findIndex(p => p.id === fromId);
        const [p] = next.splice(from, 1);
        let to = next.findIndex(q => q.id === toId);
        if (side === 'after') to += 1;
        next.splice(to, 0, p);
        saveOrder(next);
    };

    const handleDelete = async (p) => {
        if (!await confirmAction({
            title: 'Delete photo',
            message: `"${p.original_name}" will be removed from this record and from disk. This cannot be undone.`,
            confirmLabel: 'Delete photo',
        })) return;
        setError(null);
        try { await api.deleteAttachment(p.id); load(); }
        catch (err) { setError(err.message); }
    };

    // Caption saves on blur / Enter; blank clears. Local echo keeps typing smooth.
    const setCaptionLocal = (id, caption) =>
        setPhotos(ps => ps.map(p => p.id === id ? { ...p, caption } : p));
    const saveCaption = async (p) => {
        const caption = (p.caption || '').trim() || null;
        try { await api.updateAttachment(p.id, { caption }); }
        catch (err) { setError('Could not save label: ' + err.message); load(); }
    };

    // Tile DnD handlers. dataTransfer carries the id so Firefox starts the
    // drag; state carries it for React. Drop side = which half of the tile.
    const onTileDragStart = (e, id) => { setDragId(id); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', String(id)); };
    const onTileDragOver  = (e, id) => {
        if (dragId == null || dragId === id) return;
        e.preventDefault();
        const r = e.currentTarget.getBoundingClientRect();
        setDropAt({ id, side: (e.clientX - r.left) < r.width / 2 ? 'before' : 'after' });
    };
    const onTileDrop = (e, id) => {
        e.preventDefault();
        if (dragId != null && dropAt && dropAt.id === id) moveTo(dragId, id, dropAt.side);
        setDragId(null); setDropAt(null);
    };
    const onTileDragEnd = () => { setDragId(null); setDropAt(null); };

    return (
        <div className="attachments-section">
            <h4>
                <span><i className="fas fa-images" style={{ color: '#6b7280' }}></i> {title} {photos.length > 0 && `(${photos.length})`}</span>
                <button type="button" className="btn btn-secondary btn-small"
                        onClick={() => fileInputRef.current?.click()} disabled={uploading}>
                    {uploading ? <><i className="fas fa-spinner fa-spin"></i> Uploading…</> : <><i className="fas fa-upload"></i> Add photos</>}
                </button>
            </h4>
            <input ref={fileInputRef} type="file" multiple accept="image/*" style={{ display: 'none' }}
                   onChange={e => { handleFiles(e.target.files); e.target.value = ''; }} />

            {/* File drop zone — only for FILES from outside; tile drags (which
                set dragId) must not light it up or be swallowed by it. */}
            <div className={`upload-zone ${dragOver ? 'drag-over' : ''}`}
                 style={{ marginBottom: '0.75rem', padding: '0.75rem' }}
                 onClick={() => fileInputRef.current?.click()}
                 onDragOver={(e) => { if (dragId != null) return; e.preventDefault(); setDragOver(true); }}
                 onDragLeave={() => setDragOver(false)}
                 onDrop={(e) => { if (dragId != null) return; e.preventDefault(); setDragOver(false); handleFiles(e.dataTransfer.files); }}>
                <i className="fas fa-cloud-upload-alt" style={{ marginRight: '0.5rem' }}></i>
                Drop photos here or click to browse (25 MB each). Drag tiles to set the order buyers see — the first photo is the cover.
            </div>

            {error && (
                <p style={{ color: 'var(--danger, #ef4444)', fontSize: '0.8125rem', marginBottom: '0.5rem' }}>
                    <i className="fas fa-triangle-exclamation" style={{ marginRight: '0.35rem' }}></i>{error}
                </p>
            )}

            {loading ? (
                <div style={{ color: '#6b7280', fontSize: '0.875rem' }}><i className="fas fa-spinner fa-spin"></i> Loading…</div>
            ) : photos.length === 0 ? (
                <p style={{ color: '#9ca3af', fontSize: '0.8125rem' }}>No photos yet.</p>
            ) : (
                <div className="photo-grid" style={{ maxHeight: '26rem', overflowY: 'auto', paddingRight: '0.25rem' }}>
                    {photos.map((p, i) => {
                        const cls = ['photo-tile',
                            dragId === p.id ? 'dragging' : '',
                            dropAt && dropAt.id === p.id ? `drop-${dropAt.side}` : ''].join(' ');
                        return (
                            <div key={p.id} className={cls} draggable={editing !== p.id}
                                 onDragStart={(e) => onTileDragStart(e, p.id)}
                                 onDragOver={(e) => onTileDragOver(e, p.id)}
                                 onDragLeave={() => setDropAt(d => (d && d.id === p.id ? null : d))}
                                 onDrop={(e) => onTileDrop(e, p.id)}
                                 onDragEnd={onTileDragEnd}
                                 title={p.original_name}>
                                {urls[p.id]
                                    ? <img src={urls[p.id]} alt={p.caption || p.original_name} draggable={false} />
                                    : <div className="photo-placeholder"><i className="fas fa-image"></i></div>}
                                {i === 0 && <span className="photo-cover">COVER</span>}
                                <span className="photo-pos">{i + 1}</span>
                                <div className="photo-actions">
                                    {i > 0 && (
                                        <button type="button" title="Make this the cover photo" onClick={() => makeCover(p.id)}>
                                            <i className="fas fa-star"></i>
                                        </button>
                                    )}
                                    <button type="button" title="Delete photo" onClick={() => handleDelete(p)}>
                                        <i className="fas fa-trash"></i>
                                    </button>
                                </div>
                                <input className="photo-caption" type="text" maxLength={200}
                                       placeholder="Add a label (e.g. Primary bedroom)"
                                       value={p.caption || ''}
                                       onChange={e => setCaptionLocal(p.id, e.target.value)}
                                       onFocus={() => setEditing(p.id)}
                                       onBlur={() => { setEditing(null); saveCaption(p); }}
                                       onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
                                       draggable={false} />
                            </div>
                        );
                    })}
                </div>
            )}
        </div>
    );
};
