<script lang="ts">
	import { tick } from 'svelte';
	import * as Dialog from '$lib/components/ui/dialog';
	import { Button } from '$lib/components/ui/button';
	import { Badge } from '$lib/components/ui/badge';
	import { ShieldCheck, ShieldAlert, ShieldX, AlertTriangle, Info, ExternalLink, Loader2, CheckCircle2, XCircle, Terminal, Download, FileText, FileSpreadsheet, Sun, Moon } from 'lucide-svelte';
	import * as DropdownMenu from '$lib/components/ui/dropdown-menu';

	interface Props {
		open: boolean;
		imageName: string;
		envId?: number | null;
		onOpenChange?: (open: boolean) => void;
	}

	let { open = $bindable(), imageName, envId, onOpenChange }: Props = $props();

	type ScanStage = 'idle' | 'checking' | 'pulling-scanner' | 'scanning' | 'parsing' | 'complete' | 'error';

	interface VulnerabilitySummary {
		critical: number;
		high: number;
		medium: number;
		low: number;
		negligible: number;
		unknown: number;
	}

	interface Vulnerability {
		id: string;
		severity: string;
		package: string;
		version: string;
		fixedVersion?: string;
		description?: string;
		link?: string;
		scanner: 'grype' | 'trivy';
	}

	interface ScanResult {
		imageId: string;
		imageName: string;
		scanner: 'grype' | 'trivy';
		scannedAt: string;
		vulnerabilities: Vulnerability[];
		summary: VulnerabilitySummary;
		scanDuration: number;
		error?: string;
	}

	let stage = $state<ScanStage>('idle');
	let message = $state('');
	let progress = $state(0);
	let error = $state('');
	let result = $state<ScanResult | null>(null);
	let results = $state<ScanResult[]>([]);
	let scanner = $state<'grype' | 'trivy' | null>(null);
	let expandedVulns = $state<Set<string>>(new Set());
	let outputLinesByScanner = $state<Record<string, string[]>>({ grype: [], trivy: [], general: [] });
	let outputContainer: HTMLDivElement | undefined;
	let activeTab = $state<'grype' | 'trivy'>('grype');
	let scannerErrors = $state<Record<string, string>>({});
	let logDarkMode = $state(true);

	// Get output lines for active scanner (or all if scanning)
	const activeOutputLines = $derived(
		stage === 'complete' && results.length > 1
			? outputLinesByScanner[activeTab] || []
			: [...outputLinesByScanner.general, ...outputLinesByScanner.grype, ...outputLinesByScanner.trivy]
	);

	// Computed total vulnerabilities across all results
	const totalVulnerabilities = $derived(
		results.length > 0
			? results.reduce((sum, r) => sum + r.summary.critical + r.summary.high + r.summary.medium + r.summary.low + r.summary.negligible + r.summary.unknown, 0)
			: (result ? result.summary.critical + result.summary.high + result.summary.medium + result.summary.low + result.summary.negligible + result.summary.unknown : 0)
	);

	// Get active result for display
	const activeResult = $derived(
		results.length > 1
			? results.find(r => r.scanner === activeTab) || results[0]
			: results[0] || result
	);

	// Reset state when modal opens
	$effect(() => {
		if (open) {
			stage = 'idle';
			message = '';
			progress = 0;
			error = '';
			result = null;
			results = [];
			expandedVulns = new Set();
			outputLinesByScanner = { grype: [], trivy: [], general: [] };
			activeTab = 'grype';
			scannerErrors = {};
			startScan();
		}
	});

	async function startScan() {
		stage = 'checking';
		message = 'Starting vulnerability scan...';
		progress = 5;
		error = '';
		result = null;

		try {
			const url = envId ? `/api/images/scan?env=${envId}` : '/api/images/scan';
			const response = await fetch(url, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ imageName })
			});

			if (!response.ok) {
				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
			}

			const reader = response.body?.getReader();
			if (!reader) throw new Error('No response body');

			const decoder = new TextDecoder();
			let buffer = '';

			while (true) {
				const { done, value } = await reader.read();
				if (done) break;

				buffer += decoder.decode(value, { stream: true });
				const lines = buffer.split('\n');
				buffer = lines.pop() || '';

				for (const line of lines) {
					if (line.startsWith('data: ')) {
						try {
							const data = JSON.parse(line.slice(6));
							handleProgress(data);
						} catch (e) {
							// Ignore parse errors
						}
					}
				}
			}
		} catch (err) {
			error = err instanceof Error ? err.message : String(err);
			stage = 'error';
			message = `Scan failed: ${error}`;
		}
	}

	async function scrollOutputToBottom() {
		await tick();
		if (outputContainer) {
			outputContainer.scrollTop = outputContainer.scrollHeight;
		}
	}

	function handleProgress(data: any) {
		// Don't overwrite stage with 'error' for individual scanner failures when using 'both' mode
		// The scanner sends 'error' stage per-scanner, but we want to continue scanning
		if (data.stage === 'error' && data.scanner) {
			// Store individual scanner error, don't change global stage yet
			scannerErrors[data.scanner] = data.error || data.message || 'Unknown error';
		} else {
			stage = data.stage || stage;
		}
		message = data.message || message;
		progress = data.progress ?? progress;
		scanner = data.scanner || scanner;

		if (data.result) {
			result = data.result;
		}

		if (data.results && Array.isArray(data.results)) {
			results = data.results;
			if (results.length > 0) {
				activeTab = results[0].scanner;
			}
		}

		if (data.error && !data.scanner) {
			// Global error (not per-scanner)
			error = data.error;
		}

		// Store output by scanner with prefix for coloring
		const targetScanner = data.scanner || 'general';
		const prefix = targetScanner === 'grype' ? '[grype] ' : targetScanner === 'trivy' ? '[trivy] ' : '[dockhand] ';
		if (data.output) {
			outputLinesByScanner[targetScanner] = [...(outputLinesByScanner[targetScanner] || []), prefix + data.output];
			scrollOutputToBottom();
		}

		if (data.message && !data.output) {
			outputLinesByScanner[targetScanner] = [...(outputLinesByScanner[targetScanner] || []), prefix + data.message];
			scrollOutputToBottom();
		}
	}

	function toggleVulnDetails(vulnId: string) {
		const newSet = new Set(expandedVulns);
		if (newSet.has(vulnId)) {
			newSet.delete(vulnId);
		} else {
			newSet.add(vulnId);
		}
		expandedVulns = newSet;
	}

	function toggleLogTheme() {
		logDarkMode = !logDarkMode;
	}

	function getSeverityColor(severity: string): string {
		switch (severity.toLowerCase()) {
			case 'critical': return 'bg-red-500/10 text-red-500 border-red-500/30';
			case 'high': return 'bg-orange-500/10 text-orange-500 border-orange-500/30';
			case 'medium': return 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-500 border-yellow-500/30';
			case 'low': return 'bg-blue-500/10 text-blue-500 border-blue-500/30';
			case 'negligible': return 'bg-gray-500/10 text-gray-500 border-gray-500/30';
			default: return 'bg-gray-500/10 text-gray-500 border-gray-500/30';
		}
	}

	function formatDuration(ms: number): string {
		if (ms < 1000) return `${ms}ms`;
		return `${(ms / 1000).toFixed(1)}s`;
	}

	function downloadFile(content: string, filename: string, mimeType: string) {
		const blob = new Blob([content], { type: mimeType });
		const url = URL.createObjectURL(blob);
		const a = document.createElement('a');
		a.href = url;
		a.download = filename;
		document.body.appendChild(a);
		a.click();
		document.body.removeChild(a);
		URL.revokeObjectURL(url);
	}

	function sanitizeFilename(name: string): string {
		return name.replace(/[/:]/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_');
	}

	function exportToCSV() {
		if (!activeResult) return;

		const headers = ['CVE ID', 'Severity', 'Package', 'Installed Version', 'Fixed Version', 'Description', 'Link'];
		const rows = activeResult.vulnerabilities.map(v => [
			v.id,
			v.severity,
			v.package,
			v.version,
			v.fixedVersion || '',
			(v.description || '').replace(/"/g, '""'),
			v.link || ''
		]);

		const csvContent = [
			headers.join(','),
			...rows.map(row => row.map(cell => `"${cell}"`).join(','))
		].join('\n');

		const filename = `vuln-report-${sanitizeFilename(imageName)}-${activeResult.scanner}-${new Date().toISOString().split('T')[0]}.csv`;
		downloadFile(csvContent, filename, 'text/csv');
	}

	function exportToMarkdown() {
		if (!activeResult) return;

		const scanDate = new Date(activeResult.scannedAt).toLocaleString();
		const summaryParts = [];
		if (activeResult.summary.critical > 0) summaryParts.push(`**${activeResult.summary.critical} Critical**`);
		if (activeResult.summary.high > 0) summaryParts.push(`**${activeResult.summary.high} High**`);
		if (activeResult.summary.medium > 0) summaryParts.push(`${activeResult.summary.medium} Medium`);
		if (activeResult.summary.low > 0) summaryParts.push(`${activeResult.summary.low} Low`);
		if (activeResult.summary.negligible > 0) summaryParts.push(`${activeResult.summary.negligible} Negligible`);
		if (activeResult.summary.unknown > 0) summaryParts.push(`${activeResult.summary.unknown} Unknown`);

		let md = `# Vulnerability Scan Report\n\n`;
		md += `**Image:** \`${imageName}\`\n\n`;
		md += `**Scanner:** ${activeResult.scanner === 'grype' ? 'Grype (Anchore)' : 'Trivy (Aqua Security)'}\n\n`;
		md += `**Scan Date:** ${scanDate}\n\n`;
		md += `**Duration:** ${formatDuration(activeResult.scanDuration)}\n\n`;
		md += `## Summary\n\n`;
		md += summaryParts.length > 0 ? summaryParts.join(' | ') : 'No vulnerabilities found';
		md += `\n\n**Total:** ${activeResult.vulnerabilities.length} vulnerabilities\n\n`;

		if (activeResult.vulnerabilities.length > 0) {
			// Group by severity for better readability
			const bySeverity: Record<string, Vulnerability[]> = {};
			for (const vuln of activeResult.vulnerabilities) {
				const sev = vuln.severity.toLowerCase();
				if (!bySeverity[sev]) bySeverity[sev] = [];
				bySeverity[sev].push(vuln);
			}

			const severityOrder = ['critical', 'high', 'medium', 'low', 'negligible', 'unknown'];

			for (const severity of severityOrder) {
				const vulns = bySeverity[severity];
				if (!vulns || vulns.length === 0) continue;

				md += `## ${severity.charAt(0).toUpperCase() + severity.slice(1)} (${vulns.length})\n\n`;

				for (const vuln of vulns) {
					md += `### ${vuln.id}\n\n`;
					md += `- **Package:** \`${vuln.package}\`\n`;
					md += `- **Installed:** \`${vuln.version}\`\n`;
					if (vuln.fixedVersion) {
						md += `- **Fixed in:** \`${vuln.fixedVersion}\`\n`;
					} else {
						md += `- **Fixed in:** *No fix available*\n`;
					}
					if (vuln.link) {
						md += `- **Reference:** [${vuln.id}](${vuln.link})\n`;
					}
					if (vuln.description) {
						md += `\n${vuln.description}\n`;
					}
					md += `\n`;
				}
			}
		}

		md += `---\n\n*Report generated by Dockhand*\n`;

		const filename = `vuln-report-${sanitizeFilename(imageName)}-${activeResult.scanner}-${new Date().toISOString().split('T')[0]}.md`;
		downloadFile(md, filename, 'text/markdown');
	}

	function exportToJSON() {
		if (!activeResult) return;

		const report = {
			image: imageName,
			scanner: activeResult.scanner,
			scannedAt: activeResult.scannedAt,
			scanDuration: activeResult.scanDuration,
			summary: activeResult.summary,
			vulnerabilities: activeResult.vulnerabilities
		};

		const jsonContent = JSON.stringify(report, null, 2);
		const filename = `vuln-report-${sanitizeFilename(imageName)}-${activeResult.scanner}-${new Date().toISOString().split('T')[0]}.json`;
		downloadFile(jsonContent, filename, 'application/json');
	}
</script>

<Dialog.Root bind:open onOpenChange={onOpenChange}>
	<Dialog.Content class="max-w-6xl h-[90vh] flex flex-col">
		<Dialog.Header>
			<Dialog.Title class="flex items-center gap-2">
				{#if stage === 'complete' && activeResult}
					{#if activeResult.summary.critical > 0 || activeResult.summary.high > 0}
						<ShieldX class="w-5 h-5 text-red-500" />
					{:else if activeResult.summary.medium > 0}
						<ShieldAlert class="w-5 h-5 text-yellow-500" />
					{:else}
						<ShieldCheck class="w-5 h-5 text-green-500" />
					{/if}
				{:else if stage === 'error'}
					<XCircle class="w-5 h-5 text-red-500" />
				{:else}
					<ShieldCheck class="w-5 h-5" />
				{/if}
				Vulnerability scan
			</Dialog.Title>
			<Dialog.Description class="space-y-1">
				<div>Scanning <code class="text-xs bg-muted px-1.5 py-0.5 rounded">{imageName}</code></div>
				{#if activeResult?.imageId}
					<div class="text-xs text-muted-foreground font-mono">SHA: {activeResult.imageId.replace('sha256:', '')}</div>
				{/if}
			</Dialog.Description>
		</Dialog.Header>

		<div class="flex-1 min-h-0 flex flex-col overflow-hidden">
			{#if stage !== 'complete' && stage !== 'error'}
				<!-- Scanning in progress -->
				<div class="flex flex-col flex-1 min-h-0 py-4 gap-4">
					<div class="flex items-center gap-3 shrink-0">
						<Loader2 class="w-5 h-5 animate-spin text-primary" />
						<span class="text-sm">{message}</span>
					</div>
					<div class="h-2 bg-muted rounded-full overflow-hidden shrink-0">
						<div
							class="h-full bg-primary transition-all duration-300"
							style="width: {progress}%"
						></div>
					</div>
					{#if scanner}
						<p class="text-xs text-muted-foreground shrink-0">
							Using {scanner === 'grype' ? 'Grype (Anchore)' : 'Trivy (Aqua Security)'} scanner
						</p>
					{/if}

					<!-- Output Log -->
					<div class="flex flex-col flex-1 min-h-0">
						<div class="flex items-center justify-between text-xs text-muted-foreground mb-2 shrink-0">
							<div class="flex items-center gap-2">
								<Terminal class="w-3.5 h-3.5" />
								<span>Scanner output</span>
							</div>
							<button type="button" onclick={toggleLogTheme} class="p-1 rounded hover:bg-muted transition-colors" title="Toggle log theme">
								{#if logDarkMode}
									<Sun class="w-3.5 h-3.5" />
								{:else}
									<Moon class="w-3.5 h-3.5" />
								{/if}
							</button>
						</div>
						<div
							bind:this={outputContainer}
							class="{logDarkMode ? 'bg-zinc-950 text-zinc-300' : 'bg-zinc-100 text-zinc-700'} rounded-lg p-3 font-mono text-xs flex-1 min-h-0 overflow-auto"
						>
							{#each activeOutputLines as line}
								<div class="whitespace-pre-wrap break-all leading-relaxed flex items-start gap-1.5">
									{#if line.startsWith('[grype]')}
										<span class="inline-flex items-center px-1 rounded text-2xs font-medium bg-violet-500 text-white shadow-[0_1px_1px_rgba(0,0,0,0.2)] shrink-0 mt-[3px]">grype</span>
										<span>{line.slice(8)}</span>
									{:else if line.startsWith('[trivy]')}
										<span class="inline-flex items-center px-1 rounded text-2xs font-medium bg-teal-500 text-white shadow-[0_1px_1px_rgba(0,0,0,0.2)] shrink-0 mt-[3px]">trivy</span>
										<span>{line.slice(8)}</span>
									{:else if line.startsWith('[dockhand]')}
										<span class="inline-flex items-center px-1 rounded text-2xs font-medium bg-slate-500 text-white shadow-[0_1px_1px_rgba(0,0,0,0.2)] shrink-0 mt-[3px]">dockhand</span>
										<span>{line.slice(11)}</span>
									{:else}
										<span>{line}</span>
									{/if}
								</div>
							{/each}
						</div>
					</div>
				</div>
			{:else if stage === 'error'}
				<!-- Error state -->
				<div class="flex flex-col flex-1 min-h-0 py-4 gap-4">
					<!-- Show individual scanner errors if available -->
					{#if Object.keys(scannerErrors).length > 0}
						<div class="flex flex-col gap-2 shrink-0">
							{#each Object.entries(scannerErrors) as [scannerName, scannerError]}
								<div class="p-3 rounded-lg bg-destructive/10 border border-destructive/30">
									<div class="flex items-start gap-3">
										<XCircle class="w-4 h-4 text-destructive mt-0.5 shrink-0" />
										<div class="min-w-0">
											<h4 class="font-medium text-destructive text-sm">{scannerName === 'grype' ? 'Grype' : 'Trivy'} failed</h4>
											<p class="text-xs text-muted-foreground mt-1 break-words">{scannerError}</p>
										</div>
									</div>
								</div>
							{/each}
						</div>
					{:else}
						<div class="p-4 rounded-lg bg-destructive/10 border border-destructive/30 shrink-0">
							<div class="flex items-start gap-3">
								<XCircle class="w-5 h-5 text-destructive mt-0.5" />
								<div>
									<h4 class="font-medium text-destructive">Scan failed</h4>
									<p class="text-sm text-muted-foreground mt-1">{error}</p>
								</div>
							</div>
						</div>
					{/if}

					<Button class="w-fit shrink-0" onclick={startScan}>
						Retry scan
					</Button>

					<!-- Output Log -->
					<div class="flex flex-col flex-1 min-h-0">
						<div class="flex items-center justify-between text-xs text-muted-foreground mb-2 shrink-0">
							<div class="flex items-center gap-2">
								<Terminal class="w-3.5 h-3.5" />
								<span>Scanner output</span>
							</div>
							<button type="button" onclick={toggleLogTheme} class="p-1 rounded hover:bg-muted transition-colors" title="Toggle log theme">
								{#if logDarkMode}
									<Sun class="w-3.5 h-3.5" />
								{:else}
									<Moon class="w-3.5 h-3.5" />
								{/if}
							</button>
						</div>
						<div
							bind:this={outputContainer}
							class="{logDarkMode ? 'bg-zinc-950 text-zinc-300' : 'bg-zinc-100 text-zinc-700'} rounded-lg p-3 font-mono text-xs flex-1 min-h-0 overflow-auto"
						>
							{#each activeOutputLines as line}
								<div class="whitespace-pre-wrap break-all leading-relaxed flex items-start gap-1.5">
									{#if line.startsWith('[grype]')}
										<span class="inline-flex items-center px-1 rounded text-2xs font-medium bg-violet-500 text-white shadow-[0_1px_1px_rgba(0,0,0,0.2)] shrink-0 mt-[3px]">grype</span>
										<span>{line.slice(8)}</span>
									{:else if line.startsWith('[trivy]')}
										<span class="inline-flex items-center px-1 rounded text-2xs font-medium bg-teal-500 text-white shadow-[0_1px_1px_rgba(0,0,0,0.2)] shrink-0 mt-[3px]">trivy</span>
										<span>{line.slice(8)}</span>
									{:else if line.startsWith('[dockhand]')}
										<span class="inline-flex items-center px-1 rounded text-2xs font-medium bg-slate-500 text-white shadow-[0_1px_1px_rgba(0,0,0,0.2)] shrink-0 mt-[3px]">dockhand</span>
										<span>{line.slice(11)}</span>
									{:else}
										<span>{line}</span>
									{/if}
								</div>
							{/each}
						</div>
					</div>
				</div>
			{:else if stage === 'complete' && activeResult}
				<!-- Results -->
				<div class="flex flex-col flex-1 min-h-0 py-2 gap-4">
					<!-- Scanner tabs (only if multiple results) -->
					{#if results.length > 1}
						<div class="flex gap-1 border-b shrink-0">
							{#each results as r}
								<button
									class="px-4 py-2 text-sm font-medium border-b-2 transition-colors {activeTab === r.scanner ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'}"
									onclick={() => activeTab = r.scanner}
								>
									{r.scanner === 'grype' ? 'Grype' : 'Trivy'}
									{#if r.summary.critical > 0 || r.summary.high > 0}
										<Badge variant="outline" class="ml-2 bg-red-500/10 text-red-500 border-red-500/30 text-xs">
											{r.summary.critical + r.summary.high}
										</Badge>
									{:else}
										<Badge variant="outline" class="ml-2 bg-green-500/10 text-green-500 border-green-500/30 text-xs">
											{r.vulnerabilities.length}
										</Badge>
									{/if}
								</button>
							{/each}
						</div>
					{/if}

					<!-- Show any scanner errors that occurred (partial success) -->
					{#if Object.keys(scannerErrors).length > 0}
						<div class="flex flex-col gap-2 shrink-0">
							{#each Object.entries(scannerErrors) as [scannerName, scannerError]}
								<div class="p-2 rounded-lg bg-amber-500/10 border border-amber-500/30">
									<div class="flex items-start gap-2">
										<AlertTriangle class="w-4 h-4 text-amber-500 mt-0.5 shrink-0" />
										<div class="min-w-0">
											<span class="font-medium text-amber-600 dark:text-amber-500 text-sm">{scannerName === 'grype' ? 'Grype' : 'Trivy'} failed:</span>
											<span class="text-xs text-muted-foreground ml-1">{scannerError}</span>
										</div>
									</div>
								</div>
							{/each}
						</div>
					{/if}

					<!-- Summary -->
					<div class="flex flex-wrap gap-2 shrink-0">
						{#if activeResult.summary.critical > 0}
							<Badge variant="outline" class="bg-red-500/10 text-red-500 border-red-500/30">
								{activeResult.summary.critical} Critical
							</Badge>
						{/if}
						{#if activeResult.summary.high > 0}
							<Badge variant="outline" class="bg-orange-500/10 text-orange-500 border-orange-500/30">
								{activeResult.summary.high} High
							</Badge>
						{/if}
						{#if activeResult.summary.medium > 0}
							<Badge variant="outline" class="bg-yellow-500/10 text-yellow-600 dark:text-yellow-500 border-yellow-500/30">
								{activeResult.summary.medium} Medium
							</Badge>
						{/if}
						{#if activeResult.summary.low > 0}
							<Badge variant="outline" class="bg-blue-500/10 text-blue-500 border-blue-500/30">
								{activeResult.summary.low} Low
							</Badge>
						{/if}
						{#if activeResult.summary.negligible > 0}
							<Badge variant="outline" class="bg-gray-500/10 text-gray-500 border-gray-500/30">
								{activeResult.summary.negligible} Negligible
							</Badge>
						{/if}
						{#if activeResult.summary.unknown > 0}
							<Badge variant="outline" class="bg-gray-500/10 text-gray-500 border-gray-500/30">
								{activeResult.summary.unknown} Unknown
							</Badge>
						{/if}
						{#if activeResult.vulnerabilities.length === 0}
							<Badge variant="outline" class="bg-green-500/10 text-green-500 border-green-500/30">
								<CheckCircle2 class="w-3 h-3 mr-1" />
								No vulnerabilities found
							</Badge>
						{/if}
					</div>

					<!-- Scan info -->
					<div class="text-xs text-muted-foreground flex items-center gap-3 shrink-0">
						<span>Scanner: {activeResult.scanner === 'grype' ? 'Grype' : 'Trivy'}</span>
						<span>Duration: {formatDuration(activeResult.scanDuration)}</span>
						<span>Total: {activeResult.vulnerabilities.length} vulnerabilities</span>
					</div>

					<!-- Vulnerability list -->
					{#if activeResult.vulnerabilities.length > 0}
						<div class="border rounded-lg flex-1 min-h-0 max-h-[40vh] overflow-y-auto">
							<table class="w-full text-sm">
								<thead class="bg-muted sticky top-0">
									<tr>
										<th class="text-left py-2 px-3 font-medium w-[20%]">CVE ID</th>
										<th class="text-left py-2 px-3 font-medium w-[15%]">Severity</th>
										<th class="text-left py-2 px-3 font-medium w-[25%]">Package</th>
										<th class="text-left py-2 px-3 font-medium w-[20%]">Installed</th>
										<th class="text-left py-2 px-3 font-medium w-[20%]">Fixed in</th>
									</tr>
								</thead>
								<tbody>
									{#each activeResult.vulnerabilities.slice(0, 100) as vuln, i}
										<tr
											class="border-t border-muted hover:bg-muted/30 cursor-pointer transition-colors"
											onclick={() => toggleVulnDetails(vuln.id + i)}
										>
											<td class="py-2 px-3">
												<div class="flex items-center gap-1.5">
													<code class="text-xs">{vuln.id}</code>
													{#if vuln.link}
														<a
															href={vuln.link}
															target="_blank"
															rel="noopener noreferrer"
															onclick={(e) => e.stopPropagation()}
															class="text-muted-foreground hover:text-foreground"
														>
															<ExternalLink class="w-3 h-3" />
														</a>
													{/if}
												</div>
											</td>
											<td class="py-2 px-3">
												<Badge variant="outline" class={getSeverityColor(vuln.severity)}>
													{vuln.severity}
												</Badge>
											</td>
											<td class="py-2 px-3">
												<code class="text-xs">{vuln.package}</code>
											</td>
											<td class="py-2 px-3">
												<code class="text-xs text-muted-foreground">{vuln.version}</code>
											</td>
											<td class="py-2 px-3">
												{#if vuln.fixedVersion}
													<code class="text-xs text-green-600 dark:text-green-500">{vuln.fixedVersion}</code>
												{:else}
													<span class="text-xs text-muted-foreground italic">No fix available</span>
												{/if}
											</td>
										</tr>
										{#if expandedVulns.has(vuln.id + i) && vuln.description}
											<tr class="bg-muted/20">
												<td colspan="5" class="py-2 px-3">
													<p class="text-xs text-muted-foreground">{vuln.description}</p>
												</td>
											</tr>
										{/if}
									{/each}
								</tbody>
							</table>
							{#if activeResult.vulnerabilities.length > 100}
								<div class="py-2 px-3 bg-muted/50 text-xs text-muted-foreground text-center">
									Showing 100 of {activeResult.vulnerabilities.length} vulnerabilities
								</div>
							{/if}
						</div>
					{/if}

					<!-- Output Log -->
					<div class="flex flex-col flex-1 min-h-[120px]">
						<div class="flex items-center justify-between text-xs text-muted-foreground mb-2 shrink-0">
							<div class="flex items-center gap-2">
								<Terminal class="w-3.5 h-3.5" />
								<span>Scanner output ({activeOutputLines.length} lines)</span>
							</div>
							<button type="button" onclick={toggleLogTheme} class="p-1 rounded hover:bg-muted transition-colors" title="Toggle log theme">
								{#if logDarkMode}
									<Sun class="w-3.5 h-3.5" />
								{:else}
									<Moon class="w-3.5 h-3.5" />
								{/if}
							</button>
						</div>
						<div
							bind:this={outputContainer}
							class="{logDarkMode ? 'bg-zinc-950 text-zinc-300' : 'bg-zinc-100 text-zinc-700'} rounded-lg p-3 font-mono text-xs flex-1 min-h-0 overflow-auto"
						>
							{#each activeOutputLines as line}
								<div class="whitespace-pre-wrap break-all leading-relaxed flex items-start gap-1.5">
									{#if line.startsWith('[grype]')}
										<span class="inline-flex items-center px-1 rounded text-2xs font-medium bg-violet-500 text-white shadow-[0_1px_1px_rgba(0,0,0,0.2)] shrink-0 mt-[3px]">grype</span>
										<span>{line.slice(8)}</span>
									{:else if line.startsWith('[trivy]')}
										<span class="inline-flex items-center px-1 rounded text-2xs font-medium bg-teal-500 text-white shadow-[0_1px_1px_rgba(0,0,0,0.2)] shrink-0 mt-[3px]">trivy</span>
										<span>{line.slice(8)}</span>
									{:else if line.startsWith('[dockhand]')}
										<span class="inline-flex items-center px-1 rounded text-2xs font-medium bg-slate-500 text-white shadow-[0_1px_1px_rgba(0,0,0,0.2)] shrink-0 mt-[3px]">dockhand</span>
										<span>{line.slice(11)}</span>
									{:else}
										<span>{line}</span>
									{/if}
								</div>
							{/each}
						</div>
					</div>
				</div>
			{/if}
		</div>

		<Dialog.Footer class="flex justify-between">
			{#if stage === 'complete'}
				<div class="flex gap-2">
					<Button variant="outline" onclick={startScan}>
						Rescan
					</Button>
					{#if activeResult && activeResult.vulnerabilities.length > 0}
						<DropdownMenu.Root>
							<DropdownMenu.Trigger>
								{#snippet child({ props })}
									<Button variant="outline" {...props}>
										<Download class="w-4 h-4 mr-2" />
										Report
									</Button>
								{/snippet}
							</DropdownMenu.Trigger>
							<DropdownMenu.Content align="start">
								<DropdownMenu.Item onclick={exportToMarkdown}>
									<FileText class="w-4 h-4 mr-2 text-blue-500" />
									Markdown report (.md)
								</DropdownMenu.Item>
								<DropdownMenu.Item onclick={exportToCSV}>
									<FileSpreadsheet class="w-4 h-4 mr-2 text-green-500" />
									CSV spreadsheet (.csv)
								</DropdownMenu.Item>
								<DropdownMenu.Item onclick={exportToJSON}>
									<FileText class="w-4 h-4 mr-2 text-amber-500" />
									JSON data (.json)
								</DropdownMenu.Item>
							</DropdownMenu.Content>
						</DropdownMenu.Root>
					{/if}
				</div>
			{:else}
				<div></div>
			{/if}
			<Button variant="secondary" onclick={() => open = false}>
				Close
			</Button>
		</Dialog.Footer>
	</Dialog.Content>
</Dialog.Root>
