import { API_BASE, state } from './state.js';
import { showToast, escapeHtml, classifyResourceKind, buildCardTransform } from './utils.js';
import { savePlacements, fetchBinderCards, fetchBinderList, syncAndRender } from './api.js';
import { checkHazardWarning, updateBinderSelectors } from './renderer.js';
import { packItems, computeAABBSize } from './packingEngine.js';

export async function handleCardDrop(card, droppedZone, initialPos) {
    const cardId = card.dataset.id;
    const resource = state.currentCards.find(r => r.id === cardId);
    if (!resource) return;

    if (!droppedZone) {
        if (resource.status_binder_id && resource.status_binder_id !== state.currentBinder.id) {
            return;
        }


        const hasAnyZone = document.querySelector('.binder-zone') !== null;
        
        let tags = [...(resource.tags || [])];
        const hasTaskTag = tags.some(t => t.toLowerCase() === 'task');
        
        if (hasTaskTag && hasAnyZone) {
            if (resource.status === 'none') {
                return;
            }

            if (initialPos) {
                card.style.transition = 'transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
                
                setTimeout(() => {
                    card.dataset.x = initialPos.x;
                    card.dataset.y = initialPos.y;
                    card.style.transform = `translate(${initialPos.x}px, ${initialPos.y}px)`;
                    
                    savePlacements();
                    setTimeout(() => { card.style.transition = ''; }, 400);
                }, 0);
                
                showToast('タスクはカンバンゾーン外に配置できません', 'info');
            }
        }
        return;
    }

    const targetStatus = droppedZone.dataset.status;
    let needsHazardCheck = false;
    let updatePayload = { title: resource.title, content: resource.content };
    
    if (resource.status_binder_id && resource.status_binder_id !== state.currentBinder.id) {
        showToast('このカードのステータスは別のバインダーで管理されています', 'info');
        if (initialPos) {
            card.dataset.x = initialPos.x;
            card.dataset.y = initialPos.y;
            card.style.transform = `translate(${initialPos.x}px, ${initialPos.y}px)`;
            savePlacements();
        }
        return;
    }

    if (resource.status !== targetStatus || resource.status_binder_id !== state.currentBinder.id) {
        try {
            const response = await fetch(`${API_BASE}/cards/${cardId}/status`, {
                method: 'PATCH',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ status: targetStatus, status_binder_id: state.currentBinder.id })
            });

            if (!response.ok) {
                const errorData = await response.json().catch(() => ({}));
                showToast(errorData.error || 'ステータスの更新に失敗しました', 'error');
                
                if (initialPos) {
                    card.dataset.x = initialPos.x;
                    card.dataset.y = initialPos.y;
                    card.style.transform = `translate(${initialPos.x}px, ${initialPos.y}px)`;
                    savePlacements();
                }
                return; // 失敗したら以降の処理は中断
            }

            resource.status = targetStatus;
            resource.status_binder_id = state.currentBinder.id;
            needsHazardCheck = true;

            let badge = card.querySelector('.card-status-badge');
            if (!targetStatus || targetStatus === 'none') {
                if (badge) badge.remove();
            } else {
                if (!badge) {
                    badge = document.createElement('div');
                    badge.className = `card-status-badge status-${targetStatus.toLowerCase()}`;
                    badge.textContent = targetStatus;
                    const header = card.querySelector('.card-header');
                    if (header) {
                        header.appendChild(badge);
                    }
                } else {
                    badge.className = `card-status-badge status-${targetStatus.toLowerCase()}`;
                    badge.textContent = targetStatus;
                }
            }

            const spStamp = card.querySelector('.sp-task-stamp');
            if (spStamp) {
                if (!targetStatus || targetStatus === 'none') {
                    spStamp.textContent = '';
                    spStamp.className = 'sp-task-stamp status-none';
                } else {
                    spStamp.textContent = targetStatus.toUpperCase();
                    spStamp.className = `sp-task-stamp status-${targetStatus.toLowerCase()}`;
                }
            }
        } catch (error) {
            console.error('Status update failed', error);
            return;
        }
    }

    if (updatePayload.tags) {
        try {
            await fetch(`${API_BASE}/cards/${cardId}`, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(updatePayload)
            });
            resource.tags = tags; // update local
            
            const tagsHtml = tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('');
            const tagsContainer = card.querySelector('.card-tags');
            if (tagsContainer) {
                tagsContainer.innerHTML = `<div class="tags-inner">${tagsHtml}</div>`;
                setTimeout(() => {
                    const tagsInner = tagsContainer.querySelector('.tags-inner');
                    if (tagsInner) {
                        const cs = window.getComputedStyle(tagsContainer);
                        const pl = parseFloat(cs.paddingLeft) || 0;
                        const pr = parseFloat(cs.paddingRight) || 0;
                        const visibleW = tagsContainer.clientWidth - pl - pr;
                        if (tagsInner.scrollWidth > visibleW) {
                            const overflow = tagsInner.scrollWidth - visibleW + 10;
                            tagsInner.style.setProperty('--overflow-dist', `-${overflow}px`);
                            tagsInner.classList.add('marquee-active');
                        }
                    }
                }, 0);
            }
        } catch (error) {
            console.error('Tag update failed', error);
        }
    }

    if (needsHazardCheck) {
        checkHazardWarning();
    }
}

export async function handleBinderLinkDrop(cardsData, binderLinkCard) {
    if (!cardsData || cardsData.length === 0) return;
    
    const targetBinderId = binderLinkCard.dataset.targetBinderId;
    if (!targetBinderId || targetBinderId === state.currentBinder.id) return;

    try {
        const res = await fetch(`${API_BASE}/binderes/${targetBinderId}`);
        if (!res.ok) throw new Error('送信先バインダーの取得に失敗しました');
        const targetBinder = await res.json();
        
        const placements = targetBinder.placements || [];
        let addedCount = 0;
        
        const binderLinkX = parseFloat(binderLinkCard.dataset.x || 0) + (binderLinkCard.offsetWidth / 2);
        const binderLinkY = parseFloat(binderLinkCard.dataset.y || 0) + (binderLinkCard.offsetHeight / 2);

        for (const data of cardsData) {
            const card = data.element;
            const cardId = card.dataset.id;
            const resource = state.currentCards.find(r => r.id === cardId);
            
            if (!resource) continue;
            
            if (!placements.some(p => p.id === cardId)) {
                const targetX = 100 + Math.random() * 50;
                const targetY = 100 + Math.random() * 50;
                placements.push({
                    id: cardId,
                    type: 'card',
                    x: targetX,
                    y: targetY,
                    width: 250,
                    height: 150
                });
                addedCount++;
            }

            card.classList.add('card-sucking');
            const targetX_anim = binderLinkX - (card.offsetWidth / 2);
            const targetY_anim = binderLinkY - (card.offsetHeight / 2);
            
            requestAnimationFrame(() => {
                card.style.transform = `translate(${targetX_anim}px, ${targetY_anim}px) rotate(720deg) scale(0)`;
                card.style.opacity = '0';
            });
        }

        if (addedCount > 0) {
            await fetch(`${API_BASE}/binderes/${targetBinderId}/placements`, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ placements: placements })
            });
        }
        
        setTimeout(async () => {
            const removedResourceIds = [];
            for (const data of cardsData) {
                const card = data.element;
                const cardId = card.dataset.id;
                if (state.currentCards.some(r => r.id === cardId)) {
                    removedResourceIds.push(cardId);
                }
                card.remove();
            }

            binderLinkCard.classList.remove('binder-link-highlight');
            binderLinkCard.style.transform = `translate(${binderLinkCard.dataset.x}px, ${binderLinkCard.dataset.y}px) scale(1)`;

            if (removedResourceIds.length > 0) {
                await savePlacements();
            }

            state.currentCards = state.currentCards.filter(r => !removedResourceIds.includes(r.id));
            
            showToast(`「${targetBinder.title || targetBinderId}」へ ${cardsData.length} 枚のカードを移動しました`, 'success');
        }, 600); // CSSのtransition時間と合わせる

    } catch (error) {
        console.error('BinderLink drop failed', error);
        showToast(error.message || 'カードの移動に失敗しました', 'error');
        
        for (const data of cardsData) {
            const card = data.element;
            const initialPos = data.initialPos;
            card.classList.remove('card-sucking');
            card.style.opacity = '1';
            card.style.transition = ''; // 遷移スタイルを元に戻す
            if (initialPos) {
                card.dataset.x = initialPos.x;
                card.dataset.y = initialPos.y;
                card.style.transform = `translate(${initialPos.x}px, ${initialPos.y}px)`;
            }
        }
        binderLinkCard.classList.remove('binder-link-highlight');
        binderLinkCard.style.transform = `translate(${binderLinkCard.dataset.x}px, ${binderLinkCard.dataset.y}px) scale(1)`;
        
        savePlacements();
    }
}

export async function deleteCards(cards) {
    if (!cards || cards.length === 0) return;
    
    let needsSave = false;
    let needsRefresh = false;

    for (const card of cards) {
        const id = card.dataset.id;
        const type = card.dataset.type || 'card';

        if (type === 'card') {
            const resource = state.currentCards.find(r => r.id === id);
            if (resource && resource.status_binder_id === state.currentBinder.id) {
                try {
                    await fetch(`${API_BASE}/cards/${id}/status`, {
                        method: 'PATCH',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ status: 'none', status_binder_id: null })
                    });
                } catch (e) {
                    console.error('Failed to reset status on delete', e);
                }
            }
            card.remove();
            needsSave = true;
            needsRefresh = true;
        } else if (type === 'binder') {
            card.remove();
            needsSave = true;
        }
    }

    if (window.BinderManager && window.BinderManager.clearSelection) {
        window.BinderManager.clearSelection();
    }

    if (needsSave) {
        await savePlacements();
        
        fetch(`${API_BASE}/cards/gc`, {
            method: 'POST'
        }).catch(e => console.error('GC error:', e));
    }
    
    if (needsRefresh) {
        await fetchBinderCards();
        showToast('バインダーから外しました', 'success');
    } else if (needsSave) {
        showToast('バインダーリンクを取り外しました', 'success');
    }
}

export async function archiveDoneCards() {
    if (!state.currentBinder) return;

    const domCards = document.querySelectorAll('.binder-card:not(.binder-link-card)');
    const doneIds = [];
    domCards.forEach(c => {
        const id = c.dataset.id;
        if (c.classList.contains('binder-zone')) return;
        const res = state.currentCards.find(r => r.id === id);
        if (res && res.status && res.status.toLowerCase() === 'done') {
            doneIds.push(id);
        }
    });

    if (doneIds.length === 0) {
        showToast('移動する完了済みカードがありません', 'info');
        return;
    }

    try {
        const nowStr = new Date().toLocaleString('ja-JP', { 
            year: 'numeric', month: '2-digit', day: '2-digit', 
            hour: '2-digit', minute: '2-digit', second: '2-digit' 
        });
        
        for (const id of doneIds) {
            const res = state.currentCards.find(r => r.id === id);
            if (!res) continue;
            
            const appendedContent = (res.content || '') + `\n\n---\n**[アーカイブ情報]**\n- 移動元バインダー: ${state.currentBinder.title || state.currentBinder.id}\n- 移動日時: ${nowStr}`;
            
            await fetch(`${API_BASE}/cards/${id}`, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ ...res, content: appendedContent })
            });
        }

        let archiveBinder = state.binderList.find(c => c.title === '完了済みアーカイブ');
        if (!archiveBinder) {
            const res = await fetch(`${API_BASE}/binderes`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ title: '完了済みアーカイブ', description: '完了済みカードの保管場所' })
            });
            if (!res.ok) throw new Error('アーカイブバインダーの作成に失敗しました');
            archiveBinder = await res.json();
            await fetchBinderList();
            updateBinderSelectors();
        }

        const archRes = await fetch(`${API_BASE}/binderes/${archiveBinder.id}`);
        if (!archRes.ok) throw new Error('アーカイブバインダーの取得に失敗しました');
        const archData = await archRes.json();
        const archPlacements = archData.placements || [];

        let startX = 100;
        let startY = 100;
        if (archPlacements.length > 0) {
            const maxRes = archPlacements.reduce((prev, current) => {
                return (prev.y > current.y) ? prev : current;
            });
            startX = 100;
            startY = maxRes.y + 200;
        }

        doneIds.forEach((id, idx) => {
            archPlacements.push({
                id: id,
                type: 'card',
                x: startX + (idx % 5) * 300,
                y: startY + Math.floor(idx / 5) * 200,
                width: 250,
                height: 150
            });
        });

        await fetch(`${API_BASE}/binderes/${archiveBinder.id}/placements`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ placements: archPlacements })
        });

        domCards.forEach(c => {
            if (doneIds.includes(c.dataset.id)) {
                c.remove();
            }
        });
        await savePlacements();
        
        showToast(`${doneIds.length}件のカードを「完了済みアーカイブ」に移動しました`, 'success');

    } catch (error) {
        console.error(error);
        showToast(error.message || 'アーカイブ処理中にエラーが発生しました', 'error');
    }
}

export async function handlePastedPaths(paths, startX, startY) {
    showToast(`${paths.length}件のパスを登録中...`, 'info');
    let successCount = 0;
    let newIds = [];

    for (const path of paths) {
        const rawTitle = path.split(/[\/\\]/).pop() || 'Untitled_File';
        const title = rawTitle.replace(/\.[a-zA-Z0-9]{2,5}$/, '');
        const payload = {
            title: title,
            content: `Path: ${path}`,
            file_path: path,
            tags: ['Imported']
        };

        try {
            const response = await fetch(`${API_BASE}/cards`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
            });
            
            if (response.ok) {
                successCount++;
                const data = await response.json();
                if (data.id) newIds.push(data.id);
            }
        } catch (error) {
            console.error("Failed to import:", path, error);
        }
    }

    if (successCount > 0) {
        let placedCount = 0;
        if (state.currentBinder && newIds.length > 0) {
            const rect = document.getElementById('binder-container').getBoundingClientRect();
            let scale = window.BinderManager ? window.BinderManager.getScale() : 1;
            let trans = window.BinderManager ? window.BinderManager.getTranslate() : {x:0, y:0};

            const padding = 20;
            const cardWidth = 210;
            const cardHeight = 160;
            const minX = -trans.x / scale + padding;
            const maxX = (rect.width - trans.x) / scale - cardWidth - padding;
            const minY = -trans.y / scale + 40; 
            const maxY = (rect.height - trans.y) / scale - cardHeight - padding;
            
            const maxRadius = Math.min(rect.width, rect.height) / 2 / scale * 0.8;

            newIds.forEach((newId, idx) => {
                const isAlreadyPlaced = state.currentBinder.placements.some(p => p.id === newId);
                if (!isAlreadyPlaced) {
                    let targetX, targetY;
                    if (placedCount === 0) {
                        targetX = startX;
                        targetY = startY;
                    } else {
                        const r = Math.sqrt(Math.random()) * maxRadius; 
                        const theta = Math.random() * 2 * Math.PI;
                        targetX = startX + r * Math.cos(theta);
                        targetY = startY + r * Math.sin(theta);
                    }
                    
                    targetX = Math.max(minX, Math.min(targetX, maxX));
                    targetY = Math.max(minY, Math.min(targetY, maxY));

                    state.currentBinder.placements.push({
                        id: newId,
                        type: 'card',
                        x: targetX,
                        y: targetY,
                        width: cardWidth,
                        height: cardHeight
                    });
                    placedCount++;
                }
            });
        }
        
        if (placedCount < successCount) {
            const skipped = successCount - placedCount;
            showToast(`${successCount}件処理し、${placedCount}件を配置しました（${skipped}件は配置済み）`, 'success');
        } else {
            showToast(`${successCount}件のファイルを登録・配置しました`, 'success');
        }

        await syncAndRender();
        if (state.currentBinder && placedCount > 0) {
            await savePlacements();
        }
    } else {
        showToast('ファイルの登録に失敗しました', 'error');
    }
}

export async function handlePastedImage(file, startX, startY) {
    showToast('画像を保存中...', 'info');

    const formData = new FormData();
    formData.append('image', file);

    try {
        const response = await fetch(`${API_BASE}/cards/image`, {
            method: 'POST',
            body: formData
        });

        if (!response.ok) {
            let errorMsg = '画像の保存に失敗しました';
            try {
                const errorData = await response.json();
                if (errorData.error && errorData.error.message) errorMsg = errorData.error.message;
            } catch (e) {}
            throw new Error(errorMsg);
        }

        const data = await response.json();
        const newId = data.id;
        if (!newId) throw new Error('画像の保存に失敗しました');

        if (state.currentBinder) {
            const isAlreadyPlaced = state.currentBinder.placements.some(p => p.id === newId);
            if (!isAlreadyPlaced) {
                state.currentBinder.placements.push({
                    id: newId,
                    type: 'card',
                    x: startX,
                    y: startY,
                    width: 210,
                    height: 160
                });
            }
        }

        await syncAndRender();
        if (state.currentBinder) {
            await savePlacements();
        }

        showToast('画像を貼り付けました', 'success');
    } catch (error) {
        console.error('Failed to paste image:', error);
        showToast(error.message || '画像の貼り付けに失敗しました', 'error');
    }
}

export async function handlePastedUrls(urls, startX, startY) {
    showToast(`${urls.length}件のURLを登録中...`, 'info');
    let successCount = 0;
    let newIds = [];

    for (const url of urls) {
        const payload = { url: url };

        try {
            const response = await fetch(`${API_BASE}/cards/url`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
            });
            
            if (response.ok) {
                successCount++;
                const data = await response.json();
                if (data.id) newIds.push(data.id);
            }
        } catch (error) {
            console.error("Failed to import url:", url, error);
        }
    }

    if (successCount > 0) {
        let placedCount = 0;
        if (state.currentBinder && newIds.length > 0) {
            const rect = document.getElementById('binder-container').getBoundingClientRect();
            let scale = window.BinderManager ? window.BinderManager.getScale() : 1;
            let trans = window.BinderManager ? window.BinderManager.getTranslate() : {x:0, y:0};

            const padding = 20;
            const cardWidth = 210;
            const cardHeight = 160;
            const minX = -trans.x / scale + padding;
            const maxX = (rect.width - trans.x) / scale - cardWidth - padding;
            const minY = -trans.y / scale + 40; 
            const maxY = (rect.height - trans.y) / scale - cardHeight - padding;
            
            const maxRadius = Math.min(rect.width, rect.height) / 2 / scale * 0.8;

            newIds.forEach((newId, idx) => {
                const isAlreadyPlaced = state.currentBinder.placements.some(p => p.id === newId);
                if (!isAlreadyPlaced) {
                    let targetX, targetY;
                    if (placedCount === 0) {
                        targetX = startX;
                        targetY = startY;
                    } else {
                        const r = Math.sqrt(Math.random()) * maxRadius; 
                        const theta = Math.random() * 2 * Math.PI;
                        targetX = startX + r * Math.cos(theta);
                        targetY = startY + r * Math.sin(theta);
                    }
                    
                    targetX = Math.max(minX, Math.min(targetX, maxX));
                    targetY = Math.max(minY, Math.min(targetY, maxY));

                    state.currentBinder.placements.push({
                        id: newId,
                        type: 'card',
                        x: targetX,
                        y: targetY,
                        width: cardWidth,
                        height: cardHeight
                    });
                    placedCount++;
                }
            });
        }
        
        if (placedCount < successCount) {
            const skipped = successCount - placedCount;
            showToast(`${successCount}件処理し、${placedCount}件を配置しました（${skipped}件は配置済み）`, 'success');
        } else {
            showToast(`${successCount}件のURLを登録・配置しました`, 'success');
        }

        await syncAndRender();
        if (state.currentBinder && placedCount > 0) {
            await savePlacements();
        }
    } else {
        showToast('URLの登録に失敗しました', 'error');
    }
}

export async function handlePastedText(text, startX, startY) {
    showToast(`メモを登録中...`, 'info');

    const firstLine = text.split(/\r?\n/).find(line => line.trim().length > 0) || '';
    const titleChars = Array.from(firstLine.trim());
    let title = titleChars.slice(0, 20).join('');
    if (titleChars.length > 20) {
        title += '...';
    }
    if (!title) {
        title = '無題のメモ';
    }

    const payload = {
        title: title,
        content: text,
        tags: ['メモ']
    };

    try {
        const response = await fetch(`${API_BASE}/cards`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });
        
        if (!response.ok) throw new Error('Failed to create memo');
        
        const data = await response.json();
        
        if (data.id && state.currentBinder) {
            const rect = document.getElementById('binder-container').getBoundingClientRect();
            let scale = window.BinderManager ? window.BinderManager.getScale() : 1;
            let trans = window.BinderManager ? window.BinderManager.getTranslate() : {x:0, y:0};

            const padding = 20;
            const cardWidth = 250;
            const cardHeight = 150;
            const minX = -trans.x / scale + padding;
            const maxX = (rect.width - trans.x) / scale - cardWidth - padding;
            const minY = -trans.y / scale + 40; 
            const maxY = (rect.height - trans.y) / scale - cardHeight - padding;

            let targetX = Math.max(minX, Math.min(startX, maxX));
            let targetY = Math.max(minY, Math.min(startY, maxY));

            state.currentBinder.placements.push({
                id: data.id,
                type: 'card',
                x: targetX,
                y: targetY,
                width: cardWidth,
                height: cardHeight
            });

            showToast('メモを配置しました', 'success');

            await syncAndRender();
            await savePlacements();
        }
    } catch (error) {
        console.error("Failed to import text:", error);
        showToast('メモの登録に失敗しました', 'error');
    }
}


const ARRANGE_CARD_FALLBACK_W = 200;
const ARRANGE_CARD_FALLBACK_H = 150;
const ARRANGE_PADDING_X = 10;
const ARRANGE_PADDING_TOP = 60;
const ARRANGE_GAP = 10;
const ARRANGE_CLUSTER_MARGIN = 30;
const ARRANGE_MIN_CONTAINER_WIDTH = 300;
const ARRANGE_FREE_MARGIN = 20;

const RELAX_ITERATIONS = 50;
const RELAX_REPULSION_STRENGTH = 0.8; // 重なり解消の強さ（1に近いほど1回のイテレーションで完全に押し離す）
const RELAX_REPULSION_SUBSTEPS = 4; // 1イテレーションあたりの斥力解消の反復回数（引力より確実に勝たせるため複数回行う）
const RELAX_ATTRACTION_STRENGTH = 0.015; // 重心へ寄せる強さ（弱めにして重なり解消を優先させる）
const RELAX_CLEANUP_ITERATIONS = 100; // 最終仕上げ（引力なし・斥力のみ）の反復回数。重なりゼロを保証するための仕上げ

const ARRANGE_ANIM_DURATION_MS = 300;

let lastArrangeHistory = null;

function getArrangeCardDims(placement, cardElById) {
    const el = cardElById.get(String(placement.id));
    return {
        w: (el && el.offsetWidth > 0) ? el.offsetWidth : ARRANGE_CARD_FALLBACK_W,
        h: (el && el.offsetHeight > 0) ? el.offsetHeight : ARRANGE_CARD_FALLBACK_H,
    };
}

function sortByPosition(a, b) {
    const ROW_TOLERANCE = 80; // 約カードの高さの半分。この範囲なら同じ行とみなす
    if (Math.abs(a.y - b.y) > ROW_TOLERANCE) {
        return a.y - b.y;
    }
    return a.x - b.x;
}

function getArrangeCardTitle(card) {
    if (card.type === 'binder') {
        const binder = state.binderList.find(b => b.id === card.id);
        return (binder && binder.title) || card.id;
    }
    const res = state.currentCards.find(r => r.id === card.id);
    return (res && res.title) || card.id;
}

function clusterFreeCards(freeCards, mode) {
    const collator = new Intl.Collator('ja');

    if (mode === 'title') {
        const sorted = [...freeCards].sort((a, b) =>
            collator.compare(getArrangeCardTitle(a), getArrangeCardTitle(b))
        );
        return [sorted];
    }

    if (mode === 'type') {
        const TYPE_ORDER = ['url', 'task', 'memo', 'folder', 'image', 'text', 'other', 'binderLink'];
        const groups = new Map();
        freeCards.forEach(card => {
            let key = 'binderLink';
            if (card.type === 'card') {
                const res = state.currentCards.find(r => r.id === card.id);
                key = res ? classifyResourceKind(res) : 'other';
            }
            if (!groups.has(key)) groups.set(key, []);
            groups.get(key).push(card);
        });
        return TYPE_ORDER.filter(key => groups.has(key)).map(key => groups.get(key));
    }

    if (mode === 'tag') {
        const NO_TAG_KEY = '(タグなし)';
        const groups = new Map();
        freeCards.forEach(card => {
            let key = NO_TAG_KEY;
            if (card.type === 'card') {
                const res = state.currentCards.find(r => r.id === card.id);
                if (res && res.tags && res.tags.length > 0) key = res.tags[0];
            }
            if (!groups.has(key)) groups.set(key, []);
            groups.get(key).push(card);
        });
        const keys = Array.from(groups.keys()).sort((a, b) => {
            if (a === NO_TAG_KEY) return 1;
            if (b === NO_TAG_KEY) return -1;
            return collator.compare(a, b);
        });
        return keys.map(key => groups.get(key));
    }

    return [[...freeCards].sort(sortByPosition)];
}

function getInfoOverlayVirtualRect() {
    const infoEl = document.getElementById('binder-info-overlay');
    const binderContainer = document.getElementById('binder-container');
    if (!infoEl || !binderContainer) return null;

    const infoRect = infoEl.getBoundingClientRect();
    const binderRect = binderContainer.getBoundingClientRect();
    const scale = window.BinderManager ? window.BinderManager.getScale() : 1;
    const translate = window.BinderManager ? window.BinderManager.getTranslate() : { x: 0, y: 0 };

    const screenX = infoRect.left - binderRect.left;
    const screenY = infoRect.top - binderRect.top;
    const virtualX = (screenX - translate.x) / scale;
    const virtualY = (screenY - translate.y) / scale;
    const virtualW = infoRect.width / scale;
    const virtualH = infoRect.height / scale;

    return { x: virtualX, y: virtualY, width: virtualW, height: virtualH };
}

function relaxItems(items, obstacles = [], containerWidth = Infinity, options = {}) {
    const startX = options.startX ?? 0;
    const startY = options.startY ?? 0;
    const gap = ARRANGE_GAP;

    if (items.length === 0) return [];

    const nodes = items.map(item => {
        const aabb = computeAABBSize(item.width, item.height, item.rotation || 0);
        const curX = item.ref.x || startX;
        const curY = item.ref.y || startY;
        return {
            item,
            width: item.width,
            height: item.height,
            aabbW: aabb.width,
            aabbH: aabb.height,
            cx: curX + item.width / 2 + (Math.random() - 0.5) * 2,
            cy: curY + item.height / 2 + (Math.random() - 0.5) * 2,
        };
    });

    const obstacleNodes = obstacles.map(o => ({
        cx: o.x + o.width / 2,
        cy: o.y + o.height / 2,
        halfW: o.width / 2,
        halfH: o.height / 2,
    }));

    const resolveOverlaps = (strength) => {
        for (let i = 0; i < nodes.length; i++) {
            const a = nodes[i];

            for (let j = i + 1; j < nodes.length; j++) {
                const b = nodes[j];
                const halfWSum = a.aabbW / 2 + b.aabbW / 2 + gap;
                const halfHSum = a.aabbH / 2 + b.aabbH / 2 + gap;
                const dx = b.cx - a.cx;
                const dy = b.cy - a.cy;
                const overlapX = halfWSum - Math.abs(dx);
                const overlapY = halfHSum - Math.abs(dy);
                if (overlapX > 0 && overlapY > 0) {
                    if (overlapX < overlapY) {
                        const push = (overlapX / 2) * strength;
                        const dir = dx === 0 ? (Math.random() < 0.5 ? -1 : 1) : Math.sign(dx);
                        a.cx -= dir * push;
                        b.cx += dir * push;
                    } else {
                        const push = (overlapY / 2) * strength;
                        const dir = dy === 0 ? (Math.random() < 0.5 ? -1 : 1) : Math.sign(dy);
                        a.cy -= dir * push;
                        b.cy += dir * push;
                    }
                }
            }

            obstacleNodes.forEach(o => {
                const halfWSum = a.aabbW / 2 + o.halfW + gap;
                const halfHSum = a.aabbH / 2 + o.halfH + gap;
                const dx = a.cx - o.cx;
                const dy = a.cy - o.cy;
                const overlapX = halfWSum - Math.abs(dx);
                const overlapY = halfHSum - Math.abs(dy);
                if (overlapX > 0 && overlapY > 0) {
                    if (overlapX < overlapY) {
                        const dir = dx === 0 ? 1 : Math.sign(dx);
                        a.cx += dir * overlapX * strength;
                    } else {
                        const dir = dy === 0 ? 1 : Math.sign(dy);
                        a.cy += dir * overlapY * strength;
                    }
                }
            });
        }
    };

    for (let iter = 0; iter < RELAX_ITERATIONS; iter++) {
        let centroidX = 0;
        let centroidY = 0;
        nodes.forEach(n => { centroidX += n.cx; centroidY += n.cy; });
        centroidX /= nodes.length;
        centroidY /= nodes.length;
        nodes.forEach(n => {
            n.cx += (centroidX - n.cx) * RELAX_ATTRACTION_STRENGTH;
            n.cy += (centroidY - n.cy) * RELAX_ATTRACTION_STRENGTH;
        });

        for (let s = 0; s < RELAX_REPULSION_SUBSTEPS; s++) resolveOverlaps(RELAX_REPULSION_STRENGTH);

        if (containerWidth !== Infinity) {
            nodes.forEach(n => {
                const minCx = startX + n.aabbW / 2;
                const maxCx = startX + containerWidth - n.aabbW / 2;
                if (n.cx < minCx) n.cx = minCx;
                else if (maxCx > minCx && n.cx > maxCx) n.cx = maxCx;
            });
        }
    }

    let minLeft = Infinity;
    let minTop = Infinity;
    nodes.forEach(n => {
        minLeft = Math.min(minLeft, n.cx - n.width / 2);
        minTop = Math.min(minTop, n.cy - n.height / 2);
    });
    const shiftX = Math.max(0, startX - minLeft);
    const shiftY = Math.max(0, startY - minTop);
    if (shiftX > 0 || shiftY > 0) {
        nodes.forEach(n => { n.cx += shiftX; n.cy += shiftY; });
    }

    for (let i = 0; i < RELAX_CLEANUP_ITERATIONS; i++) resolveOverlaps(1.0);

    return nodes.map(n => ({
        ...n.item,
        x: n.cx - n.width / 2,
        y: n.cy - n.height / 2,
    }));
}

async function animateArrangeAndSave(placements, cardElById) {
    const movedEls = [];
    placements.forEach(p => {
        const el = cardElById.get(String(p.id));
        if (!el) return;
        el.style.transition = `transform ${ARRANGE_ANIM_DURATION_MS}ms ease-out`;
        el.dataset.x = p.x;
        el.dataset.y = p.y;
        movedEls.push(el);
    });

    await new Promise(resolve => requestAnimationFrame(resolve));
    movedEls.forEach(el => {
        el.style.transform = buildCardTransform(el.dataset.x, el.dataset.y, el.dataset.rotation, 1);
        el.style.setProperty('--card-rotation', `${el.dataset.rotation}deg`);
    });

    await new Promise(resolve => setTimeout(resolve, ARRANGE_ANIM_DURATION_MS));
    movedEls.forEach(el => { el.style.transition = ''; });

    await savePlacements();
}

export function saveArrangeHistory(placementsToSnapshot) {
    lastArrangeHistory = placementsToSnapshot.map(p => ({ id: p.id, x: p.x, y: p.y, rotation: p.rotation }));
}

export function canUndoArrange() {
    return !!(lastArrangeHistory && lastArrangeHistory.length > 0);
}

export async function undoLastArrange() {
    if (!state.currentBinder || !lastArrangeHistory || lastArrangeHistory.length === 0) return;

    const placements = state.currentBinder.placements || [];
    const byId = new Map(placements.map(p => [String(p.id), p]));
    const cardElById = new Map();
    document.querySelectorAll('.binder-card[data-id]').forEach(el => {
        cardElById.set(el.dataset.id, el);
    });

    const restored = [];
    lastArrangeHistory.forEach(snap => {
        const p = byId.get(String(snap.id));
        if (p) {
            p.x = snap.x;
            p.y = snap.y;
            if (snap.rotation !== undefined) p.rotation = snap.rotation;
            restored.push(p);
            const el = cardElById.get(String(p.id));
            if (el && snap.rotation !== undefined) el.dataset.rotation = snap.rotation;
        }
    });

    lastArrangeHistory = null;

    await animateArrangeAndSave(restored, cardElById);
    showToast('整列を元に戻しました', 'success');
}

export async function arrangeCards(mode = 'position') {
    if (!state.currentBinder || !state.currentBinder.placements) return;

    const placements = state.currentBinder.placements;
    const zones = placements.filter(p => p.type === 'zone');
    const cards = placements.filter(p => p.type === 'card' || p.type === 'binder');

    saveArrangeHistory(cards);

    const zoneEls = Array.from(document.querySelectorAll('.binder-zone'));
    const zoneElMap = new Map();
    zoneEls.forEach(el => zoneElMap.set(el.dataset.id, el));

    const cardElById = new Map();
    document.querySelectorAll('.binder-card[data-id]').forEach(el => {
        cardElById.set(el.dataset.id, el);
    });

    const zoneCards = new Map(); // status -> array of placements
    zones.forEach(z => zoneCards.set(z.status, []));
    const freeCards = [];
    const pinnedCards = [];

    cards.forEach(card => {
        if (card.pinned) {
            pinnedCards.push(card);
            return;
        }
        if (card.type === 'card') {
            const res = state.currentCards.find(r => r.id === card.id);
            if (res && res.status && zoneCards.has(res.status) && classifyResourceKind(res) === 'task') {
                zoneCards.get(res.status).push(card);
            } else {
                freeCards.push(card);
            }
        } else {
            freeCards.push(card);
        }
    });

    const pinnedObstacles = pinnedCards.map(c => {
        const { w, h } = getArrangeCardDims(c, cardElById);
        const aabb = computeAABBSize(w, h, c.rotation || 0);
        const aabbX = c.x - (aabb.width - w) / 2;
        const aabbY = c.y - (aabb.height - h) / 2;
        return {
            x: aabbX - ARRANGE_GAP,
            y: aabbY - ARRANGE_GAP,
            width: aabb.width + ARRANGE_GAP * 2,
            height: aabb.height + ARRANGE_GAP * 2
        };
    });

    let maxZoneBottom = 50;

    zones.forEach(zone => {
        const zEl = zoneElMap.get(String(zone.id));
        const zW = zEl ? zEl.offsetWidth : zone.width;
        const zH = zEl ? zEl.offsetHeight : zone.height;
        maxZoneBottom = Math.max(maxZoneBottom, zone.y + zH);

        const zCards = zoneCards.get(zone.status) || [];
        if (zCards.length === 0) return;

        const items = zCards.map(c => {
            const { w, h } = getArrangeCardDims(c, cardElById);
            return { ref: c, width: w, height: h, rotation: c.rotation };
        });

        const packed = packItems(items, [], Math.max(1, zW - ARRANGE_PADDING_X * 2), {
            startX: zone.x + ARRANGE_PADDING_X,
            startY: zone.y + ARRANGE_PADDING_TOP,
            gapX: ARRANGE_GAP,
            gapY: ARRANGE_GAP
        });

        const contentLeft = Math.min(...packed.map(p => p.x));
        const contentRight = Math.max(...packed.map(p => p.x + p.width));
        const availableWidth = zW - ARRANGE_PADDING_X * 2;
        const offsetX = Math.max(0, (availableWidth - (contentRight - contentLeft)) / 2);

        packed.forEach(p => {
            p.ref.x = p.x + offsetX;
            p.ref.y = p.y;
        });
    });

    if (freeCards.length > 0) {
        const bcEl = document.getElementById('binder-container');
        const bcRect = bcEl ? bcEl.getBoundingClientRect() : { width: window.innerWidth };
        const currentScale = window.BinderManager ? window.BinderManager.getScale() : 1;
        const currentTranslate = window.BinderManager ? window.BinderManager.getTranslate() : { x: 0, y: 0 };

        const startX = (-currentTranslate.x / currentScale) + ARRANGE_FREE_MARGIN;
        let startY = (-currentTranslate.y / currentScale) + ARRANGE_FREE_MARGIN;

        const containerWidth = Math.max(ARRANGE_MIN_CONTAINER_WIDTH, (bcRect.width / currentScale) - ARRANGE_FREE_MARGIN * 2);

        const obstacles = [...pinnedObstacles];

        zones.forEach(zone => {
            const zEl = zoneElMap.get(String(zone.id));
            const zW = zEl ? zEl.offsetWidth : zone.width;
            const zH = zEl ? zEl.offsetHeight : zone.height;
            obstacles.push({
                x: zone.x - ARRANGE_GAP,
                y: zone.y - ARRANGE_GAP,
                width: zW + ARRANGE_GAP * 2,
                height: zH + ARRANGE_GAP * 2
            });
        });

        const infoRect = getInfoOverlayVirtualRect();
        if (infoRect) {
            obstacles.push({
                x: infoRect.x - ARRANGE_GAP,
                y: infoRect.y - ARRANGE_GAP,
                width: infoRect.width + ARRANGE_GAP * 2,
                height: infoRect.height + ARRANGE_GAP * 2
            });

            const infoBottom = infoRect.y + infoRect.height + ARRANGE_FREE_MARGIN;
            startY = Math.max(startY, infoBottom);
        }

        const clusters = clusterFreeCards(freeCards, mode);

        let clusterObstacles = [...obstacles];
        clusters.forEach(group => {
            if (group.length === 0) return;

            const items = group.map(c => {
                const { w, h } = getArrangeCardDims(c, cardElById);
                return { ref: c, width: w, height: h, rotation: c.rotation };
            });

            const packed = mode === 'position'
                ? relaxItems(items, clusterObstacles, containerWidth, { startX, startY })
                : packItems(items, clusterObstacles, containerWidth, {
                    startX,
                    startY,
                    gapX: ARRANGE_GAP,
                    gapY: ARRANGE_GAP
                });

            packed.forEach(p => {
                p.ref.x = p.x;
                p.ref.y = p.y;
            });

            clusterObstacles = clusterObstacles.concat(packed.map(p => {
                const aabb = computeAABBSize(p.width, p.height, p.rotation || 0);
                const aabbX = p.x - (aabb.width - p.width) / 2;
                const aabbY = p.y - (aabb.height - p.height) / 2;
                return {
                    x: aabbX - ARRANGE_CLUSTER_MARGIN,
                    y: aabbY - ARRANGE_CLUSTER_MARGIN,
                    width: aabb.width + ARRANGE_CLUSTER_MARGIN * 2,
                    height: aabb.height + ARRANGE_CLUSTER_MARGIN * 2
                };
            }));
        });
    }

    const movedPlacements = cards.filter(c => !c.pinned);
    await animateArrangeAndSave(movedPlacements, cardElById);
    showToast('カードを整列しました', 'success');
}

export async function arrangeSelectedCards(mode = 'position') {
    if (!state.currentBinder || !state.currentBinder.placements) return;

    const selectedEls = window.BinderManager ? window.BinderManager.getSelectedCards() : [];
    const targetIds = new Set(
        selectedEls.filter(el => !el.classList.contains('binder-zone') && el.dataset.id).map(el => el.dataset.id)
    );

    const placements = state.currentBinder.placements.filter(p =>
        (p.type === 'card' || p.type === 'binder') && targetIds.has(String(p.id))
    );

    if (placements.length < 2) {
        showToast('局所整列には2枚以上のカードを選択してください', 'info');
        return;
    }

    saveArrangeHistory(placements);

    const cardElById = new Map();
    document.querySelectorAll('.binder-card[data-id]').forEach(el => {
        cardElById.set(el.dataset.id, el);
    });

    const originX = Math.min(...placements.map(p => p.x));
    const originY = Math.min(...placements.map(p => p.y));

    const clusters = clusterFreeCards(placements, mode);

    let obstacles = [];
    clusters.forEach(group => {
        if (group.length === 0) return;

        const items = group.map(c => {
            const { w, h } = getArrangeCardDims(c, cardElById);
            return { ref: c, width: w, height: h, rotation: c.rotation };
        });

        const packed = mode === 'position'
            ? relaxItems(items, obstacles, Infinity, { startX: originX, startY: originY })
            : packItems(items, obstacles, Infinity, {
                startX: originX,
                startY: originY,
                gapX: ARRANGE_GAP,
                gapY: ARRANGE_GAP
            });

        packed.forEach(p => {
            p.ref.x = p.x;
            p.ref.y = p.y;
        });

        obstacles = obstacles.concat(packed.map(p => {
            const aabb = computeAABBSize(p.width, p.height, p.rotation || 0);
            const aabbX = p.x - (aabb.width - p.width) / 2;
            const aabbY = p.y - (aabb.height - p.height) / 2;
            return {
                x: aabbX - ARRANGE_CLUSTER_MARGIN,
                y: aabbY - ARRANGE_CLUSTER_MARGIN,
                width: aabb.width + ARRANGE_CLUSTER_MARGIN * 2,
                height: aabb.height + ARRANGE_CLUSTER_MARGIN * 2
            };
        }));
    });

    await animateArrangeAndSave(placements, cardElById);
    showToast(`${placements.length}件のカードを整列しました`, 'success');
}

export async function alignSelectedCards(direction) {
    if (!state.currentBinder || !state.currentBinder.placements) return;
    if (!['top', 'bottom', 'left', 'right'].includes(direction)) return;

    const selectedEls = window.BinderManager ? window.BinderManager.getSelectedCards() : [];
    const targetIds = new Set(
        selectedEls.filter(el => !el.classList.contains('binder-zone') && el.dataset.id).map(el => el.dataset.id)
    );

    const placements = state.currentBinder.placements.filter(p =>
        (p.type === 'card' || p.type === 'binder') && targetIds.has(String(p.id))
    );

    if (placements.length < 2) {
        showToast('スタック整列には2枚以上のカードを選択してください', 'info');
        return;
    }

    saveArrangeHistory(placements);

    const cardElById = new Map();
    document.querySelectorAll('.binder-card[data-id]').forEach(el => {
        cardElById.set(el.dataset.id, el);
    });
    const dims = new Map(placements.map(p => [p, getArrangeCardDims(p, cardElById)]));

    if (direction === 'left' || direction === 'right') {
        const baseX = direction === 'left'
            ? Math.min(...placements.map(p => p.x))
            : Math.max(...placements.map(p => p.x + dims.get(p).w));

        const sorted = [...placements].sort((a, b) => a.y - b.y);
        let cursorY = Math.min(...sorted.map(p => p.y));
        sorted.forEach(p => {
            const { w, h } = dims.get(p);
            p.x = direction === 'left' ? baseX : baseX - w;
            p.y = cursorY;
            cursorY += h + ARRANGE_GAP;
        });
    } else {
        const baseY = direction === 'top'
            ? Math.min(...placements.map(p => p.y))
            : Math.max(...placements.map(p => p.y + dims.get(p).h));

        const sorted = [...placements].sort((a, b) => a.x - b.x);
        let cursorX = Math.min(...sorted.map(p => p.x));
        sorted.forEach(p => {
            const { w, h } = dims.get(p);
            p.y = direction === 'top' ? baseY : baseY - h;
            p.x = cursorX;
            cursorX += w + ARRANGE_GAP;
        });
    }

    await animateArrangeAndSave(placements, cardElById);
    showToast('選択カードを整列しました', 'success');
}
