<script lang="ts" module>
	import { untrack } from 'svelte'
	const s3LogPrefixes = [
		'[windmill] Previous logs have been saved to object storage at logs/',
		'[windmill] Previous logs have been saved to disk at logs/',
		'[windmill] No object storage set in instance settings. Previous logs have been saved to disk at logs/'
	]

	// Only search within first N characters to avoid expensive full-content scans
	const S3_LOG_SEARCH_LIMIT = 2000
</script>

<script lang="ts">
	import { ClipboardCopy, Download, Expand, Loader2 } from 'lucide-svelte'
	import { Button, Drawer, DrawerContent } from './common'
	import { copyToClipboard } from '$lib/utils'
	import { base } from '$lib/base'
	import { workspaceStore } from '$lib/stores'
	import { AnsiUp } from 'ansi_up'
	import NoWorkerWithTagWarning from './runs/NoWorkerWithTagWarning.svelte'
	import { JobService } from '$lib/gen'
	import Tooltip from './Tooltip.svelte'
	import { twMerge } from 'tailwind-merge'
	import QueuePosition from './QueuePosition.svelte'

	interface Props {
		content: string | undefined
		isLoading: boolean
		duration?: number | undefined
		mem?: number | undefined
		wrapperClass?: string
		jobId?: string | undefined
		tag: string | undefined
		small?: boolean
		drawerOpen?: boolean
		noMaxH?: boolean
		noAutoScroll?: boolean
		download?: boolean
		customEmptyMessage?: string
		tagLabel?: string
		noPadding?: boolean
		navigationId?: string
	}

	let {
		content,
		isLoading,
		duration = undefined,
		mem = undefined,
		wrapperClass = '',
		jobId = undefined,
		tag,
		small = false,
		drawerOpen = $bindable(false),
		noMaxH = false,
		noAutoScroll = false,
		download = true,
		customEmptyMessage = 'No logs are available yet',
		tagLabel = undefined,
		noPadding = false,
		navigationId = undefined
	}: Props = $props()

	// @ts-ignore
	const ansi_up = $state(new AnsiUp())

	ansi_up.use_classes = true

	let scroll = $state(true)
	let div: HTMLElement | null = $state(null)

	// let downloadStartUrl: string | undefined = undefined

	let LOG_INC = 10000
	let LOG_LIMIT = $state(LOG_INC)

	let lastJobId = $state(untrack(() => jobId))

	let loadedFromObjectStore = $state('')

	function findPrefixInfo(
		truncateContent: string
	): { prefixIndex: number; position: number } | undefined {
		// Quick check - [windmill] is rare, so bail early in the common case
		// This is much faster than doing 3 long string indexOf searches
		const windmillPos = truncateContent.indexOf('[windmill]')
		if (windmillPos === -1 || windmillPos >= S3_LOG_SEARCH_LIMIT) {
			return undefined
		}

		// Found [windmill] marker, now determine which specific prefix matches
		for (let i = 0; i < s3LogPrefixes.length; i++) {
			const prefix = s3LogPrefixes[i]
			if (
				truncateContent.length >= windmillPos + prefix.length &&
				truncateContent.startsWith(prefix, windmillPos)
			) {
				return { prefixIndex: i, position: windmillPos }
			}
		}
		return undefined
	}

	function findStartUrl(
		truncateContent: string,
		prefixInfo: { prefixIndex: number; position: number } | undefined
	) {
		if (!prefixInfo) {
			return undefined
		}

		const { prefixIndex, position } = prefixInfo
		const prefix = s3LogPrefixes[prefixIndex]
		const startOfPath = position + prefix.length
		const endOfLine = truncateContent.indexOf('\n', startOfPath)

		return truncateContent.substring(startOfPath, endOfLine === -1 ? undefined : endOfLine)
	}

	function splitAtWindmillLine(
		content: string,
		prefixInfo: { prefixIndex: number; position: number }
	): { before: string; after: string } {
		const { prefixIndex, position } = prefixInfo
		const prefix = s3LogPrefixes[prefixIndex]

		// Find line boundaries
		const lineStart = position > 0 && content[position - 1] === '\n' ? position - 1 : position
		const pathStart = position + prefix.length
		const lineEnd = content.indexOf('\n', pathStart)

		if (lineEnd === -1) {
			// [windmill] line is at the end
			return { before: content.substring(0, lineStart), after: '' }
		}

		return {
			before: content.substring(0, lineStart),
			after: content.substring(lineEnd)
		}
	}

	function tooltipText(prefixInfo: { prefixIndex: number; position: number } | undefined) {
		if (prefixInfo == undefined) {
			return 'No path/file detected to download from'
		} else if (prefixInfo.prefixIndex == 0) {
			return 'Download the previous logs from the instance configured object store'
		} else if (prefixInfo.prefixIndex == 1) {
			return 'Attempt to download the logs from disk. Assume there is a shared disk between the workers and the server at /tmp/windmill/logs. Upgrade to EE to use an object store such as S3 instead of a shared volume.'
		} else if (prefixInfo.prefixIndex == 2) {
			return 'Attempt to download the logs from disk. Assume there is a shared disk between the workers and the server at /tmp/windmill/logs. Since you are on EE, you can alternatively use an object store such as S3 configured in the instance settings instead of a shared volume..'
		}
	}

	function truncateContent(
		jobContent: string | undefined,
		loadedFromObjectStore: string,
		limit: number
	) {
		let content = loadedFromObjectStore + (jobContent ?? '')
		if (content.length > limit) {
			return content.substring(content.length - limit)
		}
		return content
	}

	export function scrollToBottom() {
		// console.log('scrollToBottom', scroll, div)
		scroll && setTimeout(() => div?.scroll({ top: div?.scrollHeight, behavior: 'smooth' }), 100)
	}

	let logViewer: Drawer | undefined = $state()

	async function getStoreLogs() {
		if (downloadStartUrl) {
			scroll = false
			let res = (await JobService.getLogFileFromStore({
				workspace: $workspaceStore ?? '',
				path: downloadStartUrl
			})) as string
			LOG_LIMIT += Math.min(LOG_INC, res.length)
			loadedFromObjectStore = res + loadedFromObjectStore
			let newC = truncateContent(content, loadedFromObjectStore, LOG_LIMIT)
			LOG_LIMIT -= newC.indexOf('\n') + 1
		} else {
			console.error('No file detected to download from')
		}
	}

	function showMoreTruncate(len: number) {
		scroll = false
		LOG_LIMIT += LOG_INC
		let newC = truncateContent(content, loadedFromObjectStore, LOG_LIMIT)
		let newlineIndex = newC.indexOf('\n') + 1
		if (newlineIndex < LOG_INC / 2) {
			LOG_LIMIT -= newlineIndex
		}
	}
	$effect.pre(() => {
		if (jobId !== lastJobId) {
			lastJobId = jobId
			loadedFromObjectStore = ''
			LOG_LIMIT = LOG_INC
			scroll = true
		}
	})
	let truncatedContent = $derived(truncateContent(content, loadedFromObjectStore, LOG_LIMIT))
	let prefixInfo = $derived(findPrefixInfo(truncatedContent))
	let downloadStartUrl = $derived(findStartUrl(truncatedContent, prefixInfo))
	$effect.pre(() => {
		truncatedContent && scrollToBottom()
	})

	// When [windmill] line is NOT at start, split into before/after to render button inline
	let splitHtml = $derived.by(() => {
		if (prefixInfo == undefined || prefixInfo.position === 0) {
			return undefined
		}
		const { before, after } = splitAtWindmillLine(truncatedContent, prefixInfo)
		return {
			before: ansi_up.ansi_to_html(before),
			after: ansi_up.ansi_to_html(after)
		}
	})

	// Only compute html when splitHtml won't be used (avoids wasteful ansi_to_html call)
	let html = $derived.by(() => {
		if (splitHtml) {
			// splitHtml is active - skip expensive computation
			return ''
		}
		if (prefixInfo == undefined) {
			// No [windmill] line - return full content
			return ansi_up.ansi_to_html(truncatedContent)
		}
		// [windmill] at start - strip the line and return the rest
		const { after } = splitAtWindmillLine(truncatedContent, prefixInfo)
		return ansi_up.ansi_to_html(after)
	})
</script>

<Drawer bind:this={logViewer} bind:open={drawerOpen} size="900px">
	<DrawerContent title="Expanded Logs" on:close={logViewer.closeDrawer}>
		{#snippet actions()}
			{#if jobId && download}
				<Button
					href="{base}/api/w/{$workspaceStore}/jobs_u/get_logs/{jobId}"
					download="windmill_logs_{jobId}.txt"
					color="light"
					size="xs"
					startIcon={{
						icon: Download
					}}
				>
					Download
				</Button>
			{/if}

			<Button
				on:click={() => copyToClipboard(content)}
				color="light"
				size="xs"
				startIcon={{
					icon: ClipboardCopy
				}}
			>
				Copy to clipboard
			</Button>
		{/snippet}
		<div>
			<pre
				class="bg-surface-secondary text-primary text-xs w-full p-2 whitespace-pre-wrap border rounded-md"
				>{#if content}{@const len =
						(content?.length ?? 0) +
						(loadedFromObjectStore?.length ?? 0)}{#if splitHtml}{@html splitHtml.before}<button
								onclick={getStoreLogs}
								>Show more... <Tooltip>{tooltipText(prefixInfo)}</Tooltip></button
							>{@html splitHtml.after}{:else if downloadStartUrl}<button
							onclick={getStoreLogs}
							>Show more... <Tooltip>{tooltipText(prefixInfo)}</Tooltip></button
						><br />{@html html}{:else if len > LOG_LIMIT}(truncated to the last {LOG_LIMIT} characters)...<br
						/><button onclick={() => showMoreTruncate(len)}>Show more..</button><br
						/>{@html html}{:else}{@html html}{/if}{:else if isLoading}Waiting for job to start...{:else}No logs are available yet{/if}</pre
			>
		</div>
	</DrawerContent>
</Drawer>

<div class="w-full h-full {wrapperClass}">
	<div class="w-full h-full relative">
		<div
			bind:this={div}
			class="w-full h-full overflow-auto bg-surface-secondary pt-4 {noMaxH ? '' : 'max-h-screen'}"
			data-nav-id={navigationId}
		>
			<div class="absolute z-10 top-0 right-0 flex flex-row-reverse justify-between text-xs">
				<div class="flex gap-2">
					{#if jobId && download}
						<div class="flex items-center">
							<a
								class="text-primary pb-0.5"
								target="_blank"
								href="{base}/api/w/{$workspaceStore}/jobs_u/get_logs/{jobId}"
								download="windmill_logs_{jobId}.txt"
								><Download size="14" />
							</a>
						</div>
					{/if}
					<button onclick={logViewer.openDrawer}><Expand size="12" /></button>
					{#if !noAutoScroll}
						<label
							class="{small
								? ''
								: 'py-2'} pr-2 text-2xs flex gap-2 font-normal text-primary items-center"
						>
							Auto scroll
							<input class="windmillapp" type="checkbox" bind:checked={scroll} />
						</label>
					{/if}
				</div>
			</div>
			{#if isLoading}
				<div class="flex gap-2 absolute top-2 left-2 items-center z-10">
					<Loader2 class="animate-spin" />
					{#if tag}
						<div class="flex flex-row items-center gap-1">
							<div class="text-primary text-2xs">{tagLabel ?? 'tag'}: {tag}</div>
							<NoWorkerWithTagWarning {tagLabel} {tag} />
						</div>
					{/if}
					{#if jobId}
						<QueuePosition {jobId} />
					{/if}
				</div>
			{:else if duration}
				<span
					class={twMerge(
						'absolute  text-primary dark:text-gray-400',
						small ? '!text-2xs' : '!text-xs',
						small ? 'top-0' : 'top-2',
						noPadding ? '' : 'left-2'
					)}>took {duration}ms</span
				>
			{/if}
			{#if mem}
				<span
					class="absolute {small ? '!text-2xs' : '!text-xs'} text-primary dark:text-gray-400 {small
						? 'top-0'
						: 'top-2'}  left-36">mem peak: {(mem / 1024).toPrecision(4)}MB</span
				>
			{/if}
			<pre
				class={twMerge(
					'whitespace-pre break-words w-full ',
					small ? '!text-2xs' : '!text-xs',
					noPadding ? '' : 'p-2'
				)}
				>{#if content}{@const len =
						(content?.length ?? 0) +
						(loadedFromObjectStore?.length ?? 0)}{#if splitHtml}<span>{@html splitHtml.before}</span><button
								onclick={getStoreLogs}
								>Show more... &nbsp;<Tooltip>{tooltipText(prefixInfo)}</Tooltip></button
							><span>{@html splitHtml.after}</span
						>{:else if downloadStartUrl}<button
							onclick={getStoreLogs}
							>Show more... &nbsp;<Tooltip>{tooltipText(prefixInfo)}</Tooltip></button
						><br /><span>{@html html}</span
						>{:else if len > LOG_LIMIT}<button onclick={() => showMoreTruncate(len)}
							>Show more..</button
						>&nbsp;({LOG_LIMIT}/{len} chars)<br /><span>{@html html}</span
						>{:else}<span>{@html html}</span>{/if}{:else if !isLoading}<span>{customEmptyMessage}</span>{/if}</pre
			>
		</div>
	</div>
</div>

<style global>
	/* Foreground colors */
	.ansi-black-fg {
		color: rgb(0, 0, 0);
	}
	.ansi-red-fg {
		color: rgb(187, 0, 0);
	}
	.ansi-green-fg {
		color: rgb(0, 187, 0);
	}
	.ansi-yellow-fg {
		color: rgb(187, 187, 0);
	}
	.ansi-blue-fg {
		color: rgb(0, 0, 187);
	}
	.ansi-magenta-fg {
		color: rgb(187, 0, 187);
	}
	.ansi-cyan-fg {
		color: rgb(0, 187, 187);
	}
	.ansi-white-fg {
		color: rgb(255, 255, 255);
	}

	.ansi-bright-black-fg {
		color: rgb(85, 85, 85);
	}
	.ansi-bright-red-fg {
		color: rgb(255, 85, 85);
	}
	.ansi-bright-green-fg {
		color: rgb(0, 255, 0);
	}
	.ansi-bright-yellow-fg {
		color: rgb(255, 255, 85);
	}
	.ansi-bright-blue-fg {
		color: rgb(85, 85, 255);
	}
	.ansi-bright-magenta-fg {
		color: rgb(255, 85, 255);
	}
	.ansi-bright-cyan-fg {
		color: rgb(85, 255, 255);
	}
	.ansi-bright-white-fg {
		color: rgb(255, 255, 255);
	}

	/* Background colors */
	.ansi-black-bg {
		background-color: rgb(0, 0, 0);
	}
	.ansi-red-bg {
		background-color: rgb(187, 0, 0);
	}
	.ansi-green-bg {
		background-color: rgb(0, 187, 0);
	}
	.ansi-yellow-bg {
		background-color: rgb(187, 187, 0);
	}
	.ansi-blue-bg {
		background-color: rgb(0, 0, 187);
	}
	.ansi-magenta-bg {
		background-color: rgb(187, 0, 187);
	}
	.ansi-cyan-bg {
		background-color: rgb(0, 187, 187);
	}
	.ansi-white-bg {
		background-color: rgb(255, 255, 255);
	}

	.ansi-bright-black-bg {
		background-color: rgb(85, 85, 85);
	}
	.ansi-bright-red-bg {
		background-color: rgb(255, 85, 85);
	}
	.ansi-bright-green-bg {
		background-color: rgb(0, 255, 0);
	}
	.ansi-bright-yellow-bg {
		background-color: rgb(255, 255, 85);
	}
	.ansi-bright-blue-bg {
		background-color: rgb(85, 85, 255);
	}
	.ansi-bright-magenta-bg {
		background-color: rgb(255, 85, 255);
	}
	.ansi-bright-cyan-bg {
		background-color: rgb(85, 255, 255);
	}
	.ansi-bright-white-bg {
		background-color: rgb(255, 255, 255);
	}

	/* Foreground colors for dark mode (Nord theme) */
	.dark .ansi-black-fg {
		color: rgb(46, 52, 64);
	}
	.dark .ansi-red-fg {
		color: rgb(191, 97, 106);
	}
	.dark .ansi-green-fg {
		color: rgb(163, 190, 140);
	}
	.dark .ansi-yellow-fg {
		color: rgb(235, 203, 139);
	}
	.dark .ansi-blue-fg {
		color: rgb(94, 129, 172);
	}
	.dark .ansi-magenta-fg {
		color: rgb(180, 142, 173);
	}
	.dark .ansi-cyan-fg {
		color: rgb(136, 192, 208);
	}
	.dark .ansi-white-fg {
		color: rgb(216, 222, 233);
	}

	.dark .ansi-bright-black-fg {
		color: rgb(67, 76, 94);
	}
	.dark .ansi-bright-red-fg {
		color: rgb(191, 97, 106);
	}
	.dark .ansi-bright-green-fg {
		color: rgb(163, 190, 140);
	}
	.dark .ansi-bright-yellow-fg {
		color: rgb(235, 203, 139);
	}
	.dark .ansi-bright-blue-fg {
		color: rgb(94, 129, 172);
	}
	.dark .ansi-bright-magenta-fg {
		color: rgb(180, 142, 173);
	}
	.dark .ansi-bright-cyan-fg {
		color: rgb(136, 192, 208);
	}
	.dark .ansi-bright-white-fg {
		color: rgb(229, 233, 240);
	}

	/* Background colors for dark mode (Nord theme) */
	.dark .ansi-black-bg {
		background-color: rgb(46, 52, 64);
	}
	.dark .ansi-red-bg {
		background-color: rgb(191, 97, 106);
	}
	.dark .ansi-green-bg {
		background-color: rgb(163, 190, 140);
	}
	.dark .ansi-yellow-bg {
		background-color: rgb(235, 203, 139);
	}
	.dark .ansi-blue-bg {
		background-color: rgb(94, 129, 172);
	}
	.dark .ansi-magenta-bg {
		background-color: rgb(180, 142, 173);
	}
	.dark .ansi-cyan-bg {
		background-color: rgb(136, 192, 208);
	}
	.dark .ansi-white-bg {
		background-color: rgb(216, 222, 233);
	}

	.dark .ansi-bright-black-bg {
		background-color: rgb(67, 76, 94);
	}
	.dark .ansi-bright-red-bg {
		background-color: rgb(191, 97, 106);
	}
	.dark .ansi-bright-green-bg {
		background-color: rgb(163, 190, 140);
	}
	.dark .ansi-bright-yellow-bg {
		background-color: rgb(235, 203, 139);
	}
	.dark .ansi-bright-blue-bg {
		background-color: rgb(94, 129, 172);
	}
	.dark .ansi-bright-magenta-bg {
		background-color: rgb(180, 142, 173);
	}
	.dark .ansi-bright-cyan-bg {
		background-color: rgb(136, 192, 208);
	}
	.dark .ansi-bright-white-bg {
		background-color: rgb(229, 233, 240);
	}

	[data-nav-id]:focus-visible {
		outline: none;
		outline-offset: 0;
	}
</style>
