<script lang="ts">
	import { scimSamlSetting, settings, settingsKeys } from './instanceSettings'
	import { Alert, Button, Tab, TabContent, Tabs } from '$lib/components/common'
	import { SettingService, SettingsService } from '$lib/gen'
	import type { TeamsChannel } from '$lib/gen/types.gen'

	import { sendUserToast } from '$lib/toast'
	import { deepEqual } from 'fast-equals'

	import { sleep } from '$lib/utils'
	import { enterpriseLicense } from '$lib/stores'

	import { createEventDispatcher } from 'svelte'
	import { setLicense } from '$lib/enterpriseUtils'
	import AuthSettings from './AuthSettings.svelte'
	import ScimOauthSettings from './ScimOauthSettings.svelte'
	import oauthConnectRegistry from '$oauth_connect_registry'
	import InstanceSetting from './InstanceSetting.svelte'
	import { writable, type Writable } from 'svelte/store'
	import { ExternalLink, Loader2 } from 'lucide-svelte'
	import YAML from 'yaml'
	import Toggle from './Toggle.svelte'
	import SettingsFooter from './workspaceSettings/SettingsFooter.svelte'
	import SettingsPageHeader from './settings/SettingsPageHeader.svelte'
	import WorkspaceRegistries from './instanceSettings/WorkspaceRegistries.svelte'
	import DbHealth from './instanceSettings/DbHealth.svelte'

	interface Props {
		tab?: string
		hideTabs?: boolean
		closeDrawer?: (() => void) | undefined
		authSubTab?: 'sso' | 'oauth' | 'scim'
		onNavigateToTab?: (category: string) => void
		quickSetup?: boolean
		yamlMode?: boolean
		hasUnsavedChanges?: boolean
		hasAnyInvalid?: boolean
	}

	let {
		tab = $bindable('Core'),
		hideTabs = false,
		closeDrawer = () => {},
		authSubTab = $bindable('sso'),
		onNavigateToTab,
		quickSetup = false,
		yamlMode = $bindable(false),
		hasUnsavedChanges = $bindable(false),
		hasAnyInvalid = $bindable(false)
	}: Props = $props()

	let values: Writable<Record<string, any>> = writable({})
	let initialOauths: Record<string, any> = $state({})
	let initialRequirePreexistingUserForOauth: boolean = $state(false)
	let requirePreexistingUserForOauth: boolean = $state(false)

	let initialValues: Record<string, any> = $state({})
	let baseUrlIsFallback = $state(false)
	// Per-instance OAuth providers (Snowflake, ServiceNow, …): instance name
	// keyed by provider, used to build their per-instance connect_config URLs.
	let instanceInputs: Record<string, string> = $state({})
	let version: string = $state('')
	let loading = $state(true)

	export function getVersion(): string {
		return version
	}

	export function getLicenseKey(): string {
		return $values?.['license_key'] ?? ''
	}

	loadSettings()
	loadVersion()

	// When the user enables object storage for the first time, default
	// `monitor_logs_on_s3` to true so S3 log files get cleaned up with their
	// jobs. Backend still defaults to false for backwards compat with
	// operators who never touched the setting.
	$effect(() => {
		if ($values['object_store_cache_config'] && $values['monitor_logs_on_s3'] === undefined) {
			values.update((v) => ({ ...v, monitor_logs_on_s3: true }))
		}
	})

	const dispatch = createEventDispatcher()

	async function loadVersion() {
		version = await SettingsService.backendVersion()
	}
	let oauths: Record<string, any> = $state({})

	/** Ensure object/array-typed settings have a non-null default for the form UI */
	const formDefaults: Record<string, any> = {
		smtp_settings: {},
		otel: {},
		indexer_settings: {},
		critical_error_channels: [],
		github_enterprise_app: {}
	}

	function applyFormDefaults(vals: Record<string, any>): void {
		for (const [key, defaultVal] of Object.entries(formDefaults)) {
			if (vals[key] == undefined) {
				vals[key] =
					typeof defaultVal === 'object' ? JSON.parse(JSON.stringify(defaultVal)) : defaultVal
			}
		}
	}

	async function loadSettings() {
		loading = true

		// Bulk-load all settings in a single API call
		const config = await SettingService.getInstanceConfig()
		const gs = (config.global_settings ?? {}) as Record<string, any>

		initialOauths = gs['oauths'] ?? {}
		requirePreexistingUserForOauth = gs['require_preexisting_user_for_oauth'] ?? false
		initialRequirePreexistingUserForOauth = requirePreexistingUserForOauth
		oauths = JSON.parse(JSON.stringify(initialOauths))

		let nvalues: Record<string, any> = { ...gs }

		baseUrlIsFallback = !nvalues['base_url']
		if (nvalues['retention_period_secs'] == undefined) {
			nvalues['retention_period_secs'] = 60 * 60 * 24 * 30
		}
		applyFormDefaults(nvalues)

		// Apply select/select_python defaults so initialValues matches what InstanceSetting's $effect does
		for (const category of settingsKeys) {
			for (const s of settings[category]) {
				if (
					(s.fieldType === 'select' || s.fieldType === 'select_python') &&
					nvalues[s.key] == undefined &&
					s.defaultValue
				) {
					nvalues[s.key] = s.defaultValue()
				}
			}
		}

		// Snapshot initialValues before applying the base_url fallback so that
		// the dirty-check detects the unsaved default and enables the Save button.
		initialValues = JSON.parse(JSON.stringify(nvalues))

		if (baseUrlIsFallback) {
			nvalues['base_url'] = window.location.origin
		}
		$values = nvalues
		loading = false

		// populate per-instance OAuth provider inputs (snowflake, servicenow, …) from db
		loadInstanceInputs(oauths)
	}

	export async function saveSettings() {
		if (yamlMode) {
			if (!syncYamlToForm()) {
				return
			}
		}

		setupTemplatedOauthUrls()

		// Remove empty or invalid entries for critical error channels
		$values.critical_error_channels = $values.critical_error_channels.filter((entry: any) => {
			if (!entry || typeof entry !== 'object') return false
			if ('teams_channel' in entry) {
				return isValidTeamsChannel(entry.teams_channel)
			}
			if ('slack_channel' in entry) {
				return typeof entry.slack_channel === 'string' && entry.slack_channel.trim() !== ''
			}
			if ('email' in entry) {
				return typeof entry.email === 'string' && entry.email.trim() !== ''
			}
			// Unknown shape
			return false
		})

		let shouldReloadPage = false
		let willRestart = false
		if ($values) {
			// Trim license key before saving
			if ($values['license_key'] && typeof $values['license_key'] === 'string') {
				$values['license_key'] = $values['license_key'].trim()
			}

			// Check which settings require a page reload or server restart
			const allSettings = [...Object.values(settings), scimSamlSetting].flat()
			let licenseKeySet = false
			for (const s of allSettings) {
				if (s.storage === 'setting' && !deepEqual(initialValues?.[s.key], $values?.[s.key])) {
					if (s.key === 'license_key') {
						licenseKeySet = true
					}
					if (s.requiresReloadOnChange) {
						shouldReloadPage = true
					}
					if (s.triggersRestart) {
						willRestart = true
					}
				}
			}

			// Build the full global_settings object for the bulk PUT
			const globalSettings: Record<string, any> = { ...$values }

			// Send explicit null for keys that were set on load but are now
			// missing/cleared, so the Merge-mode bulk endpoint deletes them
			// instead of silently preserving the old DB value. Covers both:
			//   - YAML mode: user removed a line / set it to null / set it to {}.
			//   - Form mode: user toggled a setting off (e.g.
			//     object_store_cache_config), making the value undefined.
			const isClearedValue = (v: any) => {
				if (v === undefined || v === null) return true
				if (typeof v === 'object') {
					return Array.isArray(v) ? v.length === 0 : Object.keys(v).length === 0
				}
				return false
			}
			for (const key of Object.keys(initialValues ?? {})) {
				if (excludedKeys.has(key)) continue
				if (key === 'oauths' || key === 'require_preexisting_user_for_oauth') continue
				if (!isClearedValue(globalSettings[key])) continue
				// Only flag for deletion if it was non-empty before — otherwise
				// every save would delete-then-recreate harmless `{}`/`[]` defaults.
				if (isClearedValue(initialValues[key])) continue
				globalSettings[key] = null
			}

			// Include oauths and require_preexisting_user_for_oauth
			if (!deepEqual(initialOauths, oauths)) {
				globalSettings['oauths'] = oauths
			}
			if (initialRequirePreexistingUserForOauth !== requirePreexistingUserForOauth) {
				globalSettings['require_preexisting_user_for_oauth'] = requirePreexistingUserForOauth
			}

			await SettingService.setInstanceConfig({
				requestBody: { global_settings: globalSettings }
			})

			initialValues = JSON.parse(JSON.stringify($values))
			initialOauths = JSON.parse(JSON.stringify(oauths))
			initialRequirePreexistingUserForOauth = requirePreexistingUserForOauth
			baseUrlIsFallback = false

			if (yamlMode) {
				yamlCodeInitial = yamlCode
			}

			if (licenseKeySet) {
				setLicense()
			}
		} else {
			console.error('Values not loaded')
		}
		if (shouldReloadPage) {
			sendUserToast('Settings updated, reloading page...')
			await sleep(1000)
			window.location.reload()
		} else if (willRestart) {
			sendUserToast(
				'Settings updated. Servers are restarting and changes may take up to a minute to fully propagate.',
				false,
				[],
				undefined,
				8000
			)
			dispatch('saved')
		} else {
			sendUserToast('Settings updated')
			dispatch('saved')
		}
	}

	// Per-instance OAuth providers (Snowflake, ServiceNow, …) keyed by name ->
	// their registry connect_config_template. Adding a new one needs only a
	// registry entry — no code here.
	// Every per-instance templated provider is configurable here: authorization-code
	// ones (ServiceNow, Snowflake) provide an `auth_url`, client-credentials-only
	// ones (Coupa) provide only a `token_url`. Both need the admin to enter their
	// instance host so the shared credentials point at the right endpoint.
	const connectConfigTemplates: Record<string, any> = Object.fromEntries(
		Object.entries(oauthConnectRegistry)
			.filter(([, cfg]) => cfg && typeof cfg === 'object' && 'connect_config_template' in cfg)
			.map(([name, cfg]) => [name, (cfg as any).connect_config_template])
	)

	function normalizeInstanceInput(tmpl: any, raw: string): string {
		let v = (raw ?? '').replace(/\s/g, '')
		if (tmpl.strip_suffix) {
			// accept a full host/URL or a bare name -> reduce to the bare instance
			v = v.replace(/^https?:\/\//, '').replace(/\/.*$/, '')
			if (v.endsWith(tmpl.strip_suffix)) {
				v = v.slice(0, -tmpl.strip_suffix.length)
			}
		}
		return v
	}

	// Build each per-instance provider's connect_config from the admin-entered
	// instance name + its registry template (substituting {instance} into the
	// URLs). Replaces the old per-provider setup functions.
	function setupTemplatedOauthUrls() {
		for (const [name, tmpl] of Object.entries(connectConfigTemplates)) {
			if (!oauths?.[name]) continue
			const key = tmpl.extra_params_key ?? 'instance'
			const v = normalizeInstanceInput(tmpl, instanceInputs[name] ?? '')
			instanceInputs[name] = v
			if (oauths[name].connect_config?.extra_params?.[key] === v) continue
			oauths[name].connect_config = {
				scopes: tmpl.scopes ?? [],
				// CC-only templated providers have no auth_url; store an empty string
				// (not omitted) so the instance-config parser still types the entry.
				// The backend treats an empty auth_url as the unused placeholder for
				// the client-credentials grant.
				auth_url: tmpl.auth_url ? tmpl.auth_url.replaceAll('{instance}', v) : '',
				token_url: tmpl.token_url.replaceAll('{instance}', v),
				req_body_auth: tmpl.req_body_auth ?? false,
				extra_params: { [key]: v },
				extra_params_callback: {}
			}
		}
	}

	// Recover the instance-name inputs from a saved oauths config (for load/discard).
	function loadInstanceInputs(savedOauths: Record<string, any>) {
		for (const [name, tmpl] of Object.entries(connectConfigTemplates)) {
			const key = tmpl.extra_params_key ?? 'instance'
			instanceInputs[name] = savedOauths?.[name]?.connect_config?.extra_params?.[key] ?? ''
		}
	}

	let sendingStats = $state(false)
	async function sendStats() {
		try {
			sendingStats = true
			await SettingService.sendStats()
			sendUserToast('Usage sent')
		} catch (err) {
			throw err
		} finally {
			sendingStats = false
		}
	}

	let downloadingStats = $state(false)
	async function downloadStats() {
		try {
			downloadingStats = true
			const result = await SettingService.getStats()
			const blob = new Blob([result.data ?? ''], { type: 'application/json' })
			const url = URL.createObjectURL(blob)
			const a = document.createElement('a')
			a.href = url
			const date = new Date().toISOString().split('T')[0]
			a.download = `windmill-telemetry-${date}-${result.signature}.json`
			document.body.appendChild(a)
			a.click()
			document.body.removeChild(a)
			URL.revokeObjectURL(url)
			sendUserToast('Telemetry data downloaded')
		} catch (err) {
			throw err
		} finally {
			downloadingStats = false
		}
	}

	function isValidTeamsChannel(value: any): value is TeamsChannel {
		return (
			typeof value === 'object' &&
			value !== null &&
			typeof value.team_id === 'string' &&
			value.team_id.trim() !== '' &&
			typeof value.team_name === 'string' &&
			value.team_name.trim() !== '' &&
			typeof value.channel_id === 'string' &&
			value.channel_id.trim() !== '' &&
			typeof value.channel_name === 'string' &&
			value.channel_name.trim() !== ''
		)
	}

	function openSmtpSettings() {
		if (onNavigateToTab) {
			onNavigateToTab('SMTP')
		} else {
			tab = 'SMTP'
		}
	}

	// --- Dirty state tracking (YAML-based) ---

	function stripEmpty(obj: Record<string, any>): Record<string, any> {
		return Object.fromEntries(
			Object.entries(obj)
				.filter(([_, v]) => v !== undefined && v !== '')
				.map(([k, v]) =>
					v != null && typeof v === 'object' && !Array.isArray(v) ? [k, stripEmpty(v)] : [k, v]
				)
		)
	}

	function getSettingsForCategory(category: string) {
		if (category === 'Auth/OAuth/SAML') {
			return [...(settings[category] ?? []), ...scimSamlSetting]
		}
		const base = settings[category] ?? []
		// In quick setup, reorder Core: base settings (without license_key), then extras from Jobs
		if (quickSetup && category === 'Core') {
			const licenseKey = base.find((s) => s.key === 'license_key')
			const baseWithout = base.filter((s) => s.key !== 'license_key')
			const jobSettings = settings['Jobs'] ?? []
			const jobIsolation = jobSettings.find((s) => s.key === 'job_isolation')
			const retentionPeriod = jobSettings.find((s) => s.key === 'retention_period_secs')
			const objectStorage = settings['Object Storage']?.find(
				(s) => s.key === 'object_store_cache_config'
			)
			return [
				...baseWithout,
				...(jobIsolation ? [jobIsolation] : []),
				...(licenseKey ? [licenseKey] : []),
				...(retentionPeriod ? [retentionPeriod] : []),
				...(objectStorage ? [objectStorage] : [])
			]
		}
		return base
	}

	function normalizeValue(value: any, key?: string): any {
		if (value == null) return undefined
		if (value === false) return undefined
		if (typeof value === 'string' && value.trim() === '') return undefined
		if (Array.isArray(value) && value.length === 0) return undefined
		if (typeof value === 'object' && !Array.isArray(value)) {
			// Recursively normalize: if all values in the object normalize to undefined,
			// the object itself is effectively empty (e.g. {smtp_tls_implicit: false} ≡ {})
			const hasNonEmpty = Object.values(value).some((v) => normalizeValue(v) !== undefined)
			if (!hasNonEmpty) return undefined
		}

		// Key-specific defaults: these values are equivalent to "not set"
		if (key === 'secret_backend') {
			if (
				typeof value === 'object' &&
				value?.type === 'Database' &&
				Object.keys(value).length === 1
			) {
				return undefined
			}
		}
		if (key === 'automate_username_creation' && value === true) {
			return undefined
		}
		if (key === 'critical_alerts_on_db_oversize' && typeof value === 'object') {
			if (!value.enabled && (!value.value || value.value === 0)) {
				return undefined
			}
		}

		return value
	}
	function buildCategoryYaml(
		category: string,
		vals: Record<string, any>,
		oauthsObj: Record<string, any>,
		reqPreexisting: boolean
	): string {
		const categorySettings = getSettingsForCategory(category)
		const obj: Record<string, any> = {}
		for (const s of categorySettings) {
			const normalized = normalizeValue(vals[s.key], s.key)
			if (normalized !== undefined) {
				obj[s.key] = vals[s.key]
			}
		}
		if (category === 'Auth/OAuth/SAML') {
			if (Object.keys(stripEmpty(oauthsObj)).length > 0) {
				obj['oauths'] = oauthsObj
			}
			if (reqPreexisting) {
				obj['require_preexisting_user_for_oauth'] = reqPreexisting
			}
		}
		if (category === 'Registries') {
			obj['workspace_registries'] = vals['workspace_registries'] ?? null
		}
		return YAML.stringify(obj)
	}

	let dirtyCategories: Record<string, boolean> = $derived.by(() => {
		const result: Record<string, boolean> = {}
		for (const category of settingsKeys) {
			const initialYaml = buildCategoryYaml(
				category,
				initialValues,
				initialOauths,
				initialRequirePreexistingUserForOauth
			)
			const currentYaml = buildCategoryYaml(
				category,
				$values,
				oauths,
				requirePreexistingUserForOauth
			)
			result[category] = initialYaml !== currentYaml
		}
		return result
	})

	let invalidCategories: Record<string, boolean> = $derived.by(() => {
		const currentValues = $values
		const result: Record<string, boolean> = {}
		for (const category of settingsKeys) {
			const categorySettings = getSettingsForCategory(category)
			result[category] = categorySettings.some((s) => {
				if (s.isValid && !s.isValid(currentValues?.[s.key])) return true
				if (s.validate) {
					const errors = s.validate(currentValues?.[s.key])
					return Object.keys(errors).length > 0
				}
				return false
			})
		}
		return result
	})

	$effect(() => {
		hasAnyInvalid = Object.values(invalidCategories).some(Boolean)
	})

	export function isDirty(category: string): boolean {
		return dirtyCategories[category] ?? false
	}

	export function discardCategory(category: string) {
		const categorySettings = getSettingsForCategory(category)
		for (const s of categorySettings) {
			const v = initialValues[s.key]
			$values[s.key] = v !== undefined ? JSON.parse(JSON.stringify(v)) : undefined
		}
		if (category === 'Auth/OAuth/SAML') {
			oauths = JSON.parse(JSON.stringify(initialOauths))
			requirePreexistingUserForOauth = initialRequirePreexistingUserForOauth
			loadInstanceInputs(initialOauths)
		} else if (category === 'Registries') {
			const v = initialValues['workspace_registries']
			$values['workspace_registries'] = v !== undefined ? JSON.parse(JSON.stringify(v)) : undefined
		}
	}

	export function discardAll() {
		// Reset all values to initial state (deep copy to avoid reference sharing)
		$values = JSON.parse(JSON.stringify(initialValues))
		oauths = JSON.parse(JSON.stringify(initialOauths))
		requirePreexistingUserForOauth = initialRequirePreexistingUserForOauth
		loadInstanceInputs(initialOauths)
		if (yamlMode) {
			syncFormToYaml()
		}
	}

	export async function saveCategorySettings(category: string) {
		// Category-specific pre-processing
		if (category === 'Auth/OAuth/SAML') {
			setupTemplatedOauthUrls()
		}

		if (category === 'Alerts' && $values?.critical_error_channels) {
			$values.critical_error_channels = $values.critical_error_channels.filter((entry: any) => {
				if (!entry || typeof entry !== 'object') return false
				if ('teams_channel' in entry) return isValidTeamsChannel(entry.teams_channel)
				if ('slack_channel' in entry)
					return typeof entry.slack_channel === 'string' && entry.slack_channel.trim() !== ''
				if ('email' in entry) return typeof entry.email === 'string' && entry.email.trim() !== ''
				return false
			})
		}

		if (
			category === 'Core' &&
			$values?.['license_key'] &&
			typeof $values['license_key'] === 'string'
		) {
			$values['license_key'] = $values['license_key'].trim()
		}

		let shouldReloadPage = false
		let willRestart = false
		const categorySettings = getSettingsForCategory(category)

		let licenseKeySet = false
		await Promise.all(
			categorySettings
				.filter((x) => {
					return (
						x.storage === 'setting' &&
						!deepEqual(initialValues?.[x.key], $values?.[x.key]) &&
						($values?.[x.key] !== '' ||
							initialValues?.[x.key] !== undefined ||
							initialValues?.[x.key] !== null)
					)
				})
				.map(async (x) => {
					if (x.key === 'license_key') licenseKeySet = true
					if (x.requiresReloadOnChange) shouldReloadPage = true
					if (x.triggersRestart) willRestart = true
					let value = $values?.[x.key]
					if (x.fieldType === 'codearea' && typeof value === 'string' && value.trim() === '') {
						value = undefined
					}
					return await SettingService.setGlobal({
						key: x.key,
						requestBody: { value }
					})
				})
		)

		// Update only the saved category's initial values
		for (const s of categorySettings) {
			const v = $values[s.key]
			initialValues[s.key] = v !== undefined ? JSON.parse(JSON.stringify(v)) : undefined
		}
		if (categorySettings.some((s) => s.key === 'base_url')) {
			baseUrlIsFallback = false
		}

		// Handle Auth/OAuth/SAML-specific saves
		if (category === 'Auth/OAuth/SAML') {
			if (!deepEqual(stripEmpty(initialOauths), stripEmpty(oauths))) {
				await SettingService.setGlobal({
					key: 'oauths',
					requestBody: { value: oauths }
				})
				initialOauths = JSON.parse(JSON.stringify(oauths))
			}
			if (initialRequirePreexistingUserForOauth !== requirePreexistingUserForOauth) {
				await SettingService.setGlobal({
					key: 'require_preexisting_user_for_oauth',
					requestBody: { value: requirePreexistingUserForOauth }
				})
				initialRequirePreexistingUserForOauth = requirePreexistingUserForOauth
			}
		}

		// Handle workspace_registries (saved separately like oauths)
		if (category === 'Registries') {
			if (!deepEqual(initialValues['workspace_registries'], $values['workspace_registries'])) {
				await SettingService.setGlobal({
					key: 'workspace_registries',
					requestBody: { value: $values['workspace_registries'] ?? null }
				})
				initialValues['workspace_registries'] = $values['workspace_registries']
					? JSON.parse(JSON.stringify($values['workspace_registries']))
					: undefined
			}
		}

		if (licenseKeySet) setLicense()

		if (shouldReloadPage) {
			sendUserToast('Settings updated, reloading page...')
			await sleep(1000)
			window.location.reload()
		} else if (willRestart) {
			sendUserToast(
				'Settings updated. Servers are restarting and changes may take up to a minute to fully propagate.',
				false,
				[],
				undefined,
				8000
			)
			dispatch('saved')
		} else {
			sendUserToast('Settings updated')
			dispatch('saved')
		}
	}

	let yamlCode = $state('')
	let yamlCodeInitial = $state('')
	let yamlEditor: any | undefined = $state(undefined)
	let yamlError = $state('')
	let showSensitive = $state(false)

	const SENSITIVE_UNCHANGED = '__SENSITIVE_AND_UNCHANGED__'

	const sensitiveKeys: Set<string> = new Set([
		...[...Object.values(settings), scimSamlSetting]
			.flatMap((s) => Object.values(s))
			.filter((s) => s.fieldType === 'password' || s.fieldType === 'license_key')
			.map((s) => s.key),
		'ducklake_user_pg_pwd',
		'jwt_secret',
		'workspace_registries'
	])

	// Settings that should never appear in YAML export/import.
	// `worker_configs` is a legacy ghost key: worker configs live in the `config`
	// table (managed from /workers), not in `global_settings`. Older DBs may
	// still carry a stale `global_settings.worker_configs` row; filter it here
	// so it never round-trips through this editor.
	const excludedKeys: Set<string> = new Set(['worker_configs'])

	// Nested fields inside object-valued settings that contain secrets.
	// Each entry maps a top-level key to its sensitive sub-field names.
	const nestedSensitiveFields: Record<string, string[]> = {
		smtp_settings: ['smtp_password'],
		secret_backend: ['token'],
		object_store_cache_config: ['secret_key', 'serviceAccountKey'],
		custom_instance_pg_databases: ['user_pwd'],
		rsa_keys: ['private_key'],
		github_enterprise_app: ['private_key']
	}

	/** Returns SENSITIVE_UNCHANGED if the value is non-empty and matches the initial */
	function maskField(current: any, initial: any): string | undefined {
		if (current != null && current !== '' && current === initial) return SENSITIVE_UNCHANGED
		return undefined
	}

	function maskSensitive(obj: Record<string, any>): Record<string, any> {
		const masked: Record<string, any> = {}
		for (const [key, value] of Object.entries(obj)) {
			if (key === 'oauths' && typeof value === 'object' && value !== null) {
				const maskedOauths: Record<string, any> = {}
				for (const [provider, config] of Object.entries(value as Record<string, any>)) {
					if (typeof config === 'object' && config !== null && 'secret' in config) {
						const m = maskField(config.secret, initialOauths?.[provider]?.secret)
						maskedOauths[provider] = m ? { ...config, secret: m } : config
					} else {
						maskedOauths[provider] = config
					}
				}
				masked[key] = maskedOauths
			} else if (key in nestedSensitiveFields && typeof value === 'object' && value !== null) {
				const cp = { ...value }
				const init = initialValues?.[key]
				for (const field of nestedSensitiveFields[key]) {
					const m = maskField(
						field === 'serviceAccountKey' ? JSON.stringify(cp[field]) : cp[field],
						field === 'serviceAccountKey' ? JSON.stringify(init?.[field]) : init?.[field]
					)
					if (m) cp[field] = m
				}
				masked[key] = cp
			} else if (sensitiveKeys.has(key) && value != null && value !== '') {
				masked[key] = value === initialValues?.[key] ? SENSITIVE_UNCHANGED : value
			} else {
				masked[key] = value
			}
		}
		return masked
	}

	/**
	 * Builds a sorted YAML string of all instance settings.
	 * - normalize: strip keys whose values match the default (empty/falsy or key-specific defaults)
	 * - mask: replace sensitive values with placeholder (for display)
	 */
	function buildSettingsYaml(
		vals: Record<string, any>,
		oauthsObj: Record<string, any>,
		reqPreexisting: boolean,
		opts: { normalize?: boolean; mask?: boolean } = {}
	): string {
		// Merge all settings (including oauths) into one object so they sort together
		const merged: Record<string, any> = { ...vals }
		if (oauthsObj && Object.keys(stripEmpty(oauthsObj)).length > 0) {
			merged['oauths'] = oauthsObj
		}
		if (reqPreexisting) {
			merged['require_preexisting_user_for_oauth'] = reqPreexisting
		}
		const obj: Record<string, any> = {}
		for (const key of Object.keys(merged).sort()) {
			if (excludedKeys.has(key)) continue
			if (opts.normalize && normalizeValue(merged[key], key) === undefined) continue
			obj[key] = merged[key]
		}
		// Strip runtime-only `databases` sub-field from custom_instance_pg_databases
		if (obj['custom_instance_pg_databases']?.databases) {
			obj['custom_instance_pg_databases'] = { ...obj['custom_instance_pg_databases'] }
			delete obj['custom_instance_pg_databases'].databases
		}
		return YAML.stringify(opts.mask ? maskSensitive(obj) : obj)
	}

	function syncFormToYaml() {
		yamlCode = buildSettingsYaml($values, oauths, requirePreexistingUserForOauth, {
			normalize: true,
			mask: !showSensitive
		})
		yamlCodeInitial = buildSettingsYaml(
			initialValues,
			initialOauths,
			initialRequirePreexistingUserForOauth,
			{ normalize: true, mask: !showSensitive }
		)
		yamlEditor?.setCode(yamlCode)
		yamlError = ''
	}

	function syncYamlToForm(): boolean {
		try {
			// Flush the editor's current content (bypasses the 200ms debounce in SimpleEditor)
			const currentCode = yamlEditor?.getCode() ?? yamlCode
			if (currentCode !== yamlCode) {
				yamlCode = currentCode
			}
			const parsed = YAML.parse(yamlCode)
			if (typeof parsed !== 'object' || parsed === null) {
				sendUserToast('YAML must be a mapping (key: value)', true)
				return false
			}

			// Restore sensitive values that were not changed (placeholder → original value)
			if ('oauths' in parsed && typeof parsed['oauths'] === 'object') {
				for (const [provider, config] of Object.entries(parsed['oauths'] as Record<string, any>)) {
					if (
						typeof config === 'object' &&
						config !== null &&
						config.secret === SENSITIVE_UNCHANGED
					) {
						config.secret = initialOauths?.[provider]?.secret
					}
				}
				oauths = parsed['oauths'] ?? {}
				delete parsed['oauths']
			}
			if ('require_preexisting_user_for_oauth' in parsed) {
				requirePreexistingUserForOauth = parsed['require_preexisting_user_for_oauth'] ?? false
				delete parsed['require_preexisting_user_for_oauth']
			}

			// Restore unchanged sensitive settings (placeholder → original value)
			for (const key of sensitiveKeys) {
				if (key in parsed && parsed[key] === SENSITIVE_UNCHANGED) {
					parsed[key] = initialValues?.[key]
				}
			}
			// Restore nested sensitive fields
			for (const [parentKey, fields] of Object.entries(nestedSensitiveFields)) {
				if (parsed[parentKey] && typeof parsed[parentKey] === 'object') {
					const init = initialValues?.[parentKey]
					for (const field of fields) {
						if (parsed[parentKey][field] === SENSITIVE_UNCHANGED) {
							parsed[parentKey][field] = init?.[field]
						}
					}
				}
			}

			// Preserve excluded keys from current form state
			for (const key of excludedKeys) {
				if (key in $values) {
					parsed[key] = $values[key]
				}
			}

			// Preserve runtime-only `databases` sub-field in custom_instance_pg_databases
			const existingDatabases = initialValues?.['custom_instance_pg_databases']?.databases
			if (existingDatabases && parsed['custom_instance_pg_databases']) {
				parsed['custom_instance_pg_databases'].databases = existingDatabases
			}

			$values = parsed
			applyFormDefaults($values)

			yamlError = ''
			return true
		} catch (e) {
			yamlError = String(e)
			sendUserToast('Invalid YAML: ' + e, true)
			return false
		}
	}

	let prevYamlMode = false
	let prevLoading = true
	$effect(() => {
		if (yamlMode && !prevYamlMode) {
			syncFormToYaml()
		} else if (!yamlMode && prevYamlMode) {
			if (!syncYamlToForm()) {
				// Reset toggle back to YAML on parse failure
				yamlMode = true
			}
		} else if (yamlMode && prevLoading && !loading) {
			// Settings just finished loading while in YAML mode
			syncFormToYaml()
		}
		prevYamlMode = yamlMode
		prevLoading = loading
	})

	function handleShowSensitiveToggle(checked: boolean) {
		// Sync any in-progress edits back to form state before re-rendering
		syncYamlToForm()
		showSensitive = checked
		syncFormToYaml()
	}

	/** Call before entering diff mode to sync YAML edits into form state */
	export function syncBeforeDiff(): boolean {
		if (yamlMode) {
			return syncYamlToForm()
		}
		return true
	}

	export function buildFullDiff(): { original: string; modified: string } {
		return {
			original: buildSettingsYaml(
				initialValues,
				initialOauths,
				initialRequirePreexistingUserForOauth,
				{ normalize: true }
			),
			modified: buildSettingsYaml($values, oauths, requirePreexistingUserForOauth, {
				normalize: true
			})
		}
	}

	$effect(() => {
		if (yamlMode) {
			// In YAML mode, compare editor content against snapshot taken on entry
			hasUnsavedChanges = yamlCodeInitial !== '' && yamlCode !== yamlCodeInitial
		} else {
			// Reuse per-category dirty tracking instead of rebuilding full YAML
			hasUnsavedChanges = Object.values(dirtyCategories).some(Boolean)
		}
	})
</script>

<div class="pb-12">
	{#if yamlMode}
		<div class="flex flex-row justify-between">
			<p class="text-2xs text-tertiary">
				Use this YAML to manage instance settings as code.
				<a
					href="https://www.windmill.dev/docs/advanced/instance_settings#kubernetes-operator"
					target="_blank"
					rel="noopener noreferrer">Learn more <ExternalLink size={12} class="inline-block" /></a
				>
			</p>
			<!-- svelte-ignore a11y_label_has_associated_control -->
			<div class="flex items-center justify-end gap-4 mb-2">
				<Toggle
					checked={showSensitive}
					on:change={(e) => handleShowSensitiveToggle(e.detail)}
					options={{ right: 'Show sensitive values' }}
					size="xs"
				/>
			</div>
		</div>
		<div class="border rounded w-full h-[calc(100vh-12rem)]">
			{#await import('$lib/components/SimpleEditor.svelte')}
				<Loader2 class="animate-spin" />
			{:then Module}
				<Module.default
					bind:this={yamlEditor}
					class="h-full"
					lang="yaml"
					bind:code={yamlCode}
					fixedOverflowWidgets={false}
				/>
			{/await}
		</div>
		{#if yamlError}
			<div class="text-red-500 text-xs mt-1">{yamlError}</div>
		{/if}
	{:else if hideTabs}
		{@render categoryContent(tab)}
	{:else}
		<Tabs bind:selected={tab}>
			{#each settingsKeys as category}
				<Tab value={category} label={category}></Tab>
			{/each}

			{#snippet content()}
				<div class="pt-4"></div>
				{#each Object.keys(settings) as category}
					<TabContent value={category}>
						{@render categoryContent(category)}
					</TabContent>
				{/each}
			{/snippet}
		</Tabs>
	{/if}

	{#snippet categoryContent(category: string)}
		{#if category == 'Core'}
			<SettingsPageHeader
				title="Core"
				description="Configure the core settings of your Windmill instance."
				link="https://www.windmill.dev/docs/advanced/instance_settings"
			/>
		{:else if category == 'SMTP'}
			<SettingsPageHeader
				title="SMTP"
				description="Setting SMTP unlocks sending emails upon adding new users to the workspace or the instance or sending critical alerts via email."
				link="https://www.windmill.dev/docs/advanced/instance_settings#smtp"
			/>
		{:else if category == 'Registries'}
			<SettingsPageHeader
				title="Registries"
				description="Add private registries for Pip, Bun and npm."
				link="https://www.windmill.dev/docs/advanced/imports"
			/>
			{#if !$enterpriseLicense}
				<Alert type="info" title="Private registries configuration is an EE feature" class="mb-2" />
			{/if}
		{:else if category == 'Alerts'}
			<SettingsPageHeader
				title="Alerts"
				description="Critical alerts automatically notify administrators about system events like job crashes, license issues, worker failures, and queue delays through email, Slack, or Teams."
				link="https://www.windmill.dev/docs/core_concepts/critical_alerts"
			/>
		{:else if category == 'OTEL/Prom'}
			<SettingsPageHeader
				title="OTEL/Prometheus"
				description="Configure OpenTelemetry and Prometheus metrics export for monitoring your Windmill instance."
				link="https://www.windmill.dev/docs/misc/guides/otel"
			/>
		{:else if category == 'Indexer'}
			<SettingsPageHeader
				title="Indexer"
				description="The indexer service unlocks full text search across jobs and service logs. It requires spinning up its own separate container."
				link="https://www.windmill.dev/docs/core_concepts/search_bar#setup"
			/>
			{#if !$enterpriseLicense}
				<Alert
					type="info"
					title="Full text search across jobs and service logs is an EE feature"
					class="mb-2"
				/>
			{/if}
		{:else if category == 'Telemetry'}
			<SettingsPageHeader title="Telemetry" />
			{#if $enterpriseLicense}
				<div class="text-primary pb-4 text-xs">
					Telemetry is required on Enterprise Edition for license compliance. When minimal telemetry
					is enabled, only the following data is sent:
					<ul class="list-disc list-inside pl-2">
						<li>version of your instance</li>
						<li>instance base URL</li>
						<li>login type usage (login type, count)</li>
						<li>worker usage (worker, worker instance, vCPUs, memory)</li>
						<li>user usage (author count, operator count)</li>
						<li>superadmin email addresses</li>
						<li>development instance status</li>
					</ul>
					<br />When minimal telemetry is disabled, the following is also collected:
					<ul class="list-disc list-inside pl-2">
						<li>job usage (language, total duration, count)</li>
						<li>git sync repo count (sync vs promotion mode)</li>
						<li
							>AI chat usage (provider, model, mode, session count, message count — last 30 days)</li
						>
						<li
							>resource counts (workspaces, scripts per language, flows, workflows as code, low-code
							apps, raw apps)</li
						>
						<li
							>infrastructure info (container runtime, managed database provider, database version,
							size and cluster size, max and active connections, object storage backend)</li
						>
					</ul>
					<br />For air-gapped instances, you can download the telemetry data and send it manually.
				</div>
				<div class="flex gap-2 mb-4">
					<Button
						on:click={sendStats}
						variant="default"
						btnClasses="w-auto"
						loading={sendingStats}
						size="xs"
					>
						Send usage
					</Button>
					<Button
						on:click={downloadStats}
						variant="default"
						btnClasses="w-auto"
						loading={downloadingStats}
						size="xs"
					>
						Download usage
					</Button>
				</div>
			{:else}
				<div class="text-primary pb-4 text-xs">
					Anonymous usage data is collected to help improve Windmill.
					<br />The following information is collected:
					<ul class="list-disc list-inside pl-2">
						<li>version of your instance</li>
						<li>instance base URL</li>
						<li>job usage (language, total duration, count)</li>
						<li>login type usage (login type, count)</li>
						<li>worker usage (worker, worker instance, vCPUs, memory)</li>
						<li>user usage (author count, operator count)</li>
						<li>development instance status</li>
						<li
							>AI chat usage (provider, model, mode, session count, message count — last 30 days)</li
						>
						<li
							>resource counts (workspaces, scripts per language, flows, workflows as code, low-code
							apps, raw apps)</li
						>
					</ul>
				</div>
			{/if}
		{:else if category == 'Jobs'}
			<SettingsPageHeader
				title="Jobs"
				description="Configure default timeouts and retention policies for job execution."
				link="https://www.windmill.dev/docs/advanced/instance_settings#jobs"
			/>
		{:else if category == 'Object Storage'}
			<SettingsPageHeader
				title="Object Storage"
				description="Configure S3-compatible storage for large logs and distributed dependency caching."
				link="https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill"
			/>
		{:else if category == 'Private Hub'}
			<SettingsPageHeader
				title="Private Hub"
				description="Connect to a Private Hub instance for sharing custom scripts and integrations."
				link="https://www.windmill.dev/docs/core_concepts/private_hub"
			/>
		{:else if category == 'Secret Storage'}
			<SettingsPageHeader
				title="Secret Storage"
				description="Configure where secrets (secret variables) are stored."
				link="https://www.windmill.dev/docs/core_concepts/workspace_secret_encryption"
			/>
		{:else if category == 'GitHub Enterprise App'}
			<SettingsPageHeader
				title="GitHub Enterprise App"
				description="Configure a self-managed GitHub App for GitHub Enterprise Server git sync."
			/>
		{:else if category == 'DB Health'}
			<SettingsPageHeader
				title="DB Health"
				description="On-demand database diagnostics. Analyze table sizes, job retention, connection pool health, vacuum status, and more."
			/>
			<DbHealth />
		{:else if category == 'Auth/OAuth/SAML'}
			<AuthSettings
				bind:oauths
				bind:instanceInputs
				bind:requirePreexistingUserForOauth
				baseUrl={$values?.base_url}
				bind:tab={authSubTab}
				{hideTabs}
			>
				{#snippet scim()}
					<div class="flex-col flex gap-6 pb-4">
						{#each scimSamlSetting as setting}
							<InstanceSetting
								on:closeDrawer={() => closeDrawer?.()}
								{loading}
								{setting}
								{values}
								{version}
								{oauths}
							/>
							{#if setting.key === 'scim_token'}
								<ScimOauthSettings />
							{/if}
						{/each}
					</div>
				{/snippet}
			</AuthSettings>
		{/if}

		<div class="flex-col flex gap-6 pb-6">
			{#each settings[category] as setting}
				<!-- slack connect is handled with the alert channels settings, smtp_connect is handled in InstanceSetting -->
				{#if setting.fieldType != 'slack_connect' && !(quickSetup && setting.hideInQuickSetup) && !(quickSetup && category === 'Core' && setting.key === 'license_key')}
					<InstanceSetting
						{openSmtpSettings}
						on:closeDrawer={() => closeDrawer?.()}
						{loading}
						{setting}
						{values}
						{version}
						{oauths}
						warning={setting.key === 'base_url' && baseUrlIsFallback
							? 'Auto-detected from browser — not yet saved'
							: setting.key === 'nuget_config' &&
								  $values['nuget_config'] &&
								  !/<clear\s*\/>/.test($values['nuget_config'])
								? 'Missing <clear /> in <packageSources>. Without it, default sources (like nuget.org) are merged with your custom sources, which is likely not what you want.'
								: undefined}
					/>
				{/if}
				{#if quickSetup && category === 'Core' && setting.key === 'base_url'}
					{@const licenseKeySetting = settings['Core'].find((s) => s.key === 'license_key')}
					{#if licenseKeySetting}
						<InstanceSetting
							{openSmtpSettings}
							on:closeDrawer={() => closeDrawer?.()}
							{loading}
							setting={licenseKeySetting}
							{values}
							{version}
							{oauths}
						/>
					{/if}
				{/if}
			{/each}
			{#if quickSetup && category === 'Core'}
				{@const extraSettings = [
					...settings['Jobs'].filter((s) => s.key === 'job_isolation'),
					...settings['Jobs'].filter((s) => s.key === 'retention_period_secs'),
					...(settings['Object Storage']?.filter((s) => s.key === 'object_store_cache_config') ??
						[])
				]}
				{#each extraSettings as setting}
					<InstanceSetting
						{openSmtpSettings}
						on:closeDrawer={() => closeDrawer?.()}
						{loading}
						{setting}
						{values}
						{version}
						{oauths}
					/>
				{/each}
			{/if}
		</div>

		{#if category === 'Registries'}
			<WorkspaceRegistries {values} {loading} />
		{/if}

		{#if !loading && !quickSetup && !hideTabs}
			<SettingsFooter
				hasUnsavedChanges={dirtyCategories[category] ?? false}
				disabled={invalidCategories[category] ?? false}
				onSave={() => saveCategorySettings(category)}
				onDiscard={() => discardCategory(category)}
				saveLabel={`Save ${category.toLowerCase()} settings`}
				class="bg-surface"
			/>
		{/if}
	{/snippet}
</div>
