<script lang="ts">
	import { onMount, onDestroy } from 'svelte';
	import { goto } from '$app/navigation';
	import { toast } from 'svelte-sonner';
	import { Badge } from '$lib/components/ui/badge';
	import { Button } from '$lib/components/ui/button';
	import { Input } from '$lib/components/ui/input';
	import * as Tooltip from '$lib/components/ui/tooltip';
	import MultiSelectFilter from '$lib/components/MultiSelectFilter.svelte';
	import { Play, Square, Trash2, Plus, ArrowBigDown, Search, Pencil, GitBranch, RefreshCw, Loader2, FileCode, Box, RotateCcw, ScrollText, Terminal, Eye, Network, HardDrive, Heart, HeartPulse, HeartOff, ChevronsUpDown, ChevronsDownUp, ExternalLink, Rocket, AlertTriangle, X, Layers, Pause, CircleDashed, Skull, FolderOpen, Variable, Clock, RotateCw } from 'lucide-svelte';
	import ConfirmPopover from '$lib/components/ConfirmPopover.svelte';
	import BatchOperationModal from '$lib/components/BatchOperationModal.svelte';
	import type { ComposeStackInfo, ContainerStats } from '$lib/types';
	import StackModal from './StackModal.svelte';
	import GitStackModal from './GitStackModal.svelte';
	import GitDeployProgressPopover from './GitDeployProgressPopover.svelte';
	import ContainerInspectModal from '../containers/ContainerInspectModal.svelte';
	import FileBrowserModal from '../containers/FileBrowserModal.svelte';
	import { currentEnvironment, environments, appendEnvParam, clearStaleEnvironment } from '$lib/stores/environment';
	import { onDockerEvent, isContainerListChange } from '$lib/stores/events';
	import { canAccess } from '$lib/stores/auth';
	import { EmptyState, NoEnvironment } from '$lib/components/ui/empty-state';
	import PageHeader from '$lib/components/PageHeader.svelte';
	import { DataGrid } from '$lib/components/data-grid';
	import type { DataGridSortState } from '$lib/components/data-grid/types';

	type SortField = 'name' | 'containers' | 'status' | 'cpu' | 'memory';
	type SortDirection = 'asc' | 'desc';

	let stacks = $state<ComposeStackInfo[]>([]);
	let stackSources = $state<Record<string, { sourceType: string; repository?: any; gitStack?: any }>>({});
	let stackEnvVarCounts = $state<Record<string, number>>({});
	let gitStacks = $state<any[]>([]);
	let gitRepositories = $state<any[]>([]);
	let gitCredentials = $state<any[]>([]);
	let containerStats = $state<Map<string, ContainerStats>>(new Map());
	let containerStatsHistory = $state<Map<string, { cpu: number[]; mem: number[]; netRx: number[]; netTx: number[]; diskR: number[]; diskW: number[] }>>(new Map());
	let statsUpdateCount = $state(0); // Force reactivity counter
	const MAX_HISTORY = 20;
	let loading = $state(true);
	let showCreateModal = $state(false);
	let showEditModal = $state(false);
	let showGitModal = $state(false);
	let editingStackName = $state('');
	let editingGitStack = $state<any>(null);
	let envId = $state<number | null>(null);

	// Derived: current environment details for reactive port URL generation
	const currentEnvDetails = $derived($environments.find(e => e.id === $currentEnvironment?.id) ?? null);

	// Helper: extract host from URL (e.g., tcp://192.168.1.4:2376 -> 192.168.1.4)
	function extractHostFromUrl(urlString: string): string | null {
		if (!urlString) return null;
		try {
			// Handle tcp:// and other protocols
			const normalized = urlString.replace(/^tcp:\/\//, 'http://');
			const url = new URL(normalized);
			return url.hostname;
		} catch {
			// Try regex fallback for non-standard URLs
			const match = urlString.match(/(?:\/\/)?([^:/]+)/);
			return match?.[1] || null;
		}
	}

	// Helper: get clickable URL for a port
	function getPortUrl(publicPort: number): string | null {
		const env = currentEnvDetails;
		if (!env) return null;

		// Priority 1: Use publicIp if configured
		if (env.publicIp) {
			return `http://${env.publicIp}:${publicPort}`;
		}

		// Priority 2: Extract from host for direct/hawser-standard
		const connectionType = env.connectionType || 'socket';

		if (connectionType === 'direct' && env.host) {
			const host = extractHostFromUrl(env.host);
			if (host) return `http://${host}:${publicPort}`;
		} else if (connectionType === 'hawser-standard' && env.host) {
			const host = extractHostFromUrl(env.host);
			if (host) return `http://${host}:${publicPort}`;
		}

		// No public IP available for socket or hawser-edge
		return null;
	}

	// Helper: format uptime from status string
	function formatUptime(status: string): string {
		if (!status) return '-';
		const upMatch = status.match(/Up\s+(.+?)(?:\s+\(|$)/i);
		if (upMatch) return upMatch[1].trim();
		const exitMatch = status.match(/Exited.+?(\d+\s+\w+)\s+ago/i);
		if (exitMatch) return exitMatch[1] + ' ago';
		return '-';
	}

	// Helper: get container's primary IP address
	function getContainerIp(networks: Array<{ name: string; ipAddress: string }>): string {
		if (!networks || networks.length === 0) return '-';
		return networks[0]?.ipAddress || '-';
	}

	// Helper: format bytes to human readable
	function formatBytes(bytes: number, decimals = 1): string {
		if (bytes === 0) return '0B';
		const k = 1024;
		const sizes = ['B', 'K', 'M', 'G', 'T'];
		const i = Math.floor(Math.log(bytes) / Math.log(k));
		return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + sizes[i];
	}

	// Fetch container stats
	async function fetchStats() {
		try {
			const response = await fetch(appendEnvParam('/api/containers/stats', envId));
			const stats: ContainerStats[] = await response.json();
			const statsMap = new Map<string, ContainerStats>();
			const newHistory = new Map(containerStatsHistory);

			for (const stat of stats) {
				if (!stat.id) continue;
				statsMap.set(stat.id, stat);

				// Update history for all metrics
				const history = newHistory.get(stat.id) || { cpu: [], mem: [], netRx: [], netTx: [], diskR: [], diskW: [] };
				history.cpu = [...history.cpu.slice(-(MAX_HISTORY - 1)), stat.cpuPercent];
				history.mem = [...history.mem.slice(-(MAX_HISTORY - 1)), stat.memoryPercent];
				history.netRx = [...history.netRx.slice(-(MAX_HISTORY - 1)), stat.networkRx];
				history.netTx = [...history.netTx.slice(-(MAX_HISTORY - 1)), stat.networkTx];
				history.diskR = [...history.diskR.slice(-(MAX_HISTORY - 1)), stat.blockRead];
				history.diskW = [...history.diskW.slice(-(MAX_HISTORY - 1)), stat.blockWrite];
				newHistory.set(stat.id, history);
			}

			containerStats = statsMap;
			containerStatsHistory = newHistory;
			statsUpdateCount++; // Trigger reactivity for expanded rows
		} catch (error) {
			console.error('Failed to fetch container stats:', error);
		}
	}

	// Generate sparkline SVG path
	function generateSparklinePath(data: number[], width: number, height: number): string {
		if (data.length < 2) return '';
		const max = Math.max(...data, 1);
		const step = width / (data.length - 1);
		return data.map((val, i) => {
			const x = i * step;
			const y = height - (val / max) * height;
			return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
		}).join(' ');
	}

	// Generate area path for sparkline fill
	function generateAreaPath(data: number[], width: number, height: number): string {
		if (data.length < 2) return '';
		const max = Math.max(...data, 1);
		const step = width / (data.length - 1);
		const line = data.map((val, i) => {
			const x = i * step;
			const y = height - (val / max) * height;
			return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
		}).join(' ');
		return `${line} L ${width} ${height} L 0 ${height} Z`;
	}

	// Aggregate stats for a stack (sum of all running containers)
	interface StackStats {
		cpuPercent: number;
		memoryUsage: number;
		memoryLimit: number;
		networkRx: number;
		networkTx: number;
		blockRead: number;
		blockWrite: number;
		runningCount: number;
	}

	function getStackStats(stack: ComposeStackInfo): StackStats | null {
		if (!stack.containerDetails || stack.containerDetails.length === 0) return null;

		let cpuPercent = 0;
		let memoryUsage = 0;
		let memoryLimit = 0;
		let networkRx = 0;
		let networkTx = 0;
		let blockRead = 0;
		let blockWrite = 0;
		let runningCount = 0;

		for (const container of stack.containerDetails) {
			// Only aggregate stats from running containers
			if (container.state !== 'running') continue;

			const stats = containerStats.get(container.id);
			if (stats) {
				cpuPercent += stats.cpuPercent;
				memoryUsage += stats.memoryUsage;
				memoryLimit += stats.memoryLimit;
				networkRx += stats.networkRx;
				networkTx += stats.networkTx;
				blockRead += stats.blockRead;
				blockWrite += stats.blockWrite;
				runningCount++;
			}
		}

		if (runningCount === 0) return null;

		return { cpuPercent, memoryUsage, memoryLimit, networkRx, networkTx, blockRead, blockWrite, runningCount };
	}

	// Search and sort state
	let searchInput = $state('');
	let searchQuery = $state('');
	let sortField = $state<SortField>('name');
	let sortDirection = $state<SortDirection>('asc');

	// Status filter state
	const STATUS_FILTER_STORAGE_KEY = 'dockhand-stacks-status-filter';
	let statusFilter = $state<string[]>([]);

	// Stack status types with icons and colors
	const stackStatusTypes = [
		{ value: 'running', label: 'Running', icon: Play, color: 'text-emerald-500' },
		{ value: 'partial', label: 'Partial', icon: CircleDashed, color: 'text-amber-500' },
		{ value: 'stopped', label: 'Stopped', icon: Square, color: 'text-rose-500' },
		{ value: 'not deployed', label: 'Not deployed', icon: Rocket, color: 'text-violet-500' }
	];

	function getStackStatusIcon(status: string) {
		const s = stackStatusTypes.find(t => t.value === status.toLowerCase());
		return s?.icon || Layers;
	}

	function loadStatusFilter() {
		try {
			const stored = localStorage.getItem(STATUS_FILTER_STORAGE_KEY);
			if (stored) {
				const parsed = JSON.parse(stored);
				if (Array.isArray(parsed)) {
					statusFilter = parsed;
				}
			}
		} catch {
			// Ignore localStorage errors
		}
	}

	function saveStatusFilter() {
		try {
			localStorage.setItem(STATUS_FILTER_STORAGE_KEY, JSON.stringify(statusFilter));
		} catch {
			// Ignore localStorage errors
		}
	}

	// Auto-save status filter changes
	$effect(() => {
		const filter = statusFilter;
		saveStatusFilter();
	});

	// Confirmation popover state
	let confirmDeleteName = $state<string | null>(null);
	let confirmStopName = $state<string | null>(null);
	let confirmDownName = $state<string | null>(null);

	// Stack operation loading state
	let stackActionLoading = $state<string | null>(null);

	// Container-level confirmation popover state
	let confirmStopContainerId = $state<string | null>(null);
	let confirmRestartContainerId = $state<string | null>(null);
	let confirmPauseContainerId = $state<string | null>(null);

	// Operation error state (for stack and container operations)
	let operationError = $state<{ id: string; message: string } | null>(null);
	let errorTimeouts: ReturnType<typeof setTimeout>[] = [];

	function clearErrorAfterDelay(id: string) {
		const timeoutId = setTimeout(() => {
			if (operationError?.id === id) operationError = null;
		}, 5000);
		errorTimeouts.push(timeoutId);
	}

	// Container inspect modal state
	let showInspectModal = $state(false);
	let inspectContainerId = $state('');
	let inspectContainerName = $state('');

	// File browser modal state
	let showFileBrowserModal = $state(false);
	let fileBrowserContainerId = $state('');
	let fileBrowserContainerName = $state('');

	// Multi-select state
	let selectedStacks = $state<Set<string>>(new Set());
	let confirmBulkStart = $state(false);
	let confirmBulkStop = $state(false);
	let confirmBulkDown = $state(false);
	let confirmBulkRestart = $state(false);
	let confirmBulkRemove = $state(false);

	// Batch operation modal state
	let showBatchOpModal = $state(false);
	let batchOpTitle = $state('');
	let batchOpOperation = $state('');
	let batchOpItems = $state<Array<{ id: string; name: string }>>([]);

	// Filtered and sorted stacks
	const filteredStacks = $derived.by(() => {
		let result = stacks;

		// Filter by search query
		if (searchQuery.trim()) {
			const query = searchQuery.toLowerCase();
			result = result.filter(stack =>
				stack.name.toLowerCase().includes(query) ||
				stack.status.toLowerCase().includes(query)
			);
		}

		// Filter by status
		if (statusFilter.length > 0) {
			result = result.filter(stack => statusFilter.includes(stack.status.toLowerCase()));
		}

		// Sort
		result = [...result].sort((a, b) => {
			let cmp = 0;
			switch (sortField) {
				case 'name':
					cmp = a.name.localeCompare(b.name);
					break;
				case 'containers':
					cmp = a.containers.length - b.containers.length;
					break;
				case 'status':
					cmp = a.status.localeCompare(b.status);
					break;
				case 'cpu':
					const cpuA = getStackStats(a)?.cpuPercent ?? -1;
					const cpuB = getStackStats(b)?.cpuPercent ?? -1;
					cmp = cpuA - cpuB;
					break;
				case 'memory':
					const memA = getStackStats(a)?.memoryUsage ?? -1;
					const memB = getStackStats(b)?.memoryUsage ?? -1;
					cmp = memA - memB;
					break;
			}
			// Secondary sort by name for stability when primary values are equal
			if (cmp === 0 && sortField !== 'name') {
				cmp = a.name.localeCompare(b.name);
			}
			return sortDirection === 'asc' ? cmp : -cmp;
		});

		return result;
	});

	// Check if all filtered stacks are selected
	const allFilteredSelected = $derived(
		filteredStacks.length > 0 && filteredStacks.every(s => selectedStacks.has(s.name))
	);
	const someFilteredSelected = $derived(
		filteredStacks.some(s => selectedStacks.has(s.name)) && !allFilteredSelected
	);
	const selectedInFilter = $derived(
		filteredStacks.filter(s => selectedStacks.has(s.name))
	);

	// Count by status for selected stacks
	const selectedRunning = $derived(selectedInFilter.filter(s => s.status === 'running' || s.status === 'partial'));
	const selectedStopped = $derived(selectedInFilter.filter(s => s.status === 'stopped' || s.status === 'not deployed'));

	function toggleSelectAll() {
		if (allFilteredSelected) {
			filteredStacks.forEach(s => selectedStacks.delete(s.name));
		} else {
			filteredStacks.forEach(s => selectedStacks.add(s.name));
		}
		selectedStacks = new Set(selectedStacks);
	}

	function selectNone() {
		selectedStacks = new Set();
	}

	function toggleStackSelection(stackName: string) {
		if (selectedStacks.has(stackName)) {
			selectedStacks.delete(stackName);
		} else {
			selectedStacks.add(stackName);
		}
		selectedStacks = new Set(selectedStacks);
	}

	function bulkStart() {
		batchOpTitle = `Starting ${selectedStopped.length} stack${selectedStopped.length !== 1 ? 's' : ''}`;
		batchOpOperation = 'start';
		batchOpItems = selectedStopped.map(s => ({ id: s.name, name: s.name }));
		showBatchOpModal = true;
	}

	function bulkStop() {
		batchOpTitle = `Stopping ${selectedRunning.length} stack${selectedRunning.length !== 1 ? 's' : ''}`;
		batchOpOperation = 'stop';
		batchOpItems = selectedRunning.map(s => ({ id: s.name, name: s.name }));
		showBatchOpModal = true;
	}

	function bulkDown() {
		batchOpTitle = `Bringing down ${selectedRunning.length} stack${selectedRunning.length !== 1 ? 's' : ''}`;
		batchOpOperation = 'down';
		batchOpItems = selectedRunning.map(s => ({ id: s.name, name: s.name }));
		showBatchOpModal = true;
	}

	function bulkRestart() {
		batchOpTitle = `Restarting ${selectedRunning.length} stack${selectedRunning.length !== 1 ? 's' : ''}`;
		batchOpOperation = 'restart';
		batchOpItems = selectedRunning.map(s => ({ id: s.name, name: s.name }));
		showBatchOpModal = true;
	}

	function bulkRemove() {
		batchOpTitle = `Removing ${selectedInFilter.length} stack${selectedInFilter.length !== 1 ? 's' : ''}`;
		batchOpOperation = 'remove';
		batchOpItems = selectedInFilter.map(s => ({ id: s.name, name: s.name }));
		showBatchOpModal = true;
	}

	function handleBatchComplete() {
		selectedStacks = new Set();
		fetchStacks();
	}

	// Expanded rows state - load from localStorage
	const EXPANDED_STORAGE_KEY = 'dockhand-stacks-expanded';
	let expandedStacks = $state<Set<string>>(new Set());

	// Load expanded state from localStorage on init
	function loadExpandedState() {
		try {
			const stored = localStorage.getItem(EXPANDED_STORAGE_KEY);
			if (stored) {
				const parsed = JSON.parse(stored);
				if (Array.isArray(parsed)) {
					expandedStacks = new Set(parsed);
				}
			}
		} catch {
			// Ignore localStorage errors
		}
	}

	// Save expanded state to localStorage
	function saveExpandedState() {
		try {
			localStorage.setItem(EXPANDED_STORAGE_KEY, JSON.stringify(Array.from(expandedStacks)));
		} catch {
			// Ignore localStorage errors
		}
	}

	function toggleExpand(stackName: string) {
		if (expandedStacks.has(stackName)) {
			expandedStacks.delete(stackName);
		} else {
			expandedStacks.add(stackName);
		}
		expandedStacks = new Set(expandedStacks);
		saveExpandedState();
	}

	function expandAll() {
		expandedStacks = new Set(stacks.map(s => s.name));
		saveExpandedState();
	}

	function collapseAll() {
		expandedStacks = new Set();
		saveExpandedState();
	}

	// Check if all stacks are expanded
	const allExpanded = $derived(stacks.length > 0 && stacks.every(s => expandedStacks.has(s.name)));
	const someExpanded = $derived(expandedStacks.size > 0);

	// Debounce search input
	let searchTimeout: ReturnType<typeof setTimeout>;
	$effect(() => {
		const input = searchInput; // Track dependency
		clearTimeout(searchTimeout);
		searchTimeout = setTimeout(() => {
			searchQuery = input;
		}, 200);
		return () => clearTimeout(searchTimeout);
	});

	// Track last loaded environment to show skeleton on environment change
	let lastLoadedEnvId = $state<number | null>(null);

	// Track if initial fetch has been done
	let initialFetchDone = $state(false);

	// Subscribe to environment changes using $effect
	$effect(() => {
		const env = $currentEnvironment;
		const newEnvId = env?.id ?? null;

		// Only fetch if environment actually changed or this is initial load
		if (env && (newEnvId !== envId || !initialFetchDone)) {
			envId = newEnvId;
			initialFetchDone = true;
			fetchStacks();
			fetchStats();
		} else if (!env) {
			// No environment - clear data and stop loading
			envId = null;
			stacks = [];
			containerStats = new Map();
			loading = false;
		}
	});

	async function fetchStacks() {
		// Show loading skeleton on initial load or when environment changes, but not on refreshes
		if (lastLoadedEnvId !== envId) {
			loading = true;
		}
		try {
			const [stacksRes, sourcesRes, gitStacksRes] = await Promise.all([
				fetch(appendEnvParam('/api/stacks', envId)),
				fetch(appendEnvParam('/api/stacks/sources', envId)),
				fetch(appendEnvParam('/api/git/stacks', envId))
			]);

			// Handle stale environment ID (e.g., after database reset)
			if (stacksRes.status === 404 && envId) {
				console.warn(`[Stacks] Got 404 for env ${envId}, refreshing environments`);
				clearStaleEnvironment(envId);
				environments.refresh();
				return;
			}

			const dockerStacks = await stacksRes.json();
			const sourcesData = await sourcesRes.json();
			const gitStacksData = await gitStacksRes.json();

			// Debug logging
			if (gitStacksData?.error) {
				console.error('Git stacks API error:', gitStacksData.error, 'Status:', gitStacksRes.status);
			}

			// Ensure responses are valid before using them
			stackSources = sourcesData && !sourcesData.error ? sourcesData : {};
			gitStacks = Array.isArray(gitStacksData) ? gitStacksData : [];

			// Create a set of docker stack names for quick lookup
			const dockerStackNames = new Set(dockerStacks.map((s: ComposeStackInfo) => s.name));

			// Add undeployed git stacks as placeholder entries
			const undeployedGitStacks: ComposeStackInfo[] = gitStacks
				.filter((gs: any) => !dockerStackNames.has(gs.stackName))
				.map((gs: any) => ({
					name: gs.stackName,
					status: 'not deployed',
					containers: [],
					containerDetails: [],
					configFile: gs.composePath,
					workingDir: ''
				}));

			// Add gitStack to all git-based stacks (both deployed and undeployed)
			for (const gs of gitStacks) {
				if (!stackSources[gs.stackName]) {
					// Undeployed git stack - create new source entry
					stackSources[gs.stackName] = {
						sourceType: 'git',
						repository: gs.repository,
						gitStack: gs
					};
				} else if (stackSources[gs.stackName].sourceType === 'git') {
					// Deployed git stack - add gitStack to existing source
					stackSources[gs.stackName].gitStack = gs;
				}
			}

			stacks = [...dockerStacks, ...undeployedGitStacks];

			// Fetch env var counts for internal and git stacks (in background, don't block UI)
			const allStackNames = stacks.map(s => s.name);
			fetchEnvVarCounts(allStackNames, sourcesData);
		} catch (error) {
			console.error('Failed to fetch stacks:', error);
			toast.error('Failed to load stacks');
		} finally {
			loading = false;
			lastLoadedEnvId = envId;
		}
	}

	async function fetchEnvVarCounts(stackNames: string[], sources: Record<string, any>) {
		// Only fetch for stacks that can have env vars (internal or git)
		const stacksToFetch = stackNames.filter(name => {
			const source = sources[name];
			return source && (source.sourceType === 'internal' || source.sourceType === 'git');
		});

		if (stacksToFetch.length === 0) {
			stackEnvVarCounts = {};
			return;
		}

		const counts: Record<string, number> = {};

		// Fetch in parallel with error handling
		await Promise.all(stacksToFetch.map(async (stackName) => {
			try {
				const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/env`, envId));
				if (response.ok) {
					const data = await response.json();
					const varCount = data.variables?.length || 0;
					if (varCount > 0) {
						counts[stackName] = varCount;
					}
				}
			} catch (e) {
				// Ignore errors for individual stack env var fetches
			}
		}));

		stackEnvVarCounts = counts;
	}

	function getStackSource(stackName: string) {
		return stackSources[stackName] || { sourceType: 'external' };
	}

	async function openGitModal(gitStack?: any) {
		editingGitStack = gitStack || null;
		// Fetch repositories and credentials before opening modal
		try {
			const [reposRes, credsRes] = await Promise.all([
				fetch('/api/git/repositories'),
				fetch('/api/git/credentials')
			]);
			gitRepositories = await reposRes.json();
			gitCredentials = await credsRes.json();
		} catch (error) {
			console.error('Failed to fetch git data:', error);
			gitRepositories = [];
			gitCredentials = [];
		}
		showGitModal = true;
	}

	async function startStack(name: string) {
		operationError = null;
		stackActionLoading = name;
		try {
			const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(name)}/start`, envId), { method: 'POST' });
			if (!response.ok) {
				const data = await response.json();
				operationError = { id: name, message: data.error || 'Failed to start stack' };
				toast.error(`Failed to start ${name}`);
				clearErrorAfterDelay(name);
				return;
			}
			toast.success(`Started ${name}`);
			await fetchStacks();
		} catch (error) {
			console.error('Failed to start stack:', error);
			operationError = { id: name, message: 'Failed to start stack' };
			toast.error(`Failed to start ${name}`);
			clearErrorAfterDelay(name);
		} finally {
			stackActionLoading = null;
		}
	}

	async function stopStack(name: string) {
		operationError = null;
		stackActionLoading = name;
		try {
			const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(name)}/stop`, envId), { method: 'POST' });
			if (!response.ok) {
				const data = await response.json();
				operationError = { id: name, message: data.error || 'Failed to stop stack' };
				toast.error(`Failed to stop ${name}`);
				clearErrorAfterDelay(name);
				return;
			}
			toast.success(`Stopped ${name}`);
			await fetchStacks();
		} catch (error) {
			console.error('Failed to stop stack:', error);
			operationError = { id: name, message: 'Failed to stop stack' };
			toast.error(`Failed to stop ${name}`);
			clearErrorAfterDelay(name);
		} finally {
			stackActionLoading = null;
		}
	}

	async function restartStack(name: string) {
		operationError = null;
		stackActionLoading = name;
		try {
			const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(name)}/restart`, envId), { method: 'POST' });
			if (!response.ok) {
				const data = await response.json();
				operationError = { id: name, message: data.error || 'Failed to restart stack' };
				toast.error(`Failed to restart ${name}`);
				clearErrorAfterDelay(name);
				return;
			}
			toast.success(`Restarted ${name}`);
			await fetchStacks();
		} catch (error) {
			console.error('Failed to restart stack:', error);
			operationError = { id: name, message: 'Failed to restart stack' };
			toast.error(`Failed to restart ${name}`);
			clearErrorAfterDelay(name);
		} finally {
			stackActionLoading = null;
		}
	}

	async function downStack(name: string) {
		operationError = null;
		stackActionLoading = name;
		try {
			const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(name)}/down`, envId), { method: 'POST' });
			if (!response.ok) {
				const data = await response.json();
				operationError = { id: name, message: data.error || 'Failed to bring down stack' };
				toast.error(`Failed to bring down ${name}`);
				clearErrorAfterDelay(name);
				return;
			}
			toast.success(`Brought down ${name}`);
			await fetchStacks();
		} catch (error) {
			console.error('Failed to bring down stack:', error);
			operationError = { id: name, message: 'Failed to bring down stack' };
			toast.error(`Failed to bring down ${name}`);
			clearErrorAfterDelay(name);
		} finally {
			stackActionLoading = null;
		}
	}

	function viewStackLogs(stack: ComposeStackInfo) {
		// Navigate to logs page with all containers from the stack (grouped mode)
		// Use containerDetails for full info, or fall back to containers (which is already string[])
		const containerIds = stack.containerDetails
			?.map(c => c.id)
			.filter((id): id is string => !!id)
			.join(',') || stack.containers?.filter(Boolean).join(',');
		if (containerIds) {
			const url = appendEnvParam(`/logs?containers=${containerIds}&stack=${encodeURIComponent(stack.name)}`, envId);
			goto(url);
		}
	}

	async function removeStack(name: string) {
		operationError = null;
		try {
			const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(name)}?force=true`, envId), { method: 'DELETE' });
			if (!response.ok) {
				const data = await response.json();
				operationError = { id: name, message: data.error || 'Failed to remove stack' };
				toast.error(`Failed to remove ${name}`);
				clearErrorAfterDelay(name);
				return;
			}
			toast.success(`Removed ${name}`);
			await fetchStacks();
		} catch (error) {
			console.error('Failed to remove stack:', error);
			operationError = { id: name, message: 'Failed to remove stack' };
			toast.error(`Failed to remove ${name}`);
			clearErrorAfterDelay(name);
		}
	}

	function editStack(name: string) {
		editingStackName = name;
		showEditModal = true;
	}

	function getStatusClasses(status: string): string {
		const base = 'text-xs px-1.5 py-0.5 rounded-sm font-medium inline-flex items-center gap-1 w-[6rem] justify-center shadow-sm whitespace-nowrap';
		switch (status.toLowerCase()) {
			case 'running':
				return `${base} bg-emerald-200 dark:bg-emerald-800 text-emerald-900 dark:text-emerald-100`;
			case 'stopped':
				return `${base} bg-red-200 dark:bg-red-800 text-red-900 dark:text-red-100`;
			case 'partial':
				return `${base} bg-amber-200 dark:bg-amber-800 text-amber-900 dark:text-amber-100`;
			case 'not deployed':
				return `${base} bg-violet-200 dark:bg-violet-800 text-violet-900 dark:text-violet-100`;
			default:
				return `${base} bg-slate-200 dark:bg-slate-700 text-slate-900 dark:text-slate-100`;
		}
	}

	function toggleSort(field: SortField) {
		if (sortField === field) {
			sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
		} else {
			sortField = field;
			sortDirection = field === 'containers' ? 'desc' : 'asc';
		}
	}

	// Container actions
	let containerActionLoading = $state<string | null>(null);

	async function startContainer(containerId: string, e: Event) {
		e.stopPropagation();
		operationError = null;
		containerActionLoading = containerId;
		try {
			const response = await fetch(appendEnvParam(`/api/containers/${containerId}/start`, envId), { method: 'POST' });
			if (!response.ok) {
				const data = await response.json();
				operationError = { id: containerId, message: data.error || 'Failed to start container' };
				toast.error('Failed to start container');
				clearErrorAfterDelay(containerId);
				return;
			}
			toast.success('Container started');
			await fetchStacks();
		} catch (error) {
			console.error('Failed to start container:', error);
			operationError = { id: containerId, message: 'Failed to start container' };
			toast.error('Failed to start container');
			clearErrorAfterDelay(containerId);
		} finally {
			containerActionLoading = null;
		}
	}

	async function stopContainer(containerId: string) {
		operationError = null;
		containerActionLoading = containerId;
		try {
			const response = await fetch(appendEnvParam(`/api/containers/${containerId}/stop`, envId), { method: 'POST' });
			if (!response.ok) {
				const data = await response.json();
				operationError = { id: containerId, message: data.error || 'Failed to stop container' };
				toast.error('Failed to stop container');
				clearErrorAfterDelay(containerId);
				return;
			}
			toast.success('Container stopped');
			await fetchStacks();
		} catch (error) {
			console.error('Failed to stop container:', error);
			operationError = { id: containerId, message: 'Failed to stop container' };
			toast.error('Failed to stop container');
			clearErrorAfterDelay(containerId);
		} finally {
			containerActionLoading = null;
		}
	}

	async function restartContainer(containerId: string) {
		operationError = null;
		containerActionLoading = containerId;
		try {
			const response = await fetch(appendEnvParam(`/api/containers/${containerId}/restart`, envId), { method: 'POST' });
			if (!response.ok) {
				const data = await response.json();
				operationError = { id: containerId, message: data.error || 'Failed to restart container' };
				toast.error('Failed to restart container');
				clearErrorAfterDelay(containerId);
				return;
			}
			toast.success('Container restarted');
			await fetchStacks();
		} catch (error) {
			console.error('Failed to restart container:', error);
			operationError = { id: containerId, message: 'Failed to restart container' };
			toast.error('Failed to restart container');
			clearErrorAfterDelay(containerId);
		} finally {
			containerActionLoading = null;
		}
	}

	async function pauseContainer(containerId: string) {
		operationError = null;
		containerActionLoading = containerId;
		try {
			const response = await fetch(appendEnvParam(`/api/containers/${containerId}/pause`, envId), { method: 'POST' });
			if (!response.ok) {
				const data = await response.json();
				operationError = { id: containerId, message: data.error || 'Failed to pause container' };
				toast.error('Failed to pause container');
				clearErrorAfterDelay(containerId);
				return;
			}
			toast.success('Container paused');
			await fetchStacks();
		} catch (error) {
			console.error('Failed to pause container:', error);
			operationError = { id: containerId, message: 'Failed to pause container' };
			toast.error('Failed to pause container');
			clearErrorAfterDelay(containerId);
		} finally {
			containerActionLoading = null;
		}
	}

	async function unpauseContainer(containerId: string, e: Event) {
		e.stopPropagation();
		operationError = null;
		containerActionLoading = containerId;
		try {
			const response = await fetch(appendEnvParam(`/api/containers/${containerId}/unpause`, envId), { method: 'POST' });
			if (!response.ok) {
				const data = await response.json();
				operationError = { id: containerId, message: data.error || 'Failed to unpause container' };
				toast.error('Failed to unpause container');
				clearErrorAfterDelay(containerId);
				return;
			}
			toast.success('Container unpaused');
			await fetchStacks();
		} catch (error) {
			console.error('Failed to unpause container:', error);
			operationError = { id: containerId, message: 'Failed to unpause container' };
			toast.error('Failed to unpause container');
			clearErrorAfterDelay(containerId);
		} finally {
			containerActionLoading = null;
		}
	}

	function inspectContainer(containerId: string, containerName: string) {
		inspectContainerId = containerId;
		inspectContainerName = containerName;
		showInspectModal = true;
	}

	function browseFiles(containerId: string, containerName: string) {
		fileBrowserContainerId = containerId;
		fileBrowserContainerName = containerName;
		showFileBrowserModal = true;
	}

	function getHealthClasses(health: string | undefined): string {
		switch (health) {
			case 'healthy':
				return 'text-emerald-500';
			case 'unhealthy':
				return 'text-red-500';
			case 'starting':
				return 'text-amber-500';
			default:
				return 'text-muted-foreground';
		}
	}

	function getContainerStateCounts(stack: ComposeStackInfo): Record<string, number> {
		const counts: Record<string, number> = {};
		if (stack.containerDetails) {
			for (const container of stack.containerDetails) {
				const state = container.state.toLowerCase();
				counts[state] = (counts[state] || 0) + 1;
			}
		}
		return counts;
	}

	function getStackNetworkCount(stack: ComposeStackInfo): number {
		if (!stack.containerDetails) return 0;
		const uniqueNetworks = new Set<string>();
		for (const container of stack.containerDetails) {
			for (const network of container.networks) {
				uniqueNetworks.add(network.name);
			}
		}
		return uniqueNetworks.size;
	}

	function getStackVolumeCount(stack: ComposeStackInfo): number {
		if (!stack.containerDetails) return 0;
		return stack.containerDetails.reduce((sum, c) => sum + c.volumeCount, 0);
	}

	// Handle tab visibility changes (e.g., user switches back from another tab)
	function handleVisibilityChange() {
		if (document.visibilityState === 'visible' && envId) {
			fetchStacks();
			fetchStats();
		}
	}

	onMount(() => {
		loadExpandedState();
		loadStatusFilter();

		// Initial fetch is handled by $effect - no need to duplicate here

		// Listen for tab visibility changes to refresh when user returns
		document.addEventListener('visibilitychange', handleVisibilityChange);
		document.addEventListener('resume', handleVisibilityChange);

		// Subscribe to container events (stacks are identified by container labels)
		const unsubscribe = onDockerEvent((event) => {
			if (envId && isContainerListChange(event)) {
				fetchStacks();
				fetchStats();
			}
		});

		// Refresh stacks every 30 seconds
		const stacksInterval = setInterval(() => {
			if (envId) fetchStacks();
		}, 30000);

		// Refresh stats every 5 seconds (faster for resource monitoring)
		const statsInterval = setInterval(() => {
			if (envId) fetchStats();
		}, 5000);

		return () => {
			clearInterval(stacksInterval);
			clearInterval(statsInterval);
			unsubscribe();
		};
	});

	onDestroy(() => {
		document.removeEventListener('visibilitychange', handleVisibilityChange);
		document.removeEventListener('resume', handleVisibilityChange);
		errorTimeouts.forEach(id => clearTimeout(id));
		errorTimeouts = [];
	});
</script>

<div class="flex-1 min-h-0 flex flex-col gap-3 overflow-hidden">
	<div class="shrink-0 flex flex-wrap justify-between items-center gap-3">
		<PageHeader icon={Layers} title="Compose stacks" count={stacks.length}>
			{#if stacks.length > 0}
				<button
					type="button"
					onclick={allExpanded ? collapseAll : expandAll}
					class="inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded-full border border-border hover:border-foreground/30 hover:shadow-sm transition-all cursor-pointer text-muted-foreground hover:text-foreground"
					title={allExpanded ? 'Collapse all' : 'Expand all'}
				>
					{#if allExpanded}
						<ChevronsDownUp class="w-3 h-3" />
						Collapse
					{:else}
						<ChevronsUpDown class="w-3 h-3" />
						Expand
					{/if}
				</button>
			{/if}
		</PageHeader>
		<div class="flex flex-wrap items-center gap-2">
			<div class="relative">
				<Search class="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
				<Input
					type="text"
					placeholder="Search stacks..."
					bind:value={searchInput}
					onkeydown={(e) => e.key === 'Escape' && (searchInput = '')}
					class="pl-8 h-8 w-48 text-sm"
				/>
			</div>
			<MultiSelectFilter
				bind:value={statusFilter}
				options={stackStatusTypes}
				placeholder="All statuses"
				pluralLabel="statuses"
				width="w-44"
				defaultIcon={Layers}
			/>
			<Button size="sm" variant="outline" onclick={fetchStacks}>
				<RefreshCw class="w-3.5 h-3.5 mr-1" />
				Refresh
			</Button>
			{#if $canAccess('stacks', 'create')}
				<Button size="sm" variant="outline" onclick={() => openGitModal()}>
					<GitBranch class="w-3.5 h-3.5 mr-1" />
					From Git
				</Button>
				<Button size="sm" variant="secondary" onclick={() => showCreateModal = true}>
					<Plus class="w-3.5 h-3.5 mr-1" />
					Create
				</Button>
			{/if}
		</div>
	</div>

	<!-- Selection bar -->
	{#if selectedStacks.size > 0}
		<div class="flex items-center gap-2 text-xs text-muted-foreground">
			<span>{selectedInFilter.length} selected</span>
			<button
				type="button"
				class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-border shadow-sm hover:border-foreground/30 hover:shadow transition-all"
				onclick={selectNone}
			>
				Clear
			</button>
			{#if selectedStopped.length > 0 && $canAccess('stacks', 'start')}
				<ConfirmPopover
					open={confirmBulkStart}
					action="Start"
					itemType="stacks"
					itemName="{selectedStopped.length} stack{selectedStopped.length !== 1 ? 's' : ''}"
					title="Start {selectedStopped.length}"
					variant="secondary"
					unstyled
					onConfirm={bulkStart}
					onOpenChange={(open) => confirmBulkStart = open}
				>
					{#snippet children({ open })}
						<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-border shadow-sm hover:text-green-600 hover:border-green-500/40 hover:shadow transition-all cursor-pointer">
							<Play class="w-3 h-3" />
							Start
						</span>
					{/snippet}
				</ConfirmPopover>
			{/if}
			{#if selectedRunning.length > 0 && $canAccess('stacks', 'restart')}
				<ConfirmPopover
					open={confirmBulkRestart}
					action="Restart"
					itemType="stacks"
					itemName="{selectedRunning.length} stack{selectedRunning.length !== 1 ? 's' : ''}"
					title="Restart {selectedRunning.length}"
					variant="secondary"
					unstyled
					onConfirm={bulkRestart}
					onOpenChange={(open) => confirmBulkRestart = open}
				>
					{#snippet children({ open })}
						<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-border shadow-sm hover:text-amber-600 hover:border-amber-500/40 hover:shadow transition-all cursor-pointer">
							<RotateCcw class="w-3 h-3" />
							Restart
						</span>
					{/snippet}
				</ConfirmPopover>
			{/if}
			{#if selectedRunning.length > 0 && $canAccess('stacks', 'stop')}
				<ConfirmPopover
					open={confirmBulkStop}
					action="Stop"
					itemType="stacks"
					itemName="{selectedRunning.length} stack{selectedRunning.length !== 1 ? 's' : ''}"
					title="Stop {selectedRunning.length}"
					unstyled
					onConfirm={bulkStop}
					onOpenChange={(open) => confirmBulkStop = open}
				>
					{#snippet children({ open })}
						<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-border shadow-sm hover:text-red-600 hover:border-red-500/40 hover:shadow transition-all cursor-pointer">
							<Square class="w-3 h-3" />
							Stop
						</span>
					{/snippet}
				</ConfirmPopover>
			{/if}
			{#if selectedRunning.length > 0 && $canAccess('stacks', 'stop')}
				<ConfirmPopover
					open={confirmBulkDown}
					action="Down"
					itemType="stacks"
					itemName="{selectedRunning.length} stack{selectedRunning.length !== 1 ? 's' : ''}"
					title="Down {selectedRunning.length}"
					unstyled
					onConfirm={bulkDown}
					onOpenChange={(open) => confirmBulkDown = open}
				>
					{#snippet children({ open })}
						<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-border shadow-sm hover:text-orange-600 hover:border-orange-500/40 hover:shadow transition-all cursor-pointer">
							<ArrowBigDown class="w-3 h-3" />
							Down
						</span>
					{/snippet}
				</ConfirmPopover>
			{/if}
			{#if $canAccess('stacks', 'remove')}
			<ConfirmPopover
				open={confirmBulkRemove}
				action="Remove"
				itemType="stacks"
				itemName="{selectedInFilter.length} stack{selectedInFilter.length !== 1 ? 's' : ''}"
				title="Remove {selectedInFilter.length}"
				unstyled
				onConfirm={bulkRemove}
				onOpenChange={(open) => confirmBulkRemove = open}
			>
				{#snippet children({ open })}
					<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-border shadow-sm hover:text-destructive hover:border-destructive/40 hover:shadow transition-all cursor-pointer">
						<Trash2 class="w-3 h-3" />
						Remove
					</span>
				{/snippet}
			</ConfirmPopover>
			{/if}
		</div>
	{/if}

	{#if !loading && ($environments.length === 0 || !$currentEnvironment)}
		<NoEnvironment />
	{:else if !loading && stacks.length === 0}
		<EmptyState
			icon={Layers}
			title="No compose stacks found"
			description="Create a stack or deploy from Git to get started"
		/>
	{:else}
		<DataGrid
			data={filteredStacks}
			keyField="name"
			gridId="stacks"
			loading={loading}
			selectable
			bind:selectedKeys={selectedStacks}
			expandable
			bind:expandedKeys={expandedStacks}
			onExpandChange={(key, expanded) => saveExpandedState()}
			sortState={{ field: sortField, direction: sortDirection }}
			onSortChange={(state) => {
				sortField = state.field as SortField;
				sortDirection = state.direction;
			}}
			onRowClick={(stack, e) => {
				const hasContainers = stack.containers && stack.containers.length > 0;
				if (hasContainers) {
					toggleExpand(stack.name);
				}
			}}
			rowClass={(stack) => {
				const isExp = expandedStacks.has(stack.name);
				const isSel = selectedStacks.has(stack.name);
				return `${isExp ? 'bg-muted/40' : ''} ${isSel ? 'bg-muted/30' : ''}`;
			}}
		>
			{#snippet cell(column, stack, rowState)}
				{@const source = getStackSource(stack.name)}
				{#if column.id === 'name'}
					<span class="font-medium text-xs">{stack.name}</span>
					{#if stackEnvVarCounts[stack.name]}
						<Tooltip.Root>
							<Tooltip.Trigger>
								<span class="inline-flex items-center gap-0.5 ml-1.5 text-2xs px-1 py-0.5 rounded bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300">
									<Variable class="w-2.5 h-2.5" />
									{stackEnvVarCounts[stack.name]}
								</span>
							</Tooltip.Trigger>
							<Tooltip.Content class="whitespace-nowrap">
								{stackEnvVarCounts[stack.name]} environment variable{stackEnvVarCounts[stack.name] !== 1 ? 's' : ''} configured
							</Tooltip.Content>
						</Tooltip.Root>
					{/if}
				{:else if column.id === 'source'}
					{#if source.sourceType === 'git'}
						<Tooltip.Root>
							<Tooltip.Trigger>
								<span class="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-sm bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 shadow-sm">
									<GitBranch class="w-3 h-3" />
									Git
								</span>
							</Tooltip.Trigger>
							<Tooltip.Content>
								{#if source.repository}
									{source.repository.url} ({source.repository.branch})
								{:else}
									Deployed from Git repository
								{/if}
							</Tooltip.Content>
						</Tooltip.Root>
					{:else if source.sourceType === 'internal'}
						<Tooltip.Root>
							<Tooltip.Trigger>
								<span class="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-sm bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 shadow-sm">
									<FileCode class="w-3 h-3" />
									Internal
								</span>
							</Tooltip.Trigger>
							<Tooltip.Content class="whitespace-nowrap">Created in Dockhand</Tooltip.Content>
						</Tooltip.Root>
					{:else}
						<Tooltip.Root>
							<Tooltip.Trigger>
								<span class="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-sm bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200 shadow-sm">
									<ExternalLink class="w-3 h-3" />
									External
								</span>
							</Tooltip.Trigger>
							<Tooltip.Content class="whitespace-nowrap">Created outside Dockhand</Tooltip.Content>
						</Tooltip.Root>
					{/if}
				{:else if column.id === 'containers'}
					<div class="flex items-center gap-1">
						{#if getContainerStateCounts(stack).running}
							<span class="inline-flex items-center gap-0.5 text-emerald-600 dark:text-emerald-400" title="Running">
								<Play class="w-3.5 h-3.5" />
								<span class="text-xs font-medium">{getContainerStateCounts(stack).running}</span>
							</span>
						{/if}
						{#if getContainerStateCounts(stack).exited}
							<span class="inline-flex items-center gap-0.5 text-red-600 dark:text-red-400" title="Exited">
								<Square class="w-3.5 h-3.5" />
								<span class="text-xs font-medium">{getContainerStateCounts(stack).exited}</span>
							</span>
						{/if}
						{#if getContainerStateCounts(stack).paused}
							<span class="inline-flex items-center gap-0.5 text-amber-600 dark:text-amber-400" title="Paused">
								<Pause class="w-3.5 h-3.5" />
								<span class="text-xs font-medium">{getContainerStateCounts(stack).paused}</span>
							</span>
						{/if}
						{#if getContainerStateCounts(stack).restarting}
							<span class="inline-flex items-center gap-0.5 text-blue-600 dark:text-blue-400" title="Restarting">
								<span class="w-3.5 h-3.5 flex items-center justify-center"><RefreshCw class="w-3.5 h-3.5 animate-spin" /></span>
								<span class="text-xs font-medium">{getContainerStateCounts(stack).restarting}</span>
							</span>
						{/if}
						{#if getContainerStateCounts(stack).created}
							<span class="inline-flex items-center gap-0.5 text-slate-500 dark:text-slate-400" title="Created">
								<CircleDashed class="w-3.5 h-3.5" />
								<span class="text-xs font-medium">{getContainerStateCounts(stack).created}</span>
							</span>
						{/if}
						{#if getContainerStateCounts(stack).dead}
							<span class="inline-flex items-center gap-0.5 text-rose-700 dark:text-rose-400" title="Dead">
								<Skull class="w-3.5 h-3.5" />
								<span class="text-xs font-medium">{getContainerStateCounts(stack).dead}</span>
							</span>
						{/if}
						{#if stack.containers.length === 0}
							<span class="text-xs text-muted-foreground">-</span>
						{/if}
					</div>
				{:else if column.id === 'cpu'}
					{@const stats = getStackStats(stack)}
					<div class="text-right">
						{#if stats}
							<span class="text-xs font-mono {stats.cpuPercent > 80 ? 'text-red-500' : stats.cpuPercent > 50 ? 'text-yellow-500' : 'text-muted-foreground'}">{stats.cpuPercent.toFixed(1)}%</span>
						{:else if stack.status === 'running' || stack.status === 'partial'}
							<span class="text-xs text-muted-foreground/50">...</span>
						{:else}
							<span class="text-gray-400 dark:text-gray-600 text-xs">-</span>
						{/if}
					</div>
				{:else if column.id === 'memory'}
					{@const stats = getStackStats(stack)}
					<div class="text-right">
						{#if stats}
							<span class="text-xs font-mono text-muted-foreground" title="{formatBytes(stats.memoryUsage)} / {formatBytes(stats.memoryLimit)}">{formatBytes(stats.memoryUsage)}</span>
						{:else if stack.status === 'running' || stack.status === 'partial'}
							<span class="text-xs text-muted-foreground/50">...</span>
						{:else}
							<span class="text-gray-400 dark:text-gray-600 text-xs">-</span>
						{/if}
					</div>
				{:else if column.id === 'networkIO'}
					{@const stats = getStackStats(stack)}
					<div class="text-right whitespace-nowrap">
						{#if stats}
							<span class="text-xs font-mono text-muted-foreground" title="↓{formatBytes(stats.networkRx)} received / ↑{formatBytes(stats.networkTx)} sent">
								<span class="text-2xs text-blue-400">↓</span>{formatBytes(stats.networkRx, 0)} <span class="text-2xs text-orange-400">↑</span>{formatBytes(stats.networkTx, 0)}
							</span>
						{:else if stack.status === 'running' || stack.status === 'partial'}
							<span class="text-xs text-muted-foreground/50">...</span>
						{:else}
							<span class="text-gray-400 dark:text-gray-600 text-xs">-</span>
						{/if}
					</div>
				{:else if column.id === 'diskIO'}
					{@const stats = getStackStats(stack)}
					<div class="text-right whitespace-nowrap">
						{#if stats}
							<span class="text-xs font-mono text-muted-foreground" title="↓{formatBytes(stats.blockRead)} read / ↑{formatBytes(stats.blockWrite)} written">
								<span class="text-2xs text-green-400">r</span>{formatBytes(stats.blockRead, 0)} <span class="text-2xs text-yellow-400">w</span>{formatBytes(stats.blockWrite, 0)}
							</span>
						{:else if stack.status === 'running' || stack.status === 'partial'}
							<span class="text-xs text-muted-foreground/50">...</span>
						{:else}
							<span class="text-gray-400 dark:text-gray-600 text-xs">-</span>
						{/if}
					</div>
				{:else if column.id === 'networks'}
					<span class="text-xs text-muted-foreground">
						{getStackNetworkCount(stack) || '-'}
					</span>
				{:else if column.id === 'volumes'}
					<span class="text-xs text-muted-foreground">
						{getStackVolumeCount(stack) || '-'}
					</span>
				{:else if column.id === 'status'}
					{@const StatusIcon = getStackStatusIcon(stack.status)}
					<span class={getStatusClasses(stack.status)}>
						<StatusIcon class="w-3 h-3" />
						{stack.status}
					</span>
				{:else if column.id === 'actions'}
					<div class="relative flex gap-1 justify-end">
						{#if operationError?.id === stack.name && operationError?.message}
							<div class="absolute bottom-full right-0 mb-1 z-50 bg-destructive text-destructive-foreground rounded-md shadow-lg p-2 text-xs flex items-start gap-2 max-w-lg w-max">
								<AlertTriangle class="w-3 h-3 flex-shrink-0 mt-0.5" />
								<span class="break-words">{operationError.message}</span>
								<button onclick={() => operationError = null} class="flex-shrink-0 hover:bg-white/20 rounded p-0.5">
									<X class="w-3 h-3" />
								</button>
							</div>
						{/if}
						{#if stack.status === 'not deployed' && source.gitStack}
							<button
								type="button"
								onclick={() => openGitModal(source.gitStack)}
								title="Edit git stack"
								class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
							>
								<Pencil class="w-3 h-3 text-muted-foreground hover:text-purple-500" />
							</button>
							<GitDeployProgressPopover
								stackId={source.gitStack.id}
								stackName={stack.name}
								onComplete={fetchStacks}
							>
								{#snippet children()}
									<button
										type="button"
										title="Deploy"
										class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
									>
										<Rocket class="w-3 h-3 text-muted-foreground hover:text-violet-500" />
									</button>
								{/snippet}
							</GitDeployProgressPopover>
						{:else}
							{#if source.sourceType === 'git' && source.gitStack}
								<GitDeployProgressPopover
									stackId={source.gitStack.id}
									stackName={stack.name}
									onComplete={fetchStacks}
								>
									{#snippet children()}
										<button
											type="button"
											title="Sync from Git"
											class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
										>
											<RefreshCw class="w-3 h-3 text-muted-foreground hover:text-purple-500" />
										</button>
									{/snippet}
								</GitDeployProgressPopover>
							{/if}
							{#if $canAccess('stacks', 'edit')}
								{#if source.sourceType === 'internal'}
									<button
										type="button"
										onclick={() => editStack(stack.name)}
										title="Edit"
										class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
									>
										<Pencil class="w-3 h-3 text-muted-foreground hover:text-blue-500" />
									</button>
								{:else if source.sourceType === 'git' && source.gitStack}
									<button
										type="button"
										onclick={() => openGitModal(source.gitStack)}
										title="Edit git stack"
										class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
									>
										<Pencil class="w-3 h-3 text-muted-foreground hover:text-purple-500" />
									</button>
								{/if}
							{/if}
							{#if stack.containers && stack.containers.length > 0}
								<button
									type="button"
									onclick={() => viewStackLogs(stack)}
									title="View logs"
									class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
								>
									<ScrollText class="w-3 h-3 text-muted-foreground hover:text-blue-500" />
								</button>
							{/if}
							{#if stackActionLoading === stack.name}
								<div class="p-1">
									<Loader2 class="w-3 h-3 animate-spin text-muted-foreground" />
								</div>
							{:else if stack.status === 'running' || stack.status === 'partial'}
								{#if $canAccess('stacks', 'restart')}
									<ConfirmPopover
										open={false}
										action="Restart"
										itemType="stack"
										itemName={stack.name}
										title="Restart"
										onConfirm={() => restartStack(stack.name)}
										onOpenChange={() => {}}
									>
										{#snippet children({ open })}
											<RotateCcw class="w-3 h-3 {open ? 'text-amber-500' : 'text-muted-foreground hover:text-amber-500'}" />
										{/snippet}
									</ConfirmPopover>
								{/if}
								{#if $canAccess('stacks', 'stop')}
									<ConfirmPopover
										open={confirmStopName === stack.name}
										action="Stop"
										itemType="stack"
										itemName={stack.name}
										title="Stop"
										onConfirm={() => stopStack(stack.name)}
										onOpenChange={(open) => confirmStopName = open ? stack.name : null}
									>
										{#snippet children({ open })}
											<Square class="w-3 h-3 {open ? 'text-destructive' : 'text-muted-foreground hover:text-destructive'}" />
										{/snippet}
									</ConfirmPopover>
								{/if}
								{#if $canAccess('stacks', 'stop')}
									<ConfirmPopover
										open={confirmDownName === stack.name}
										action="Down"
										itemType="stack"
										itemName={stack.name}
										title="Down (remove containers)"
										onConfirm={() => downStack(stack.name)}
										onOpenChange={(open) => confirmDownName = open ? stack.name : null}
									>
										{#snippet children({ open })}
											<ArrowBigDown class="w-3 h-3 {open ? 'text-orange-500' : 'text-muted-foreground hover:text-orange-500'}" />
										{/snippet}
									</ConfirmPopover>
								{/if}
							{:else}
								{#if $canAccess('stacks', 'start')}
									<button
										type="button"
										onclick={() => startStack(stack.name)}
										title="Start"
										class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
									>
										<Play class="w-3 h-3 text-muted-foreground hover:text-green-500" />
									</button>
								{/if}
							{/if}
						{/if}
						{#if $canAccess('stacks', 'remove')}
							<ConfirmPopover
								open={confirmDeleteName === stack.name}
								action="Delete"
								itemType="stack"
								itemName={stack.name}
								title="Remove"
								onConfirm={() => removeStack(stack.name)}
								onOpenChange={(open) => confirmDeleteName = open ? stack.name : null}
							>
								{#snippet children({ open })}
									<Trash2 class="w-3 h-3 {open ? 'text-destructive' : 'text-muted-foreground hover:text-destructive'}" />
								{/snippet}
							</ConfirmPopover>
						{/if}
					</div>
				{/if}
			{/snippet}

			{#snippet expandedRow(stack, rowState)}
				{#if stack.containerDetails?.length > 0}
					<div class="p-4 pl-12 shadow-inner bg-muted/30">
						<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-3">
							{#each stack.containerDetails as container (container.id)}
								{@const isLoading = containerActionLoading === container.id}
								<div class="p-3 rounded-lg bg-background border text-xs">
									<div class="flex items-center gap-2 mb-2">
										<Box class="w-4 h-4 shrink-0 {container.state === 'running' ? 'text-emerald-500' : 'text-muted-foreground'}" />
										<span class="font-medium truncate flex-1" title={container.name}>{container.service}</span>
										{#if container.health}
											<span title={container.health}>
												{#if container.health === 'healthy'}
													<HeartPulse class="w-3.5 h-3.5 {getHealthClasses(container.health)}" />
												{:else if container.health === 'unhealthy'}
													<HeartOff class="w-3.5 h-3.5 {getHealthClasses(container.health)}" />
												{:else}
													<Heart class="w-3.5 h-3.5 {getHealthClasses(container.health)}" />
												{/if}
											</span>
										{/if}
										<span class={getStatusClasses(container.state)}>{container.state}</span>
									</div>
									<div class="text-muted-foreground mb-2 space-y-0.5">
										<div class="truncate" title={container.image}>{container.image}</div>
										<div class="flex items-center gap-2 text-2xs">
											<span class="inline-flex items-center gap-1">
												<Clock class="w-2.5 h-2.5" />
												{formatUptime(container.status)}
											</span>
											{#if container.restartCount > 0}
												<span class="inline-flex items-center gap-0.5 text-amber-600 dark:text-amber-400" title="{container.restartCount} restart{container.restartCount > 1 ? 's' : ''}">
													<RotateCw class="w-2.5 h-2.5" />
													{container.restartCount}
												</span>
											{/if}
										</div>
									</div>
									<!-- CPU/Memory/Net/Disk mini sparkline graphs -->
									{#if container.state === 'running'}
										{@const stats = containerStats.get(container.id)}
										{@const history = containerStatsHistory.get(container.id)}
										{#key statsUpdateCount}
										<div class="grid grid-cols-4 gap-1.5 mb-2">
											<!-- CPU sparkline -->
											<div class="space-y-0">
												<div class="flex justify-between text-2xs">
													<span class="text-muted-foreground">CPU</span>
													<span class="font-mono {stats?.cpuPercent && stats.cpuPercent > 80 ? 'text-red-500' : stats?.cpuPercent && stats.cpuPercent > 50 ? 'text-yellow-500' : 'text-muted-foreground'}">{stats?.cpuPercent?.toFixed(0) ?? '-'}%</span>
												</div>
												{#if history?.cpu && history.cpu.length >= 2}
													<svg class="w-full h-4" viewBox="0 0 60 16" preserveAspectRatio="none">
														<path d={generateAreaPath(history.cpu, 60, 16)} fill="rgba(59, 130, 246, 0.15)" />
														<path d={generateSparklinePath(history.cpu, 60, 16)} fill="none" stroke="rgb(59, 130, 246)" stroke-width="1" />
													</svg>
												{:else}
													<div class="h-4 bg-muted/30 rounded animate-pulse"></div>
												{/if}
											</div>
											<!-- Memory sparkline -->
											<div class="space-y-0">
												<div class="flex justify-between text-2xs">
													<span class="text-muted-foreground">Mem</span>
													<span class="font-mono text-muted-foreground">{stats ? formatBytes(stats.memoryUsage) : '-'}</span>
												</div>
												{#if history?.mem && history.mem.length >= 2}
													<svg class="w-full h-4" viewBox="0 0 60 16" preserveAspectRatio="none">
														<path d={generateAreaPath(history.mem, 60, 16)} fill="rgba(168, 85, 247, 0.15)" />
														<path d={generateSparklinePath(history.mem, 60, 16)} fill="none" stroke="rgb(168, 85, 247)" stroke-width="1" />
													</svg>
												{:else}
													<div class="h-4 bg-muted/30 rounded animate-pulse"></div>
												{/if}
											</div>
											<!-- Network I/O sparkline -->
											<div class="space-y-0">
												<div class="flex justify-between text-2xs">
													<span class="text-muted-foreground">Net</span>
													<span class="font-mono text-muted-foreground">{stats ? formatBytes(stats.networkRx + stats.networkTx) : '-'}</span>
												</div>
												{#if history?.netRx && history.netRx.length >= 2}
													<svg class="w-full h-4" viewBox="0 0 60 16" preserveAspectRatio="none">
														<path d={generateAreaPath(history.netRx.map((rx, i) => rx + (history.netTx[i] || 0)), 60, 16)} fill="rgba(34, 197, 94, 0.15)" />
														<path d={generateSparklinePath(history.netRx.map((rx, i) => rx + (history.netTx[i] || 0)), 60, 16)} fill="none" stroke="rgb(34, 197, 94)" stroke-width="1" />
													</svg>
												{:else}
													<div class="h-4 bg-muted/30 rounded animate-pulse"></div>
												{/if}
											</div>
											<!-- Disk I/O sparkline -->
											<div class="space-y-0">
												<div class="flex justify-between text-2xs">
													<span class="text-muted-foreground">Disk</span>
													<span class="font-mono text-muted-foreground">{stats ? formatBytes(stats.blockRead + stats.blockWrite) : '-'}</span>
												</div>
												{#if history?.diskR && history.diskR.length >= 2}
													<svg class="w-full h-4" viewBox="0 0 60 16" preserveAspectRatio="none">
														<path d={generateAreaPath(history.diskR.map((r, i) => r + (history.diskW[i] || 0)), 60, 16)} fill="rgba(251, 146, 60, 0.15)" />
														<path d={generateSparklinePath(history.diskR.map((r, i) => r + (history.diskW[i] || 0)), 60, 16)} fill="none" stroke="rgb(251, 146, 60)" stroke-width="1" />
													</svg>
												{:else}
													<div class="h-4 bg-muted/30 rounded animate-pulse"></div>
												{/if}
											</div>
										</div>
										{/key}
									{/if}
									<div class="flex flex-wrap gap-1.5 mb-2 text-2xs">
										<!-- Clickable ports (dedupe by publicPort for IPv4/IPv6) -->
										{#if container.ports.length > 0}
											{@const uniquePorts = container.ports.filter((p, i, arr) => p.publicPort && arr.findIndex(x => x.publicPort === p.publicPort) === i)}
											{#each uniquePorts.slice(0, 2) as port}
												{@const url = getPortUrl(port.publicPort)}
												{#if url}
													<a
														href={url}
														target="_blank"
														rel="noopener noreferrer"
														onclick={(e) => e.stopPropagation()}
														class="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
														title="Open {url} in new tab"
													>
														<code>:{port.publicPort}</code>
														<ExternalLink class="w-2.5 h-2.5" />
													</a>
												{:else}
													<span class="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
														<code>:{port.publicPort}</code>
													</span>
												{/if}
											{/each}
											{#if uniquePorts.length > 2}
												<span class="text-muted-foreground">+{uniquePorts.length - 2}</span>
											{/if}
										{/if}
										<!-- Network with IP -->
										{#if container.networks.length > 0}
											{@const ip = getContainerIp(container.networks)}
											<Tooltip.Root>
												<Tooltip.Trigger>
													<span class="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
														<Network class="w-2.5 h-2.5" />
														{ip !== '-' ? ip : container.networks.length}
													</span>
												</Tooltip.Trigger>
												<Tooltip.Content class="whitespace-nowrap max-w-none">
													{#each container.networks as net}
														<div class="font-mono text-xs">{net.name}: {net.ipAddress || 'no IP'}</div>
													{/each}
												</Tooltip.Content>
											</Tooltip.Root>
										{/if}
										<!-- Volumes -->
										{#if container.volumeCount > 0}
											<span class="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200" title="{container.volumeCount} volume{container.volumeCount > 1 ? 's' : ''} mounted">
												<HardDrive class="w-2.5 h-2.5" />
												{container.volumeCount}
											</span>
										{/if}
									</div>
									<div class="flex items-center justify-between pt-2 border-t border-muted">
										<div class="flex gap-1">
											<button
												type="button"
												title="View logs"
												onclick={(e) => { e.stopPropagation(); goto(appendEnvParam(`/logs?container=${container.id}`, envId)); }}
												class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
											>
												<ScrollText class="w-3.5 h-3.5 text-muted-foreground hover:text-foreground" />
											</button>
											{#if container.state === 'running' && $canAccess('containers', 'exec')}
												<button
													type="button"
													title="Open terminal"
													onclick={(e) => { e.stopPropagation(); goto(appendEnvParam(`/terminal?container=${container.id}`, envId)); }}
													class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
												>
													<Terminal class="w-3.5 h-3.5 text-muted-foreground hover:text-foreground" />
												</button>
											{/if}
											{#if container.state === 'running' && $canAccess('containers', 'files')}
												<button
													type="button"
													title="Browse files"
													onclick={(e) => { e.stopPropagation(); browseFiles(container.id, container.name); }}
													class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
												>
													<FolderOpen class="w-3.5 h-3.5 text-muted-foreground hover:text-foreground" />
												</button>
											{/if}
											<button
												type="button"
												title="Inspect container"
												onclick={(e) => { e.stopPropagation(); inspectContainer(container.id, container.name); }}
												class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
											>
												<Eye class="w-3.5 h-3.5 text-muted-foreground hover:text-foreground" />
											</button>
										</div>
										<div class="relative flex gap-1">
											{#if operationError?.id === container.id && operationError?.message}
												<div class="absolute bottom-full right-0 mb-1 z-50 bg-destructive text-destructive-foreground rounded-md shadow-lg p-2 text-xs flex items-start gap-2 max-w-lg w-max">
													<AlertTriangle class="w-3 h-3 flex-shrink-0 mt-0.5" />
													<span class="break-words">{operationError.message}</span>
													<button onclick={() => operationError = null} class="flex-shrink-0 hover:bg-white/20 rounded p-0.5">
														<X class="w-3 h-3" />
													</button>
												</div>
											{/if}
											{#if isLoading}
												<Loader2 class="w-3.5 h-3.5 animate-spin text-muted-foreground" />
											{:else}
												{#if container.state === 'running'}
													{#if $canAccess('containers', 'restart')}
														<ConfirmPopover
															open={confirmRestartContainerId === container.id}
															action="Restart"
															itemType="container"
															itemName={container.service}
															title="Restart"
															onConfirm={() => restartContainer(container.id)}
															onOpenChange={(open) => confirmRestartContainerId = open ? container.id : null}
														>
															{#snippet children({ open })}
																<RotateCcw class="w-3.5 h-3.5 {open ? 'text-amber-500' : 'text-muted-foreground hover:text-amber-500'}" />
															{/snippet}
														</ConfirmPopover>
													{/if}
													{#if $canAccess('containers', 'pause')}
														<ConfirmPopover
															open={confirmPauseContainerId === container.id}
															action="Pause"
															itemType="container"
															itemName={container.service}
															title="Pause"
															onConfirm={() => pauseContainer(container.id)}
															onOpenChange={(open) => confirmPauseContainerId = open ? container.id : null}
														>
															{#snippet children({ open })}
																<Pause class="w-3.5 h-3.5 {open ? 'text-amber-500' : 'text-muted-foreground hover:text-amber-500'}" />
															{/snippet}
														</ConfirmPopover>
													{/if}
													{#if $canAccess('containers', 'stop')}
														<ConfirmPopover
															open={confirmStopContainerId === container.id}
															action="Stop"
															itemType="container"
															itemName={container.service}
															title="Stop"
															onConfirm={() => stopContainer(container.id)}
															onOpenChange={(open) => confirmStopContainerId = open ? container.id : null}
														>
															{#snippet children({ open })}
																<Square class="w-3.5 h-3.5 {open ? 'text-destructive' : 'text-muted-foreground hover:text-destructive'}" />
															{/snippet}
														</ConfirmPopover>
													{/if}
												{:else if container.state === 'paused'}
													{#if $canAccess('containers', 'unpause')}
														<button
															type="button"
															title="Unpause"
															onclick={(e) => unpauseContainer(container.id, e)}
															class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
														>
															<Play class="w-3.5 h-3.5 text-muted-foreground hover:text-emerald-500" />
														</button>
													{/if}
												{:else}
													{#if $canAccess('containers', 'start')}
														<button
															type="button"
															title="Start"
															onclick={(e) => startContainer(container.id, e)}
															class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
														>
															<Play class="w-3.5 h-3.5 text-muted-foreground hover:text-emerald-500" />
														</button>
													{/if}
												{/if}
											{/if}
										</div>
									</div>
								</div>
							{/each}
						</div>
					</div>
				{/if}
			{/snippet}
		</DataGrid>
	{/if}
</div>

<!-- Create Stack Modal -->
<StackModal
	bind:open={showCreateModal}
	mode="create"
	onClose={() => showCreateModal = false}
	onSuccess={fetchStacks}
/>

<!-- Edit Stack Modal -->
<StackModal
	bind:open={showEditModal}
	mode="edit"
	stackName={editingStackName}
	onClose={() => {
		showEditModal = false;
		editingStackName = '';
	}}
	onSuccess={fetchStacks}
/>

<GitStackModal
	bind:open={showGitModal}
	gitStack={editingGitStack}
	environmentId={envId}
	repositories={gitRepositories}
	credentials={gitCredentials}
	onClose={() => {
		showGitModal = false;
		editingGitStack = null;
	}}
	onSaved={fetchStacks}
/>

<ContainerInspectModal
	bind:open={showInspectModal}
	containerId={inspectContainerId}
	containerName={inspectContainerName}
/>

<FileBrowserModal
	bind:open={showFileBrowserModal}
	containerId={fileBrowserContainerId}
	containerName={fileBrowserContainerName}
	envId={envId ?? undefined}
	onclose={() => showFileBrowserModal = false}
/>

<BatchOperationModal
	bind:open={showBatchOpModal}
	title={batchOpTitle}
	operation={batchOpOperation}
	entityType="stacks"
	items={batchOpItems}
	envId={envId ?? undefined}
	options={{ force: true }}
	onClose={() => showBatchOpModal = false}
	onComplete={handleBatchComplete}
/>
