<script module lang="ts">
	export const EDITOR_BAR_WIDTH_THRESHOLD = 1420
	// Below this width even the icon-only helpers cluster is too crowded —
	// collapse them into a single "Helpers" dropdown menu.
	export const EDITOR_BAR_HELPERS_COMPACT_THRESHOLD = 800
	// Tighter threshold for editors embedded inline in narrow panes
	// (flow-step editor, raw-app inline script editor).
	export const EDITOR_BAR_HELPERS_INLINE_THRESHOLD = 600

	function getImportWmillTsStatement(lang: string | undefined) {
		if (lang === 'deno') {
			return `import * as wmill from "npm:windmill-client@1"\n`
		} else if (lang === 'bun' || lang === 'bunnative') {
			return `import * as wmill from "windmill-client"\n`
		} else if (lang === 'nativets') {
			return `import * as wmill from "./windmill.ts"\n`
		}
		return ''
	}
</script>

<script lang="ts">
	import { ResourceService, VariableService, WorkspaceService, type Script } from '$lib/gen'

	import { workspaceStore } from '$lib/stores'
	import { base } from '$lib/base'
	import type Editor from './Editor.svelte'
	import ItemPicker from './ItemPicker.svelte'
	import VariableEditor from './VariableEditor.svelte'
	import Button from './common/button/Button.svelte'
	import HighlightCode from './HighlightCode.svelte'
	import DrawerContent from './common/drawer/DrawerContent.svelte'
	import { Drawer } from './common'
	import WorkspaceScriptPicker from './flows/pickers/WorkspaceScriptPicker.svelte'
	import PickHubScript from './flows/pickers/PickHubScript.svelte'
	import ToggleHubWorkspace from './ToggleHubWorkspace.svelte'
	import Skeleton from './common/skeleton/Skeleton.svelte'
	import { SCRIPT_EDITOR_SHOW_EXPLORE_OTHER_SCRIPTS } from '$lib/consts'
	import { createEventDispatcher, untrack } from 'svelte'
	import { sendUserToast } from '$lib/toast'
	import { getScriptByPath, scriptLangToEditorLang } from '$lib/scripts'
	import { bashRunsInCustomImage } from '$lib/script_helpers'
	import Toggle from './Toggle.svelte'

	import {
		DatabaseIcon,
		DiffIcon,
		DollarSign,
		File,
		GitBranch,
		History,
		Library,
		Link,
		Package,
		Plus,
		RotateCw,
		Sigma,
		Save,
		Settings,
		Users
	} from 'lucide-svelte'
	import { capitalize, formatS3Object, toCamel, type Item } from '$lib/utils'
	import DropdownV2 from './DropdownV2.svelte'
	import type { Schema, SchemaProperty, SupportedLanguage } from '$lib/common'
	import ScriptVersionHistory from './ScriptVersionHistory.svelte'
	import { getResetCode } from '$lib/script_helpers'
	import Popover from './Popover.svelte'
	import ResourceEditorDrawer from './ResourceEditorDrawer.svelte'
	import type { EditorBarUi } from './custom_ui'
	import EditorSettings from './EditorSettings.svelte'
	import { quicktype, InputData, JSONSchemaInput, FetchingJSONSchemaStore } from 'quicktype-core'
	import S3FilePicker from './S3FilePicker.svelte'
	import DucklakeIcon from './icons/DucklakeIcon.svelte'
	import MetricsDrawer from './metrics/MetricsDrawer.svelte'
	import { endsWithUnterminatedStatement } from './sqlDdl'
	import FlowInlineScriptAiButton from './copilot/FlowInlineScriptAIButton.svelte'
	import GitRepoPopoverPicker from './GitRepoPopoverPicker.svelte'
	import { insertDelegateToGitRepoInCode } from '$lib/ansibleUtils'

	interface Props {
		lang: SupportedLanguage | 'bunnative' | undefined
		editor: Editor | undefined
		websocketAlive: {
			pyright: boolean
			ruff: boolean
			deno: boolean
			go: boolean
			shellcheck: boolean
		}
		iconOnly?: boolean
		compactHelpers?: boolean
		validCode?: boolean
		// Asset-parse validity, when the context cares about it (pipeline
		// editor). `undefined` = not applicable; `false` makes the badge red
		// even if the main function parses.
		validAssets?: boolean | undefined
		kind?: 'script' | 'trigger' | 'approval'
		template?:
			| 'pgsql'
			| 'mysql'
			| 'script'
			| 'docker'
			| 'powershell'
			| 'bunnative'
			| 'claudesandbox'
			| 'wac_python'
			| 'wac_typescript'
			| 'ci_test_bun'
			| 'ci_test_python'
		collabMode?: boolean
		collabLive?: boolean
		collabUsers?: { name: string }[]
		scriptPath?: string | undefined
		args?: Record<string, any>
		noHistory?: boolean
		saveToWorkspace?: boolean
		customUi?: EditorBarUi
		lastDeployedCode?: string | undefined
		diffMode?: boolean
		showHistoryDrawer?: boolean
		right?: import('svelte').Snippet
		openAiChat?: boolean
		moduleId?: string
		// Workspace to scope variable/resource/data-table lookups to. Defaults to
		// the nav `$workspaceStore`; an AI-session live editor passes the session's
		// acting workspace (a fork) so the helper pickers hit the right workspace.
		workspace?: string
	}

	let {
		lang,
		editor,
		websocketAlive,
		iconOnly = false,
		compactHelpers = false,
		validCode = true,
		validAssets = undefined,
		kind = 'script',
		template = 'script',
		collabMode = false,
		collabLive = false,
		collabUsers = [],
		scriptPath = undefined,
		noHistory = false,
		saveToWorkspace = false,
		customUi = {},
		lastDeployedCode = undefined,
		diffMode = false,
		showHistoryDrawer = $bindable(false),
		right,
		openAiChat = false,
		moduleId = undefined,
		workspace = undefined
	}: Props = $props()

	let ws = $derived(workspace ?? $workspaceStore)

	let contextualVariablePicker: ItemPicker | undefined = $state()
	let variablePicker: ItemPicker | undefined = $state()
	let resourcePicker: ItemPicker | undefined = $state()
	let resourceTypePicker: ItemPicker | undefined = $state()
	let variableEditor: VariableEditor | undefined = $state()
	let resourceEditor: ResourceEditorDrawer | undefined = $state()
	let s3FilePicker: S3FilePicker | undefined = $state()
	let ducklakePicker: ItemPicker | undefined = $state()
	let dataTablePicker: ItemPicker | undefined = $state()
	let databasePicker: ItemPicker | undefined = $state()
	let metricsDrawer: MetricsDrawer | undefined = $state()
	let gitRepoPickerOpen = $state(false)

	let showContextVarPicker = $derived(
		[
			'python3',
			'bash',
			'powershell',
			'go',
			'deno',
			'bun',
			'bunnative',
			'nativets',
			'php',
			'rust',
			'csharp',
			'nu',
			'java',
			'ruby',
			'rlang',
			'ansible',
			'postgresql',
			'mysql',
			'bigquery',
			'mssql',
			'oracledb',
			'snowflake',
			'duckdb'
			// for related places search: ADD_NEW_LANG
		].includes(lang ?? '')
	)

	let showVarPicker = $derived(
		[
			'python3',
			'bash',
			'powershell',
			'go',
			'deno',
			'bun',
			'bunnative',
			'nativets',
			'php',
			'rust',
			'csharp',
			'nu',
			'java',
			'ruby',
			'rlang'
			// for related places search: ADD_NEW_LANG
		].includes(lang ?? '')
	)

	let showResourcePicker = $derived(
		[
			'python3',
			'bash',
			'powershell',
			'go',
			'deno',
			'bun',
			'bunnative',
			'nativets',
			'php',
			'rust',
			'csharp',
			'nu',
			'java',
			'ruby',
			'rlang'
			// for related places search: ADD_NEW_LANG
		].includes(lang ?? '')
	)

	let showS3Picker = $derived(
		['duckdb', 'python3'].includes(lang ?? '') ||
			['typescript', 'javascript'].includes(scriptLangToEditorLang(lang))
	)
	// Declared metrics compile to a SELECT, so only a DuckDB script can take the
	// insertion; the other DuckLake-capable languages call it through the SDK.
	let showMetricsDrawer = $derived(lang === 'duckdb')
	let showDucklakePicker = $derived(
		['duckdb', 'python3'].includes(lang ?? '') ||
			['typescript', 'javascript'].includes(scriptLangToEditorLang(lang))
	)
	let showDataTablePicker = $derived(
		['duckdb', 'python3'].includes(lang ?? '') ||
			['typescript', 'javascript'].includes(scriptLangToEditorLang(lang))
	)
	let showDatabasePicker = $derived(['duckdb'].includes(lang ?? ''))
	let showGitRepoPicker = $derived(lang === 'ansible')

	let showResourceTypePicker = $derived(
		['typescript', 'javascript'].includes(scriptLangToEditorLang(lang)) ||
			lang === 'python3' ||
			lang === 'php' ||
			lang === 'rust'
	)

	let codeViewer: Drawer | undefined = $state()
	let codeObj: { language: SupportedLanguage; content: string } | undefined = $state(undefined)

	function getHelperItems(): Item[] {
		const items: Item[] = []
		if (showContextVarPicker && customUi?.contextVar != false) {
			items.push({
				displayName: 'Context variable',
				icon: DollarSign,
				action: () => contextualVariablePicker?.openDrawer()
			})
		}
		if (showVarPicker && customUi?.variable != false) {
			items.push({
				displayName: 'Variable',
				icon: DollarSign,
				action: () => variablePicker?.openDrawer()
			})
		}
		if (showS3Picker && customUi?.s3object != false) {
			items.push({
				displayName: 'S3 object',
				icon: File,
				action: () => s3FilePicker?.open()
			})
		}
		if (showResourcePicker && customUi?.resource != false) {
			items.push({
				displayName: 'Resource',
				icon: Package,
				action: () => resourcePicker?.openDrawer()
			})
		}
		if (showGitRepoPicker && customUi?.resource != false) {
			items.push({
				displayName: 'Git repository',
				icon: GitBranch,
				action: () => (gitRepoPickerOpen = true)
			})
		}
		if (showResourceTypePicker && customUi?.type != false) {
			items.push({
				displayName: 'Resource type',
				icon: Package,
				action: () => resourceTypePicker?.openDrawer()
			})
		}
		if (showDatabasePicker && customUi?.database != false) {
			items.push({
				displayName: 'Database',
				icon: DatabaseIcon,
				action: () => databasePicker?.openDrawer()
			})
		}
		if (showDucklakePicker && customUi?.ducklake != false) {
			items.push({
				displayName: 'Ducklake',
				icon: DucklakeIcon,
				action: () => ducklakePicker?.openDrawer()
			})
		}
		if (showMetricsDrawer && customUi?.metrics != false) {
			items.push({
				displayName: 'Metrics',
				icon: Sigma,
				action: () => metricsDrawer?.open()
			})
		}
		if (showDataTablePicker && customUi?.dataTable != false) {
			items.push({
				displayName: 'Data table',
				icon: DatabaseIcon,
				action: () => dataTablePicker?.openDrawer()
			})
		}
		if (customUi?.reset != false) {
			items.push({
				displayName: 'Reset content',
				icon: RotateCw,
				action: () => clearContent(),
				separatorTop: items.length > 0
			})
		}
		return items
	}

	function insertDelegateToGitRepo(resourcePath: string) {
		if (!editor) return

		const currentCode = editor.getCode()
		const newCode = insertDelegateToGitRepoInCode(currentCode, resourcePath)
		editor.setCode(newCode)
	}

	function addEditorActions() {
		editor?.addAction('insert-variable', 'Windmill: Insert variable', () => {
			variablePicker?.openDrawer()
		})
		editor?.addAction('insert-resource', 'Windmill: Insert resource', () => {
			resourcePicker?.openDrawer()
		})
	}

	$effect(() => {
		editor && untrack(() => addEditorActions())
	})

	async function loadVariables() {
		return await VariableService.listVariable({ workspace: ws ?? '' })
	}

	async function loadContextualVariables() {
		return await VariableService.listContextualVariables({
			workspace: ws ?? 'NO_W'
		})
	}

	let scriptPicker: Drawer | undefined = $state()
	let pick_existing: 'hub' | 'workspace' = $state('hub')
	let filter = $state('')

	async function onScriptPick(e: { detail: { path: string } }) {
		codeObj = undefined
		codeViewer?.openDrawer?.()
		codeObj = await getScriptByPath(e.detail.path ?? '', ws)
	}

	const dispatch = createEventDispatcher()

	function compile(schema: Schema) {
		function rec(x: { [name: string]: SchemaProperty }, root = false) {
			let res = '{\n'
			const entries = Object.entries(x)
			if (entries.length == 0) {
				return 'any'
			}
			let i = 0
			for (let [name, prop] of entries) {
				if (prop.type == 'object') {
					res += `${name}: ${rec(prop.properties ?? {})}`
				} else if (prop.type == 'array') {
					res += `${name}: ${prop?.items?.type ?? 'any'}[]`
				} else {
					let typ = prop?.type ?? 'any'
					if (typ == 'integer') {
						typ = 'number'
					}
					res += `${name}: ${typ}`
				}
				i++
				if (i < entries.length) {
					res += ',\n'
				}
			}
			if (!root) {
				res += '\n}'
			}
			return res
		}
		return rec(schema.properties, true)
	}

	async function quicktypeJSONSchema(targetLanguage, typeName, jsonSchemaString, rendererOptions) {
		const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore())

		// We could add multiple schemas for multiple types,
		// but here we're just making one type from JSON schema.
		await schemaInput.addSource({ name: typeName, schema: jsonSchemaString })

		const inputData = new InputData()
		inputData.addInput(schemaInput)

		return await quicktype({
			inputData,
			lang: targetLanguage,
			rendererOptions
		})
	}
	async function resourceTypePickCallback(name: string) {
		if (!editor) return
		const resourceType = await ResourceService.getResourceType({
			workspace: ws ?? 'NO_W',
			path: name
		})

		if (lang == 'python3') {
			const pySchema = pythonCompile(resourceType.schema as any)

			editor.insertAtCursor(`class ${name}(TypedDict):\n${pySchema}\n`)
			const code = editor.getCode()
			if (!code.includes('from typing import TypedDict')) {
				editor.insertAtBeginning('from typing import TypedDict\n')
			}
		} else if (lang === 'php') {
			const phpSchema = phpCompile(resourceType.schema as any)
			const rtName = toCamel(capitalize(name))
			editor.insertAtCursor(`if (!class_exists('${rtName}')) {\nclass ${rtName} {\n${phpSchema}\n`)
			editor.backspace()
			editor.insertAtCursor('}')
		} else if (lang === 'rust') {
			const { lines: lines } = await quicktypeJSONSchema(
				'rust',
				name,
				JSON.stringify(resourceType.schema),
				{
					'leading-comments': false,
					density: 'dense',
					'derive-debug': true
				}
			)
			editor.insertAtCurrentLine(lines.join('\n'))
		} else {
			const tsSchema = compile(resourceType.schema as any)
			editor.insertAtCursor(`type ${toCamel(capitalize(name))} = ${tsSchema}`)
		}
		sendUserToast(`${name} inserted at cursor`)
	}

	function phpCompile(schema: Schema) {
		let res = '  '
		const entries = Object.entries(schema.properties)
		if (entries.length === 0) {
			return 'array'
		}
		let i = 0
		for (let [name, prop] of entries) {
			let typ = 'array'
			if (prop.type === 'array') {
				typ = 'array'
			} else if (prop.type === 'string') {
				typ = 'string'
			} else if (prop.type === 'number') {
				typ = 'float'
			} else if (prop.type === 'integer') {
				typ = 'int'
			} else if (prop.type === 'boolean') {
				typ = 'bool'
			}
			res += `public ${typ} $${name};`
			i++
			if (i < entries.length) {
				res += '\n'
			}
		}
		return res
	}
	function pythonCompile(schema: Schema) {
		let res = ''
		const entries = Object.entries(schema.properties)
		if (entries.length === 0) {
			return 'dict'
		}
		let i = 0
		for (let [name, prop] of entries) {
			let typ = 'dict'
			if (prop.type === 'array') {
				typ = 'list'
			} else if (prop.type === 'string') {
				typ = 'str'
			} else if (prop.type === 'number') {
				typ = 'float'
			} else if (prop.type === 'integer') {
				typ = 'int'
			} else if (prop.type === 'boolean') {
				typ = 'bool'
			}
			res += `${name}: ${typ}`
			i++
			if (i < entries.length) {
				res += '\n'
			}
		}
		return res
	}

	function clearContent() {
		if (editor) {
			const resetCode = getResetCode(lang, kind as Script['kind'], template)
			editor.setCode(resetCode)
		}
	}

	function windmillPathToCamelCaseName(path: string): string {
		const parts = path.split('/')
		const lastPart = parts[parts.length - 1]

		const words = lastPart.split('_')

		return words
			.map((word, index) => {
				if (index === 0) {
					// Lowercase the first word
					return word.toLowerCase()
				} else {
					// Capitalize the first letter of subsequent words
					return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
				}
			})
			.join('')
	}
</script>

{#if scriptPath}
	<Drawer bind:open={showHistoryDrawer} size="1200px">
		<DrawerContent title="Versions History" on:close={() => (showHistoryDrawer = false)}>
			<ScriptVersionHistory {scriptPath} />
		</DrawerContent>
	</Drawer>
{/if}

<Drawer bind:this={scriptPicker} size="900px">
	<DrawerContent title="Code" on:close={scriptPicker.closeDrawer}>
		{#if pick_existing == 'hub'}
			<PickHubScript bind:filter {kind} on:pick={onScriptPick}>
				<ToggleHubWorkspace bind:selected={pick_existing} />
			</PickHubScript>
		{:else}
			<WorkspaceScriptPicker bind:filter {kind} on:pick={onScriptPick}>
				<ToggleHubWorkspace bind:selected={pick_existing} />
			</WorkspaceScriptPicker>
		{/if}
	</DrawerContent>
</Drawer>

<Drawer bind:this={codeViewer} size="600px">
	<DrawerContent title="Code" on:close={codeViewer.closeDrawer}>
		{#if codeObj}
			<HighlightCode language={codeObj?.language} code={codeObj?.content} />
		{:else}
			<Skeleton layout={[[40]]} />
		{/if}
	</DrawerContent>
</Drawer>

<ItemPicker
	bind:this={contextualVariablePicker}
	pickCallback={(path, name) => {
		if (!editor) return
		if (lang == 'deno') {
			editor.insertAtCursor(`Deno.env.get('${name}')`)
		} else if (lang === 'bun' || lang === 'bunnative' || lang == 'nativets') {
			editor.insertAtCursor(`process.env["${name}"]`)
		} else if (lang == 'python3') {
			if (!editor.getCode().includes('import os')) {
				editor.insertAtBeginning('import os\n')
			}
			editor.insertAtCursor(`os.environ.get("${name}")`)
		} else if (lang == 'go') {
			if (!editor.getCode().includes('"os"')) {
				editor.insertAtLine('import "os"\n', 2)
			}
			editor.insertAtCursor(`os.Getenv("${name}")`)
		} else if (lang == 'bash') {
			editor.insertAtCursor(`$${name}`)
		} else if (lang == 'powershell') {
			editor.insertAtCursor(`$Env:${name}`)
		} else if (lang == 'php') {
			editor.insertAtCursor(`getenv('${name}');`)
		} else if (lang == 'rust') {
			editor.insertAtCursor(`std::env::var("${name}").unwrap();`)
		} else if (lang == 'csharp') {
			editor.insertAtCursor(`Environment.GetEnvironmentVariable("${name}");`)
		} else if (lang == 'nu') {
			editor.insertAtCursor(`$env.${name}`)
		} else if (lang == 'java') {
			editor.insertAtCursor(`System.getenv("${name}");`)
			// for related places search: ADD_NEW_LANG
		} else if (lang == 'ruby') {
			editor.insertAtCursor(`ENV['${name}']`)
		} else if (lang == 'rlang') {
			editor.insertAtCursor(`Sys.getenv("${name}")`)
		} else if (lang == 'ansible') {
			editor.insertAtCursor(`{{ lookup('env', '${name}') }}`)
		} else if (
			['postgresql', 'mysql', 'bigquery', 'mssql', 'oracledb', 'snowflake', 'duckdb'].includes(
				lang ?? ''
			)
		) {
			editor.insertAtCursor(`%%${name}%%`)
		}
		sendUserToast(`${name} inserted at cursor`)
	}}
	tooltip="Contextual Variables are variables whose values are contextual to the Script
	execution. They are are automatically set by Windmill."
	documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets#contextual-variables"
	itemName="Contextual Variable"
	extraField="name"
	loadItems={loadContextualVariables}
/>

<ItemPicker
	bind:this={variablePicker}
	pickCallback={(path, name) => {
		if (!editor) return
		if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
			if (!editor.getCode().includes('import * as wmill from')) {
				editor.insertAtBeginning(getImportWmillTsStatement(lang))
			}
			editor.insertAtCursor(`(await wmill.getVariable('${path}'))`)
		} else if (lang == 'python3') {
			if (!editor.getCode().includes('import wmill')) {
				editor.insertAtBeginning('import wmill\n')
			}
			editor.insertAtCursor(`wmill.get_variable("${path}")`)
		} else if (lang == 'go') {
			if (!editor.getCode().includes('wmill "github.com/windmill-labs/windmill-go-client"')) {
				editor.insertAtLine('import wmill "github.com/windmill-labs/windmill-go-client"\n\n', 3)
			}
			editor.insertAtCursor(`v, _ := wmill.GetVariable("${path}")`)
		} else if (lang == 'bash') {
			if (bashRunsInCustomImage(editor.getCode())) {
				// Custom image: no wmill CLI. Fall back to curl, then busybox wget
				// (the default `# sandbox alpine:latest` image ships wget, not curl).
				// get_value returns a JSON-quoted string, so strip the outer quotes
				// to match the `jq -r .value` output of the non-sandbox branch.
				editor.insertAtCursor(
					`{ curl -sf -H "Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/variables/get_value/${path}" 2>/dev/null || wget -qO- --header="Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/variables/get_value/${path}"; } | sed 's/^"//;s/"$//'`
				)
			} else {
				editor.insertAtCursor(`wmill variable get ${path} --json | jq -r .value`)
			}
		} else if (lang == 'powershell') {
			editor.insertAtCursor(`$Headers = @{\n"Authorization" = "Bearer $Env:WM_TOKEN"`)
			editor.arrowDown()
			editor.insertAtCursor(
				`\nInvoke-RestMethod -Headers $Headers -Uri "$Env:BASE_INTERNAL_URL/api/w/$Env:WM_WORKSPACE/variables/get_value/${path}"`
			)
		} else if (lang == 'php') {
			editor.insertAtCursor(`$ch = curl_init(getenv('BASE_INTERNAL_URL') . '/api/w/' . getenv('WM_WORKSPACE') . '/variables/get_value/${path}');
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . getenv('WM_TOKEN')));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$var = json_decode(curl_exec($ch));`)
		} else if (lang == 'csharp') {
			editor.insertAtCursor(`var baseUrl = Environment.GetEnvironmentVariable("BASE_INTERNAL_URL");
var workspace = Environment.GetEnvironmentVariable("WM_WORKSPACE");
var uri = $"{baseUrl}/api/w/{workspace}/variables/get_value/${path}";
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("WM_TOKEN")}");

string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri);
`)
		} else if (lang == 'nu') {
			editor.insertAtCursor(`get_variable ${path}`)
		} else if (lang == 'java') {
			editor.insertAtCursor(`(Wmill.getVariable("${path}"))`)
			// for related places search: ADD_NEW_LANG
		} else if (lang == 'ruby') {
			if (!editor.getCode().includes("require 'windmill/mini'")) {
				editor.insertAtBeginning("require 'windmill/mini'\n")
			}
			editor.insertAtCursor(`get_variable("${path}")`)
		} else if (lang == 'rlang') {
			editor.insertAtCursor(`get_variable("${path}")`)
		}
		sendUserToast(`${name} inserted at cursor`)
	}}
	tooltip="Variables are dynamic values that have a key associated to them and can be retrieved during the execution of a Script or Flow."
	documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
	itemName="Variable"
	extraField="path"
	loadItems={loadVariables}
	buttons={{ 'Edit/View': (x) => variableEditor?.editVariable(x) }}
>
	{#snippet submission()}
		<div class="flex flex-row">
			<Button
				variant="accent"
				startIcon={{ icon: Plus }}
				on:click={() => {
					variableEditor?.initNew()
				}}
			>
				New variable
			</Button>
		</div>
	{/snippet}
</ItemPicker>

<ItemPicker
	bind:this={resourcePicker}
	pickCallback={(path, _, resType) => {
		if (!editor) return
		if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
			if (!editor.getCode().includes('import * as wmill from')) {
				editor.insertAtBeginning(getImportWmillTsStatement(lang))
			}
			editor.insertAtCursor(`(await wmill.getResource('${path}'))`)
		} else if (lang == 'python3') {
			if (!editor.getCode().includes('import wmill')) {
				editor.insertAtBeginning('import wmill\n')
			}
			editor.insertAtCursor(`wmill.get_resource("${path}")`)
		} else if (lang == 'go') {
			if (!editor.getCode().includes('wmill "github.com/windmill-labs/windmill-go-client"')) {
				editor.insertAtLine('import wmill "github.com/windmill-labs/windmill-go-client"\n\n', 3)
			}
			editor.insertAtCursor(`r, _ := wmill.GetResource("${path}")`)
		} else if (lang == 'bash') {
			if (bashRunsInCustomImage(editor.getCode())) {
				// Custom image: no wmill CLI. Fall back to curl, then busybox wget
				// (the default `# sandbox alpine:latest` image ships wget, not curl).
				// get_value_interpolated returns JSON, matching the `jq .value` branch.
				editor.insertAtCursor(
					`curl -sf -H "Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/resources/get_value_interpolated/${path}" 2>/dev/null || wget -qO- --header="Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/resources/get_value_interpolated/${path}"`
				)
			} else {
				editor.insertAtCursor(`wmill resource get ${path} --json | jq .value`)
			}
		} else if (lang == 'powershell') {
			editor.insertAtCursor(`$Headers = @{\n"Authorization" = "Bearer $Env:WM_TOKEN"`)
			editor.arrowDown()
			editor.insertAtCursor(
				`\nInvoke-RestMethod -Headers $Headers -Uri "$Env:BASE_INTERNAL_URL/api/w/$Env:WM_WORKSPACE/resources/get_value_interpolated/${path}"`
			)
		} else if (lang == 'php') {
			editor.insertAtCursor(`$ch = curl_init(getenv('BASE_INTERNAL_URL') . '/api/w/' . getenv('WM_WORKSPACE') . '/resources/get_value_interpolated/${path}');
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . getenv('WM_TOKEN')));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = json_decode(curl_exec($ch));`)
		} else if (lang == 'csharp') {
			if (!editor.getCode().includes(`using System.Text.Json.Nodes;`)) {
				editor.insertAtBeginning(`using System.Text.Json.Nodes;\n`)
			}
			editor.insertAtCursor(`var baseUrl = Environment.GetEnvironmentVariable("BASE_INTERNAL_URL");
var workspace = Environment.GetEnvironmentVariable("WM_WORKSPACE");
var uri = $"{baseUrl}/api/w/{workspace}/resources/get_value_interpolated/${path}";
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("WM_TOKEN")}");

JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetStringAsync(uri));
`)
		} else if (lang == 'nu') {
			editor.insertAtCursor(`get_resource ${path}`)
		} else if (lang == 'java') {
			editor.insertAtCursor(`(Wmill.getResource("${path}"))`)
			// for related places search: ADD_NEW_LANG
		} else if (lang == 'ruby') {
			if (!editor.getCode().includes("require 'windmill/mini'")) {
				editor.insertAtBeginning("require 'windmill/mini'\n")
			}
			editor.insertAtCursor(`get_resource("${path}")`)
		} else if (lang == 'rlang') {
			editor.insertAtCursor(`get_resource("${path}")`)
		} else if (lang == 'duckdb') {
			let t = { postgresql: 'postgres', mysql: 'mysql', bigquery: 'bigquery' }[resType]
			if (!t) {
				sendUserToast(`Resource type ${resType} is not supported in DuckDB`, true)
				editor.insertAtCursor(`'res://${path}'`)
				return
			} else {
				editor.insertAtCursor(`ATTACH 'res://${path}' AS db (TYPE ${t});`)
			}
		}

		sendUserToast(`${path} inserted at cursor`)
	}}
	tooltip="Resources represent connections to third party systems. Resources are a good way to define a connection to a frequently used third party system such as a database."
	documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
	itemName="Resource"
	buttons={{ 'Edit/View': (x) => resourceEditor?.initEdit(x) }}
	extraField="description"
	extraField2="resource_type"
	loadItems={async () => await ResourceService.listResource({ workspace: ws ?? 'NO_W' })}
>
	{#snippet submission()}
		<div class="flex flex-row gap-x-1 mr-2">
			<Button
				startIcon={{ icon: Plus }}
				target="_blank"
				variant="accent"
				href="{base}/resources?connect_app=undefined"
			>
				Add resource
			</Button>
		</div>
	{/snippet}
</ItemPicker>

{#if showResourceTypePicker}
	<ItemPicker
		bind:this={resourceTypePicker}
		pickCallback={async (_, name) => {
			resourceTypePickCallback(name)
		}}
		tooltip="Resources Types are the schemas associated with a Resource. They define the structure of the data that is returned from a Resource."
		documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
		itemName="Resource Type"
		extraField="name"
		loadItems={async () => await ResourceService.listResourceType({ workspace: ws ?? 'NO_W' })}
	/>
{/if}
<ResourceEditorDrawer
	bind:this={resourceEditor}
	workspace={ws}
	on:refresh={resourcePicker.openDrawer}
/>
<VariableEditor bind:this={variableEditor} workspace={ws} on:create={variablePicker.openDrawer} />

{#if showDucklakePicker}
	<ItemPicker
		bind:this={ducklakePicker}
		pickCallback={async (_, name) => {
			if (lang === 'duckdb') {
				const connStr = name == 'main' ? 'ducklake' : `ducklake://${name}`
				editor?.insertAtCursor(`ATTACH '${connStr}' AS dl; USE dl;\n`)
			} else if (lang === 'python3') {
				if (!editor?.getCode().includes('import wmill')) {
					editor?.insertAtBeginning('import wmill\n')
				}
				editor?.insertAtCursor(`dl = wmill.ducklake(${name == 'main' ? '' : `'${name}'`})\n`)
			} else if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
				if (!editor?.getCode().includes('import * as wmill from')) {
					editor?.insertAtBeginning(getImportWmillTsStatement(lang))
				}
				editor?.insertAtCursor(`let sql = wmill.ducklake(${name == 'main' ? '' : `'${name}'`})\n`)
			}
		}}
		tooltip="Attach a Ducklake to your scripts. Ducklake allows you to manipulate large data on S3 blob files through a traditional SQL interface."
		documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/ducklake"
		itemName="ducklake"
		loadItems={async () =>
			(await WorkspaceService.listDucklakes({ workspace: ws ?? 'NO_W' })).map((path) => ({ path }))}
	>
		{#snippet submission()}
			<div class="flex flex-row gap-x-1 mr-2">
				<Button
					startIcon={{ icon: Settings }}
					target="_blank"
					variant="accent"
					href="{base}/workspace_settings?tab=ducklake"
				>
					Go to settings
				</Button>
			</div>
		{/snippet}
	</ItemPicker>
{/if}

{#if showDataTablePicker}
	<ItemPicker
		bind:this={dataTablePicker}
		pickCallback={async (_, name) => {
			if (lang === 'duckdb') {
				const connStr = name == 'main' ? 'datatable' : `datatable://${name}`
				editor?.insertAtCursor(`ATTACH '${connStr}' AS dt; USE dt;\n`)
			} else if (lang === 'python3') {
				if (!editor?.getCode().includes('import wmill')) {
					editor?.insertAtBeginning('import wmill\n')
				}
				editor?.insertAtCursor(`db = wmill.datatable(${name == 'main' ? '' : `'${name}'`})\n`)
			} else if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
				if (!editor?.getCode().includes('import * as wmill from')) {
					editor?.insertAtBeginning(getImportWmillTsStatement(lang))
				}
				editor?.insertAtCursor(`let sql = wmill.datatable(${name == 'main' ? '' : `'${name}'`})\n`)
				editor?.insertAtCursor(`let query_result = await sql\`SELECT * FROM _\`.fetchOne()\n`)
			}
		}}
		tooltip="Attach a datatable to your script."
		documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables"
		itemName="data table"
		loadItems={async () =>
			(await WorkspaceService.listDataTables({ workspace: ws ?? 'NO_W' })).map((d) => ({
				path: d.name
			}))}
	>
		{#snippet submission()}
			<div class="flex flex-row gap-x-1 mr-2">
				<Button
					startIcon={{ icon: Settings }}
					target="_blank"
					variant="accent"
					href="{base}/workspace_settings?tab=windmill_data_tables"
				>
					Go to settings
				</Button>
			</div>
		{/snippet}
	</ItemPicker>
{/if}

{#if showDatabasePicker}
	<ItemPicker
		bind:this={databasePicker}
		pickCallback={(path, _, resType) => {
			if (!editor) return
			if (lang == 'duckdb') {
				let t = { postgresql: 'postgres', mysql: 'mysql', bigquery: 'bigquery' }[resType]
				editor.insertAtCursor(`ATTACH 'res://${path}' AS db (TYPE ${t});`)
			}
			sendUserToast(`${path} inserted at cursor`)
		}}
		tooltip="Attach a database resource in your script. This allows you to query data from the database using SQL."
		documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
		itemName="Database"
		buttons={{ 'Edit/View': (x) => resourceEditor?.initEdit(x) }}
		extraField="description"
		extraField2="resource_type"
		loadItems={async () =>
			await ResourceService.listResource({
				workspace: ws ?? 'NO_W',
				resourceType: 'postgresql,mysql,bigquery'
			})}
	></ItemPicker>
{/if}

{#if showMetricsDrawer && customUi?.metrics != false}
	<!-- Appended rather than inserted at the cursor: the snippet is a whole statement
	     block, and when it relies on an ATTACH already in the script it must follow it. -->
	<MetricsDrawer
		bind:this={metricsDrawer}
		workspace={ws}
		getCode={() => editor?.getCode() ?? ''}
		onInsert={(sql) => {
			// Terminate whatever the script ends with: appending a fresh statement
			// after an unterminated one produces invalid SQL. The separator starts on
			// its own line so the `;` cannot land inside a trailing line comment.
			const existing = editor?.getCode() ?? ''
			const sep =
				existing.trim() === '' ? '' : endsWithUnterminatedStatement(existing) ? '\n;\n\n' : '\n\n'
			editor?.append(sep + sql + '\n')
		}}
	/>
{/if}

<S3FilePicker
	bind:this={s3FilePicker}
	readOnlyMode={false}
	onSelectAndClose={(s3obj) => {
		let s = `'${formatS3Object(s3obj)}'`
		if (lang === 'duckdb') {
			editor?.insertAtCursor(`SELECT * FROM ${s};`)
		} else if (lang === 'python3') {
			if (!editor?.getCode().includes('import wmill')) {
				editor?.insertAtBeginning('import wmill\n')
			}
			editor?.insertAtCursor(`wmill.load_s3_file(${s})`)
		} else if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
			if (!editor?.getCode().includes('import * as wmill from')) {
				editor?.insertAtBeginning(getImportWmillTsStatement(lang))
			}
			editor?.insertAtCursor(`wmill.loadS3File(${s})`)
		}
	}}
/>

<div class="flex justify-between items-center overflow-y-auto w-full p-0.5">
	<div class="flex gap-3 items-center">
		<div
			title={!validCode
				? 'Main function not parsable'
				: validAssets === false
					? 'Assets not parsable'
					: 'Parsable'}
			class="rounded-full w-2 h-2 mx-2 {validCode && validAssets !== false
				? 'bg-green-300'
				: 'bg-red-300'}"
		></div>
		<div class="flex items-center gap-2">
			{#if compactHelpers}
				{#snippet helpersDropdown()}
					<DropdownV2 items={getHelperItems} placement="bottom-start">
						{#snippet buttonReplacement()}
							<Button
								nonCaptureEvent
								variant="subtle"
								unifiedSize="sm"
								startIcon={{ icon: Plus }}
								title="Helpers"
							>
								Helpers
							</Button>
						{/snippet}
					</DropdownV2>
				{/snippet}
				{#if showGitRepoPicker && customUi?.resource != false}
					<!-- Wrap the Helpers dropdown so the Git-repo popover anchors to
					     the visible Helpers button rather than an sr-only placeholder. -->
					<GitRepoPopoverPicker
						bind:isOpen={gitRepoPickerOpen}
						on:selected={(e) => insertDelegateToGitRepo(e.detail.resourcePath)}
					>
						{@render helpersDropdown()}
					</GitRepoPopoverPicker>
				{:else}
					{@render helpersDropdown()}
				{/if}
			{:else}
				{#if showContextVarPicker && customUi?.contextVar != false}
					<Button
						aiId="editor-bar-add-context-variable"
						aiDescription="Add context variable"
						title="Add context variable"
						variant="subtle"
						on:click={contextualVariablePicker.openDrawer}
						unifiedSize="sm"
						startIcon={{ icon: DollarSign }}
						{iconOnly}
						>+Context var
					</Button>
				{/if}
				{#if showVarPicker && customUi?.variable != false}
					<Button
						aiId="editor-bar-add-variable"
						aiDescription="Add variable"
						title="Add variable"
						variant="subtle"
						on:click={variablePicker.openDrawer}
						unifiedSize="sm"
						startIcon={{ icon: DollarSign }}
						{iconOnly}
					>
						+Variable
					</Button>
				{/if}

				{#if showS3Picker && customUi?.s3object != false}
					<Button
						aiId="editor-bar-add-s3-object"
						aiDescription="Add S3 Object"
						title="Add S3 object"
						variant="subtle"
						on:click={() => s3FilePicker?.open()}
						unifiedSize="sm"
						startIcon={{ icon: File }}
						{iconOnly}
						>+S3 Object
					</Button>
				{/if}

				{#if showResourcePicker && customUi?.resource != false}
					<Button
						aiId="editor-bar-add-resource"
						aiDescription="Add resource"
						title="Add resource"
						unifiedSize="sm"
						variant="subtle"
						on:click={resourcePicker.openDrawer}
						{iconOnly}
						startIcon={{ icon: Package }}
					>
						+Resource
					</Button>
				{/if}

				{#if showGitRepoPicker && customUi?.resource != false}
					<GitRepoPopoverPicker
						bind:isOpen={gitRepoPickerOpen}
						on:selected={(e) => insertDelegateToGitRepo(e.detail.resourcePath)}
					>
						<Button
							aiId="editor-bar-add-git-repo"
							aiDescription="Delegate to Git repository"
							title="Delegate to Git repository"
							unifiedSize="sm"
							variant="subtle"
							on:click={() => (gitRepoPickerOpen = true)}
							{iconOnly}
							startIcon={{ icon: GitBranch }}
						>
							+Git Repo
						</Button>
					</GitRepoPopoverPicker>
				{/if}

				{#if showResourceTypePicker && customUi?.type != false}
					<Button
						aiId="editor-bar-add-resource-type"
						aiDescription="Add resource type"
						title="Add resource type"
						variant="subtle"
						unifiedSize="sm"
						on:click={() => resourceTypePicker?.openDrawer()}
						{iconOnly}
						startIcon={{ icon: Package }}
					>
						+Type
					</Button>
				{/if}

				{#if showDatabasePicker && customUi?.database != false}
					<Button
						aiId="editor-bar-add-database"
						aiDescription="Add database"
						title="Add database"
						variant="subtle"
						on:click={() => databasePicker?.openDrawer()}
						unifiedSize="sm"
						startIcon={{ icon: DatabaseIcon }}
						{iconOnly}
						>+Database
					</Button>
				{/if}

				{#if showDucklakePicker && customUi?.ducklake != false}
					<Button
						aiId="editor-bar-use-ducklake"
						aiDescription="Use Ducklake"
						title="Use Ducklake"
						variant="subtle"
						on:click={() => ducklakePicker?.openDrawer()}
						unifiedSize="sm"
						startIcon={{ icon: DucklakeIcon }}
						{iconOnly}
						>+Ducklake
					</Button>
				{/if}

				{#if showMetricsDrawer && customUi?.metrics != false}
					<Button
						aiId="editor-bar-metrics"
						aiDescription="Open the measures and dimensions declared on DuckLake tables"
						title="Metrics"
						variant="subtle"
						on:click={() => metricsDrawer?.open()}
						unifiedSize="sm"
						startIcon={{ icon: Sigma }}
						{iconOnly}
						>Metrics
					</Button>
				{/if}

				{#if showDataTablePicker && customUi?.dataTable != false}
					<Button
						aiId="editor-bar-use-datatable"
						aiDescription="Use DataTable"
						title="Use DataTable"
						variant="subtle"
						on:click={() => dataTablePicker?.openDrawer()}
						unifiedSize="sm"
						startIcon={{ icon: DatabaseIcon }}
						{iconOnly}
						>+Data table
					</Button>
				{/if}

				{#if customUi?.reset != false}
					<Button
						aiId="editor-bar-reset-content"
						aiDescription="Reset content"
						title="Reset Content"
						unifiedSize="sm"
						variant="subtle"
						on:click={clearContent}
						{iconOnly}
						startIcon={{ icon: RotateCw }}
					>
						Reset
					</Button>
				{/if}
			{/if}

			{#if customUi?.assistants != false}
				{#if lang == 'deno' || lang == 'python3' || lang == 'go' || lang == 'bash'}
					<Button
						aiId="editor-bar-reload-assistants"
						aiDescription="Reload assistants"
						unifiedSize="sm"
						variant="subtle"
						on:click={() => editor?.reloadWebsocket()}
						startIcon={{
							icon: RotateCw,
							classes: websocketAlive[lang] == false ? 'animate-spin' : ''
						}}
						title="Reload assistants"
					>
						{#if !iconOnly}
							Assistants
						{/if}
						<span class="-my-1">
							{#if lang == 'deno'}
								(<span class={websocketAlive.deno ? 'green' : 'text-red-700'}>Deno</span>)
							{:else if lang == 'go'}
								(<span class={websocketAlive.go ? 'green' : 'text-red-700'}>Go</span>)
							{:else if lang == 'python3'}
								(<span class={websocketAlive.pyright ? 'green' : 'text-red-700'}>Pyright</span>
								<span class={websocketAlive.ruff ? 'green' : 'text-red-700'}>Ruff</span>)
							{:else if lang == 'bash'}
								(<span class={websocketAlive.shellcheck ? 'green' : 'text-red-700'}>Shellcheck</span
								>)
							{/if}
						</span>
					</Button>
				{/if}
			{/if}

			{#if customUi?.diffMode != false}
				<div class="flex items-center px-3">
					<Toggle
						options={{ right: 'Diff' }}
						size="sm"
						checked={diffMode}
						disabled={!lastDeployedCode}
						on:change={(e) => {
							const turnOn = e.detail
							dispatch(turnOn ? 'showDiffMode' : 'hideDiffMode')
						}}
					/>
					<Popover>
						{#snippet text()}
							Toggle diff mode
						{/snippet}
						<DiffIcon class="ml-1 text-primary" size={14} />
					</Popover>
				</div>
			{/if}

			{#if collabMode && customUi?.multiplayer != false}
				<div class="flex items-center px-3">
					<Toggle
						options={{ right: '' }}
						size="xs"
						checked={collabLive}
						on:change={() => dispatch('toggleCollabMode')}
					/>
					<Popover>
						{#snippet text()}
							Multiplayer
						{/snippet}
						<Users class="ml-1 text-primary" size={14} />
					</Popover>
					{#if collabLive}
						<button
							title="Show invite link"
							class="p-1 rounded hover:bg-gray-400 mx-1 border"
							onclick={() => dispatch('collabPopup')}><Link size={14} /></button
						>
						<div class="isolate flex -space-x-2 pl-2">
							{#each collabUsers as user}
								<span
									class="inline-flex h-6 w-6 items-center justify-center rounded-full ring-2 ring-white bg-gray-600"
									title={user.name}
								>
									<span class="text-sm font-semibold leading-none text-white">
										{user.name.substring(0, 2).toLocaleUpperCase()}
									</span>
								</span>
							{/each}
						</div>
					{/if}
				</div>
			{/if}

			{#if customUi?.aiGen != false}
				{#if openAiChat}
					<FlowInlineScriptAiButton {moduleId} btnProps={{ variant: 'subtle' }} />
				{/if}
			{/if}

			<EditorSettings {customUi} btnProps={{ variant: 'subtle', unifiedSize: 'md' }} />
		</div>
	</div>

	<div class="flex flex-row items-center gap-2 whitespace-nowrap">
		{@render right?.()}
		{#if scriptPath && !noHistory && customUi?.history != false}
			<Button
				unifiedSize="sm"
				variant="subtle"
				on:click={() => (showHistoryDrawer = true)}
				{iconOnly}
				startIcon={{ icon: History }}
				title="See history"
			>
				History
			</Button>
		{/if}
		{#if SCRIPT_EDITOR_SHOW_EXPLORE_OTHER_SCRIPTS && customUi?.library != false}
			<Button
				unifiedSize="sm"
				variant="subtle"
				on:click={scriptPicker.openDrawer}
				{iconOnly}
				startIcon={{ icon: Library }}
				title="Explore other scripts"
			>
				Library
			</Button>
		{/if}
		{#if saveToWorkspace && customUi?.saveToWorkspace != false}
			<Button
				unifiedSize="sm"
				variant="subtle"
				startIcon={{ icon: Save }}
				on:click={() => dispatch('createScriptFromInlineScript')}
				iconOnly={false}
			>
				Save to workspace
			</Button>
		{/if}
	</div>
</div>

<style lang="postcss">
	span.green {
		@apply text-green-600 animate-[pulse_5s_ease-in-out_infinite];
	}
</style>
