<script lang="ts">
	import { base } from '$app/paths';
	import { goto } from '$app/navigation';
	import { page } from '$app/state';
	import {
		api,
		ApiError,
		type AdminDto,
		type App,
		type AppDomain,
		type AppMemberDto,
		type AppRole,
		type Script,
		type Trigger,
		type Topic,
		type TopicAuthMode,
		type SecretListItem
	} from '$lib/api';
	import CodeEditor from '$lib/CodeEditor.svelte';
	import ConfirmModal from '$lib/ConfirmModal.svelte';
	import ActionMenu from '$lib/ActionMenu.svelte';
	import RoleChip from '$lib/RoleChip.svelte';
	import { currentUser } from '$lib/auth';
	import { canAdminApp, canWriteApp } from '$lib/capabilities';

	const me = $derived($currentUser);

	const SAMPLE_SOURCE =
		'#{\n  statusCode: 200,\n  body: #{ ok: true, echo: ctx.request.body }\n}';

	type Tab = 'scripts' | 'domains' | 'members' | 'settings' | 'triggers' | 'topics' | 'secrets';

	// Common IANA timezones offered in the cron form dropdown. Not
	// exhaustive — the backend validates any IANA name via chrono-tz.
	const COMMON_TIMEZONES = [
		'UTC',
		'America/Los_Angeles',
		'America/Denver',
		'America/Chicago',
		'America/New_York',
		'America/Sao_Paulo',
		'Europe/London',
		'Europe/Berlin',
		'Europe/Paris',
		'Europe/Moscow',
		'Asia/Kolkata',
		'Asia/Shanghai',
		'Asia/Tokyo',
		'Australia/Sydney'
	];

	let slug = $derived(page.params.slug ?? '');
	let app = $state<App | null>(null);
	let myRole = $state<AppRole | null>(null);
	let loadError = $state<string | null>(null);
	let loading = $state(true);
	/// F-U-014: the canonical public origin (from `/version`). Used to
	/// build webhook URLs the operator shares with external systems so
	/// they don't accidentally inherit the admin's LAN address.
	let publicBaseUrl = $state<string | null>(null);
	// Tab is URL-driven so the layout's <AppTabBar> can link directly to
	// `?tab=<id>` and the in-page panel switches accordingly. The legacy
	// state-based switching was incompatible with cross-route tab
	// continuity (the user complained that subroute pages dropped the
	// tab bar — now the layout owns it everywhere).
	const isValidTab = (s: string | null): s is Tab =>
		s === 'scripts' ||
		s === 'domains' ||
		s === 'members' ||
		s === 'settings' ||
		s === 'triggers' ||
		s === 'topics' ||
		s === 'secrets';
	let activeTab = $derived.by<Tab>(() => {
		const qp = page.url.searchParams.get('tab');
		return isValidTab(qp) ? qp : 'scripts';
	});

	let scripts = $state<Script[]>([]);
	let domains = $state<AppDomain[]>([]);
	let members = $state<AppMemberDto[]>([]);

	/// v1.1.1 dead-letters surface — design notes §4 mandates the
	/// dashboard surface this since there's no default handler.
	let unresolvedDeadLetters = $state<number>(0);
	async function loadDeadLetterCount(idOrSlug: string) {
		try {
			const r = await api.deadLetters.count(idOrSlug);
			unresolvedDeadLetters = r.unresolved;
		} catch {
			// Non-fatal: the page renders fine without the badge if
			// the count endpoint is unreachable (e.g. older server).
			unresolvedDeadLetters = 0;
		}
	}

	// Derive UI gates from the capabilities helper so the rules stay
	// in lockstep with the backend's `can()`. canAdminApp also covers
	// the Members + Settings + Domains-mutation tabs; canWriteApp
	// covers New script.
	const canWrite = $derived(canWriteApp(me, myRole));
	const canAdmin = $derived(canAdminApp(me, myRole));

	// Script create
	let showCreateScript = $state(false);
	let createScriptName = $state('');
	let createScriptDescription = $state('');
	let createScriptSource = $state(SAMPLE_SOURCE);
	// v1.1.3: endpoint (default — handles routes/triggers) vs module
	// (library imported by other scripts). Modules cannot be bound to
	// routes or used as trigger targets.
	let createScriptKind = $state<'endpoint' | 'module'>('endpoint');
	let creatingScript = $state(false);
	let createScriptError = $state<string | null>(null);

	// Domain create
	let createDomainPattern = $state('');
	let creatingDomain = $state(false);
	let createDomainError = $state<string | null>(null);

	// Settings
	let editName = $state('');
	let editDescription = $state('');
	let editSlug = $state('');
	let savingSettings = $state(false);
	let settingsError = $state<string | null>(null);
	let slugTakeoverNeeded = $state<App | null>(null);

	// Delete confirmations
	let confirmingDeleteApp = $state(false);
	let deletingApp = $state(false);
	let deleteAppError = $state<string | null>(null);
	let domainToRemove = $state<AppDomain | null>(null);
	let removingDomain = $state(false);
	let removeDomainError = $state<string | null>(null);

	// Triggers tab (v1.1.4 — cron triggers). Admin-gated, like Members.
	let triggers = $state<Trigger[]>([]);
	let createCronScriptId = $state('');
	let createCronSchedule = $state('0 0 9 * * MON-FRI');
	let createCronTimezone = $state('UTC');
	let creatingCron = $state(false);
	let createCronError = $state<string | null>(null);
	// Pub/Sub triggers (v1.1.5).
	let createPubsubScriptId = $state('');
	let createPubsubTopic = $state('');
	let creatingPubsub = $state(false);
	let createPubsubError = $state<string | null>(null);
	// Email triggers (v1.1.7).
	let createEmailScriptId = $state('');
	let createEmailSecret = $state('');
	let creatingEmail = $state(false);
	let createEmailError = $state<string | null>(null);
	// Queue triggers (v1.1.9).
	let createQueueScriptId = $state('');
	let createQueueName = $state('');
	let createQueueVisibilityTimeout = $state(30);
	let createQueueMaxAttempts = $state(3);
	let creatingQueue = $state(false);
	let createQueueError = $state<string | null>(null);
	// F-U-001: KV / Docs / Files / Dead-letter trigger forms.
	let createKvScriptId = $state('');
	let createKvCollectionGlob = $state('*');
	let createKvOps = $state<{ insert: boolean; update: boolean; delete: boolean }>({
		insert: true,
		update: true,
		delete: true
	});
	let creatingKv = $state(false);
	let createKvError = $state<string | null>(null);
	let createDocsScriptId = $state('');
	let createDocsCollectionGlob = $state('*');
	let createDocsOps = $state<{ create: boolean; update: boolean; delete: boolean }>({
		create: true,
		update: true,
		delete: true
	});
	let creatingDocs = $state(false);
	let createDocsError = $state<string | null>(null);
	let createFilesScriptId = $state('');
	let createFilesCollectionGlob = $state('*');
	let createFilesOps = $state<{ create: boolean; update: boolean; delete: boolean }>({
		create: true,
		update: true,
		delete: true
	});
	let creatingFiles = $state(false);
	let createFilesError = $state<string | null>(null);
	let createDeadLetterScriptId = $state('');
	let createDeadLetterSourceFilter = $state('');
	let createDeadLetterTriggerIdFilter = $state('');
	let createDeadLetterScriptIdFilter = $state('');
	let creatingDeadLetter = $state(false);
	let createDeadLetterError = $state<string | null>(null);
	let triggerToRemove = $state<Trigger | null>(null);
	let removingTrigger = $state(false);
	// Endpoint scripts only — modules can't be trigger targets.
	const endpointScripts = $derived(scripts.filter((s) => s.kind === 'endpoint'));

	async function loadTriggers(idOrSlug: string) {
		try {
			const r = await api.triggers.list(idOrSlug);
			triggers = r.triggers;
		} catch {
			triggers = [];
		}
	}

	async function submitCreateCron(e: SubmitEvent) {
		e.preventDefault();
		if (!app) return;
		creatingCron = true;
		createCronError = null;
		try {
			await api.triggers.createCron(app.id, {
				script_id: createCronScriptId,
				schedule: createCronSchedule.trim(),
				timezone: createCronTimezone
			});
			createCronScriptId = '';
			await loadTriggers(app.id);
		} catch (err) {
			createCronError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			creatingCron = false;
		}
	}

	async function submitCreatePubsub(e: SubmitEvent) {
		e.preventDefault();
		if (!app) return;
		creatingPubsub = true;
		createPubsubError = null;
		try {
			await api.triggers.createPubsub(app.id, {
				script_id: createPubsubScriptId,
				topic_pattern: createPubsubTopic.trim()
			});
			createPubsubScriptId = '';
			createPubsubTopic = '';
			await loadTriggers(app.id);
		} catch (err) {
			createPubsubError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			creatingPubsub = false;
		}
	}

	async function submitCreateEmail(e: SubmitEvent) {
		e.preventDefault();
		if (!app) return;
		creatingEmail = true;
		createEmailError = null;
		try {
			await api.triggers.createEmail(app.id, {
				script_id: createEmailScriptId,
				inbound_secret: createEmailSecret.trim() === '' ? null : createEmailSecret
			});
			createEmailScriptId = '';
			createEmailSecret = '';
			await loadTriggers(app.id);
		} catch (err) {
			createEmailError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			creatingEmail = false;
		}
	}

	// F-U-001: handlers for kv / docs / files / dead-letter trigger creation.
	function selectedOps<T extends Record<string, boolean>>(ops: T): Array<keyof T> {
		return (Object.keys(ops) as Array<keyof T>).filter((k) => ops[k]);
	}

	async function submitCreateKv(e: SubmitEvent) {
		e.preventDefault();
		if (!app) return;
		creatingKv = true;
		createKvError = null;
		try {
			await api.triggers.createKv(app.id, {
				script_id: createKvScriptId,
				collection_glob: createKvCollectionGlob.trim(),
				ops: selectedOps(createKvOps) as Array<'insert' | 'update' | 'delete'>
			});
			createKvScriptId = '';
			createKvCollectionGlob = '*';
			await loadTriggers(app.id);
		} catch (err) {
			createKvError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			creatingKv = false;
		}
	}

	async function submitCreateDocs(e: SubmitEvent) {
		e.preventDefault();
		if (!app) return;
		creatingDocs = true;
		createDocsError = null;
		try {
			await api.triggers.createDocs(app.id, {
				script_id: createDocsScriptId,
				collection_glob: createDocsCollectionGlob.trim(),
				ops: selectedOps(createDocsOps) as Array<'create' | 'update' | 'delete'>
			});
			createDocsScriptId = '';
			createDocsCollectionGlob = '*';
			await loadTriggers(app.id);
		} catch (err) {
			createDocsError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			creatingDocs = false;
		}
	}

	async function submitCreateFiles(e: SubmitEvent) {
		e.preventDefault();
		if (!app) return;
		creatingFiles = true;
		createFilesError = null;
		try {
			await api.triggers.createFiles(app.id, {
				script_id: createFilesScriptId,
				collection_glob: createFilesCollectionGlob.trim(),
				ops: selectedOps(createFilesOps) as Array<'create' | 'update' | 'delete'>
			});
			createFilesScriptId = '';
			createFilesCollectionGlob = '*';
			await loadTriggers(app.id);
		} catch (err) {
			createFilesError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			creatingFiles = false;
		}
	}

	async function submitCreateDeadLetter(e: SubmitEvent) {
		e.preventDefault();
		if (!app) return;
		creatingDeadLetter = true;
		createDeadLetterError = null;
		try {
			await api.triggers.createDeadLetter(app.id, {
				script_id: createDeadLetterScriptId,
				source_filter:
					createDeadLetterSourceFilter.trim() === '' ? null : createDeadLetterSourceFilter.trim(),
				trigger_id_filter:
					createDeadLetterTriggerIdFilter.trim() === ''
						? null
						: createDeadLetterTriggerIdFilter.trim(),
				script_id_filter:
					createDeadLetterScriptIdFilter.trim() === ''
						? null
						: createDeadLetterScriptIdFilter.trim()
			});
			createDeadLetterScriptId = '';
			createDeadLetterSourceFilter = '';
			createDeadLetterTriggerIdFilter = '';
			createDeadLetterScriptIdFilter = '';
			await loadTriggers(app.id);
		} catch (err) {
			createDeadLetterError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			creatingDeadLetter = false;
		}
	}

	async function submitCreateQueue(e: SubmitEvent) {
		e.preventDefault();
		if (!app) return;
		creatingQueue = true;
		createQueueError = null;
		try {
			await api.triggers.createQueue(app.id, {
				script_id: createQueueScriptId,
				queue_name: createQueueName.trim(),
				visibility_timeout_secs: createQueueVisibilityTimeout,
				retry_max_attempts: createQueueMaxAttempts
			});
			createQueueScriptId = '';
			createQueueName = '';
			createQueueVisibilityTimeout = 30;
			createQueueMaxAttempts = 3;
			await loadTriggers(app.id);
		} catch (err) {
			createQueueError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			creatingQueue = false;
		}
	}

	// The inbound-email webhook URL for a given email trigger (shown so
	// the operator can configure their provider).
	// F-U-014: prefer the server-known public_base_url (from /version)
	// so the URL we hand the operator works from outside the admin's
	// network. Fall back to window.location.origin only if /version
	// hasn't loaded yet (visibly distinguishable in the UI).
	function emailInboundUrl(triggerId: string): string {
		if (!app) return '';
		const origin = publicBaseUrl ?? window.location.origin;
		return `${origin.replace(/\/$/, '')}/api/v1/email-inbound/${app.id}/${triggerId}`;
	}

	async function confirmRemoveTrigger() {
		if (!app || !triggerToRemove) return;
		removingTrigger = true;
		try {
			await api.triggers.remove(app.id, triggerToRemove.id);
			triggerToRemove = null;
			await loadTriggers(app.id);
		} catch (err) {
			createCronError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			removingTrigger = false;
		}
	}

	// Topics tab (v1.1.6 — externally-subscribable realtime topics). Admin-gated.
	let topics = $state<Topic[]>([]);
	let createTopicName = $state('');
	let createTopicExternal = $state(false);
	let createTopicAuthMode = $state<TopicAuthMode>('public');
	let creatingTopic = $state(false);
	let createTopicError = $state<string | null>(null);
	// Edit modal.
	let topicToEdit = $state<Topic | null>(null);
	let editTopicExternal = $state(false);
	let editTopicAuthMode = $state<TopicAuthMode>('public');
	let savingTopic = $state(false);
	let editTopicError = $state<string | null>(null);
	// F-S-014: warn whenever the resulting (external, auth_mode) pair is
	// strictly more permissive than the current one — not just on the
	// internal → external flip. The order least → most permissive is:
	//   internal (any auth)  <  external+session  <  external+token  <  external+public
	const editPermissivenessChange = $derived.by(() => {
		if (!topicToEdit) return false;
		const rank = (external: boolean, mode: TopicAuthMode) => {
			if (!external) return 0;
			if (mode === 'session') return 1;
			if (mode === 'token') return 2;
			return 3; // public
		};
		const before = rank(topicToEdit.external_subscribable, topicToEdit.auth_mode);
		const after = rank(editTopicExternal, editTopicAuthMode);
		return after > before;
	});
	// Kept for narrow callers; semantics now covered by editPermissivenessChange.
	const editFlipToExternal = $derived(editPermissivenessChange);
	// Delete confirm.
	let topicToRemove = $state<Topic | null>(null);
	let removingTopic = $state(false);

	async function loadTopics(idOrSlug: string) {
		try {
			const r = await api.topics.list(idOrSlug);
			topics = r.topics;
		} catch {
			topics = [];
		}
	}

	async function submitCreateTopic(e: SubmitEvent) {
		e.preventDefault();
		if (!app) return;
		creatingTopic = true;
		createTopicError = null;
		try {
			await api.topics.create(app.id, {
				name: createTopicName.trim(),
				external_subscribable: createTopicExternal,
				auth_mode: createTopicAuthMode
			});
			createTopicName = '';
			createTopicExternal = false;
			createTopicAuthMode = 'public';
			await loadTopics(app.id);
		} catch (err) {
			createTopicError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			creatingTopic = false;
		}
	}

	function openEditTopic(t: Topic) {
		topicToEdit = t;
		editTopicExternal = t.external_subscribable;
		editTopicAuthMode = t.auth_mode;
		editTopicError = null;
	}

	async function confirmEditTopic() {
		if (!app || !topicToEdit) return;
		savingTopic = true;
		editTopicError = null;
		try {
			await api.topics.update(app.id, topicToEdit.name, {
				external_subscribable: editTopicExternal,
				auth_mode: editTopicAuthMode
			});
			topicToEdit = null;
			await loadTopics(app.id);
		} catch (err) {
			editTopicError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			savingTopic = false;
		}
	}

	async function confirmRemoveTopic() {
		if (!app || !topicToRemove) return;
		removingTopic = true;
		try {
			await api.topics.remove(app.id, topicToRemove.name);
			topicToRemove = null;
			await loadTopics(app.id);
		} catch (err) {
			createTopicError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			removingTopic = false;
		}
	}

	// Secrets tab (v1.1.7). The dashboard only ever sees names +
	// last-modified — values never leave the server. The create form's
	// value input is masked by default; revealing it requires a confirm.
	let secrets = $state<SecretListItem[]>([]);
	let createSecretName = $state('');
	let createSecretValue = $state('');
	let showSecretValue = $state(false);
	let revealConfirm = $state(false);
	let creatingSecret = $state(false);
	let createSecretError = $state<string | null>(null);
	let secretToRemove = $state<SecretListItem | null>(null);
	let removingSecret = $state(false);
	// True when the name already exists — set is an overwrite.
	const secretNameExists = $derived(
		secrets.some((s) => s.name === createSecretName.trim())
	);

	async function loadSecrets(idOrSlug: string) {
		try {
			const r = await api.secrets.list(idOrSlug);
			secrets = r.secrets;
		} catch {
			secrets = [];
		}
	}

	function toggleShowSecretValue(e: Event) {
		const target = e.currentTarget as HTMLInputElement;
		if (target.checked) {
			// Revealing a secret on screen is sensitive — gate behind a
			// confirm. Revert the checkbox until the user confirms.
			target.checked = false;
			revealConfirm = true;
		} else {
			showSecretValue = false;
		}
	}

	function confirmRevealSecret() {
		showSecretValue = true;
		revealConfirm = false;
	}

	async function submitCreateSecret(e: SubmitEvent) {
		e.preventDefault();
		if (!app) return;
		creatingSecret = true;
		createSecretError = null;
		try {
			await api.secrets.set(app.id, createSecretName.trim(), createSecretValue);
			createSecretName = '';
			createSecretValue = '';
			showSecretValue = false;
			await loadSecrets(app.id);
		} catch (err) {
			createSecretError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			creatingSecret = false;
		}
	}

	async function confirmRemoveSecret() {
		if (!app || !secretToRemove) return;
		removingSecret = true;
		try {
			await api.secrets.remove(app.id, secretToRemove.name);
			secretToRemove = null;
			await loadSecrets(app.id);
		} catch (err) {
			createSecretError =
				err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err);
		} finally {
			removingSecret = false;
		}
	}

	// Members tab
	let eligibleUsers = $state<AdminDto[]>([]);
	let eligibleLoadError = $state<string | null>(null);
	let addMemberUserId = $state('');
	let addMemberRole = $state<AppRole>('viewer');
	let addingMember = $state(false);
	let addMemberError = $state<string | null>(null);
	let memberToRemove = $state<AppMemberDto | null>(null);
	let removingMember = $state(false);
	let removeMemberError = $state<string | null>(null);
	let roleChangeBusy = $state<string | null>(null);
	let memberActionError = $state<string | null>(null);

	async function loadApp() {
		loading = true;
		loadError = null;
		try {
			// F-U-014: fire the /version request alongside the app fetch so
			// emailInboundUrl can prefer the server-known public_base_url.
			api
				.version()
				.then((v) => (publicBaseUrl = v.public_base_url || null))
				.catch(() => (publicBaseUrl = null));
			const fetched = await api.apps.get(slug);
			if (fetched.redirect_to && fetched.redirect_to !== slug) {
				await goto(`${base}/apps/${fetched.redirect_to}`, { replaceState: true });
				return;
			}
			app = {
				id: fetched.id,
				slug: fetched.slug,
				name: fetched.name,
				description: fetched.description,
				created_at: fetched.created_at,
				updated_at: fetched.updated_at
			};
			myRole = fetched.my_role;
			editName = app.name;
			editDescription = app.description ?? '';
			editSlug = app.slug;
			const loaders: Promise<unknown>[] = [
				loadScripts(app.id),
				loadDomains(app.id),
				loadDeadLetterCount(app.id)
			];
			if (canAdmin) {
				loaders.push(
					loadMembers(app.id),
					loadEligibleUsers(),
					loadTriggers(app.id),
					loadTopics(app.id),
					loadSecrets(app.id)
				);
			}
			await Promise.all(loaders);
		} catch (e) {
			loadError = e instanceof Error ? e.message : String(e);
		} finally {
			loading = false;
		}
	}

	async function loadScripts(appId: string) {
		try {
			scripts = await api.scripts.list({ app: appId });
		} catch (e) {
			scripts = [];
			loadError = e instanceof Error ? e.message : String(e);
		}
	}

	async function loadDomains(appId: string) {
		try {
			domains = await api.domains.listForApp(appId);
		} catch (e) {
			domains = [];
			loadError = e instanceof Error ? e.message : String(e);
		}
	}

	async function loadMembers(appId: string) {
		try {
			members = await api.appMembers.list(appId);
		} catch (e) {
			members = [];
			memberActionError = e instanceof Error ? e.message : String(e);
		}
	}

	async function loadEligibleUsers() {
		eligibleLoadError = null;
		try {
			const all = await api.admins.list();
			// Only inactive=false members are valid invite targets — the
			// API rejects everyone else anyway, so filter upfront.
			eligibleUsers = all.filter(
				(u) => u.is_active && u.instance_role === 'member'
			);
		} catch (e) {
			eligibleUsers = [];
			// member-with-app_admin can hit /apps/.../members but cannot
			// browse /admins (gated on InstanceManageUsers). The add form
			// will render disabled with the explanatory message below.
			eligibleLoadError =
				e instanceof ApiError && e.status === 403
					? 'Only instance owners/admins can browse the user directory to invite new members.'
					: e instanceof Error
						? e.message
						: String(e);
		}
	}

	const eligibleAfterFilter = $derived(
		eligibleUsers.filter((u) => !members.some((m) => m.user_id === u.id))
	);

	async function submitCreateScript(event: Event) {
		event.preventDefault();
		if (!app) return;
		creatingScript = true;
		createScriptError = null;
		try {
			await api.scripts.create({
				app_id: app.id,
				name: createScriptName.trim(),
				description: createScriptDescription.trim() || null,
				source: createScriptSource,
				kind: createScriptKind
			});
			showCreateScript = false;
			createScriptName = '';
			createScriptDescription = '';
			createScriptSource = SAMPLE_SOURCE;
			createScriptKind = 'endpoint';
			await loadScripts(app.id);
		} catch (e) {
			createScriptError = e instanceof Error ? e.message : String(e);
			if (e instanceof ApiError && e.status === 422) {
				createScriptError = `Validation: ${createScriptError}`;
			}
		} finally {
			creatingScript = false;
		}
	}

	async function submitCreateDomain(event: Event) {
		event.preventDefault();
		if (!app) return;
		creatingDomain = true;
		createDomainError = null;
		try {
			await api.domains.create(app.id, createDomainPattern.trim());
			createDomainPattern = '';
			await loadDomains(app.id);
		} catch (e) {
			createDomainError = e instanceof Error ? e.message : String(e);
		} finally {
			creatingDomain = false;
		}
	}

	function askRemoveDomain(d: AppDomain) {
		removeDomainError = null;
		domainToRemove = d;
	}

	async function confirmRemoveDomain() {
		if (!app || !domainToRemove) return;
		removingDomain = true;
		removeDomainError = null;
		try {
			await api.domains.remove(app.id, domainToRemove.id);
			domainToRemove = null;
			await loadDomains(app.id);
		} catch (e) {
			removeDomainError = e instanceof Error ? e.message : String(e);
		} finally {
			removingDomain = false;
		}
	}

	async function saveSettings(event: Event, forceTakeover = false) {
		event.preventDefault();
		if (!app) return;
		savingSettings = true;
		settingsError = null;
		if (!forceTakeover) slugTakeoverNeeded = null;
		try {
			const slugChanged = editSlug.trim() !== app.slug;
			const updated = await api.apps.update(app.id, {
				name: editName.trim() !== app.name ? editName.trim() : undefined,
				description:
					editDescription !== (app.description ?? '')
						? editDescription || null
						: undefined,
				slug: slugChanged ? editSlug.trim() : undefined,
				force_takeover: forceTakeover || undefined
			});
			if (slugChanged) {
				await goto(`${base}/apps/${updated.slug}`, { replaceState: true });
				return;
			}
			app = updated;
		} catch (e) {
			if (e instanceof ApiError && e.status === 409 && e.body) {
				const body = e.body as { conflict_kind?: string; current_app?: App };
				if (body.conflict_kind === 'historical' && body.current_app) {
					slugTakeoverNeeded = body.current_app;
					settingsError = null;
					return;
				}
			}
			settingsError = e instanceof Error ? e.message : String(e);
		} finally {
			savingSettings = false;
		}
	}

	async function submitAddMember(event: Event) {
		event.preventDefault();
		if (!app || !addMemberUserId) return;
		addingMember = true;
		addMemberError = null;
		try {
			await api.appMembers.add(app.id, {
				user_id: addMemberUserId,
				role: addMemberRole
			});
			addMemberUserId = '';
			addMemberRole = 'viewer';
			await loadMembers(app.id);
		} catch (e) {
			addMemberError = e instanceof Error ? e.message : String(e);
		} finally {
			addingMember = false;
		}
	}

	async function changeMemberRole(member: AppMemberDto, role: AppRole) {
		if (!app || member.role === role) return;
		roleChangeBusy = member.user_id;
		memberActionError = null;
		try {
			await api.appMembers.setRole(app.id, member.user_id, role);
			await loadMembers(app.id);
		} catch (e) {
			memberActionError = e instanceof Error ? e.message : String(e);
		} finally {
			roleChangeBusy = null;
		}
	}

	function askRemoveMember(member: AppMemberDto) {
		removeMemberError = null;
		memberToRemove = member;
	}

	async function confirmRemoveMember() {
		if (!app || !memberToRemove) return;
		removingMember = true;
		removeMemberError = null;
		try {
			const removedSelf = !!me && memberToRemove.user_id === me.id;
			await api.appMembers.remove(app.id, memberToRemove.user_id);
			memberToRemove = null;
			if (removedSelf) {
				// We just revoked our own access to this app; the next
				// fetch of /apps/{slug} would 403. Bounce back to the
				// apps list rather than render a broken tab.
				await goto(`${base}/apps`);
				return;
			}
			await loadMembers(app.id);
		} catch (e) {
			removeMemberError = e instanceof Error ? e.message : String(e);
		} finally {
			removingMember = false;
		}
	}

	function shortDate(iso: string): string {
		try {
			return new Date(iso).toLocaleDateString();
		} catch {
			return iso;
		}
	}

	function askDeleteApp() {
		deleteAppError = null;
		confirmingDeleteApp = true;
	}

	async function confirmDeleteApp() {
		if (!app) return;
		deletingApp = true;
		deleteAppError = null;
		try {
			// force=true cascades scripts (and thereby their routes +
			// execution logs); domains and slug-history rows cascade off
			// the app row itself.
			await api.apps.remove(app.id, { force: true });
			await goto(`${base}/apps`);
		} catch (e) {
			deleteAppError = e instanceof Error ? e.message : String(e);
		} finally {
			deletingApp = false;
		}
	}

	$effect(() => {
		void loadApp();
	});

	// Defense-in-depth: a viewer / editor following a stale link to an
	// admin-only tab (?tab=settings, etc.) gets bounced back to Scripts.
	// The backend still 403s the underlying calls, but no point showing
	// an empty tab. activeTab is URL-driven now so we update the URL.
	$effect(() => {
		if (
			!canAdmin &&
			(activeTab === 'settings' ||
				activeTab === 'members' ||
				activeTab === 'triggers' ||
				activeTab === 'topics' ||
				activeTab === 'secrets')
		) {
			void goto(`${base}/apps/${slug}?tab=scripts`, { replaceState: true });
		}
	});
</script>

{#if app}
	{#if activeTab === 'scripts'}
		<section>
			<div class="row">
				<h2>Scripts</h2>
				{#if canWrite}
					<button
						type="button"
						onclick={() => (showCreateScript = !showCreateScript)}
					>
						{showCreateScript ? 'Cancel' : 'New script'}
					</button>
				{/if}
			</div>

			{#if showCreateScript && canWrite}
				<form class="create-form" onsubmit={submitCreateScript}>
					<div class="row">
						<label>
							<span>Name</span>
							<input bind:value={createScriptName} required placeholder="echo" />
						</label>
						<label>
							<span>Kind</span>
							<select bind:value={createScriptKind}>
								<option value="endpoint">Endpoint (handles HTTP / triggers)</option>
								<option value="module">Module (imported by other scripts)</option>
							</select>
						</label>
						<label>
							<span>Description</span>
							<input bind:value={createScriptDescription} placeholder="optional" />
						</label>
					</div>
					<label class="full">
						<span>Source (Rhai)</span>
						<CodeEditor bind:value={createScriptSource} language="rhai" minHeight="14rem" />
					</label>
					{#if createScriptKind === 'module'}
						<p class="muted small">
							Modules expose <code>fn</code> and <code>const</code> declarations to other
							scripts via <code>import "name" as alias;</code>. They cannot be bound to
							routes or used as trigger targets.
						</p>
					{/if}
					{#if createScriptError}
						<div class="error">{createScriptError}</div>
					{/if}
					<div class="actions">
						<button type="submit" disabled={creatingScript}>
							{creatingScript ? 'Creating…' : 'Create script'}
						</button>
					</div>
				</form>
			{/if}

			{#if scripts.length === 0}
				<p class="muted">No scripts in this app yet.</p>
			{:else}
				<ul class="list">
					{#each scripts as script (script.id)}
						<li>
							<a href="{base}/scripts/{script.id}">
								<div class="primary">
									<strong>{script.name}</strong>
									<span class="muted">v{script.version}</span>
									{#if script.kind === 'module'}
										<span class="kind-badge kind-module" title="Library imported by other scripts">module</span>
									{:else}
										<span class="kind-badge kind-endpoint" title="Handles HTTP routes and trigger events">endpoint</span>
									{/if}
								</div>
								<div class="secondary muted">{script.description ?? '—'}</div>
							</a>
						</li>
					{/each}
				</ul>
			{/if}
		</section>
	{:else if activeTab === 'domains'}
		<section>
			<h2>Domain claims</h2>
			<p class="muted">
				Hosts this app answers on. Routes inside this app can only bind to
				these. Use <code>app.example.com</code> for exact, <code>*.example.com</code> for
				wildcard, or <code>{'{'}tenant{'}'}.example.com</code> to bind a capture.
			</p>
			{#if canAdmin}
				<form class="create-form inline" onsubmit={submitCreateDomain}>
					<input
						bind:value={createDomainPattern}
						required
						placeholder="app.example.com"
					/>
					<button type="submit" disabled={creatingDomain}>
						{creatingDomain ? 'Adding…' : 'Add domain'}
					</button>
				</form>
				{#if createDomainError}
					<div class="error">{createDomainError}</div>
				{/if}
			{/if}
			{#if domains.length === 0}
				<p class="muted">No domain claims yet.</p>
			{:else}
				<ul class="list">
					{#each domains as d (d.id)}
						<li class="domain-row">
							<div>
								<code>{d.pattern}</code>
								<span class="muted">— {d.shape}</span>
							</div>
							{#if canAdmin}
								<button
									type="button"
									class="secondary danger"
									onclick={() => askRemoveDomain(d)}
								>
									Delete
								</button>
							{/if}
						</li>
					{/each}
				</ul>
			{/if}
		</section>
	{:else if activeTab === 'members' && canAdmin}
		<section>
			<h2>Members</h2>
			<p class="muted">
				Users with explicit access to this app. Instance owners and admins
				already have implicit access — they are not listed here. Use the Users
				page to invite a <code>member</code> first, then grant them app access
				below.
			</p>

			<form class="create-form" onsubmit={submitAddMember}>
				<div class="row">
					<label class="grow">
						<span>User</span>
						<select
							bind:value={addMemberUserId}
							disabled={!!eligibleLoadError || eligibleAfterFilter.length === 0}
							required
						>
							<option value="" disabled>Pick a member to invite…</option>
							{#each eligibleAfterFilter as u (u.id)}
								<option value={u.id}>{u.username}{u.email ? ` (${u.email})` : ''}</option>
							{/each}
						</select>
					</label>
					<label>
						<span>Role</span>
						<select bind:value={addMemberRole} disabled={!!eligibleLoadError}>
							<option value="viewer">viewer</option>
							<option value="editor">editor</option>
							<option value="app_admin">app admin</option>
						</select>
					</label>
				</div>
				{#if eligibleLoadError}
					<p class="muted">{eligibleLoadError}</p>
				{:else if eligibleAfterFilter.length === 0}
					<p class="muted">
						No eligible users to invite. Create a <code>member</code> on the Users
						page first.
					</p>
				{/if}
				{#if addMemberError}
					<div class="error">{addMemberError}</div>
				{/if}
				<div class="actions">
					<button
						type="submit"
						disabled={addingMember || !addMemberUserId || !!eligibleLoadError}
					>
						{addingMember ? 'Adding…' : 'Add member'}
					</button>
				</div>
			</form>

			{#if memberActionError}
				<div class="error">{memberActionError}</div>
			{/if}

			{#if members.length === 0}
				<p class="muted">No explicit members yet.</p>
			{:else}
				<div class="table">
					<div class="row head-row">
						<div>User</div>
						<div>Instance</div>
						<div>App role</div>
						<div>Joined</div>
						<div class="actions-col"></div>
					</div>
					{#each members as m (m.user_id)}
						<div class="row member-row" class:inactive={!m.is_active}>
							<div>
								<strong>{m.username}</strong>
								{#if m.email}<span class="muted">{m.email}</span>{/if}
								{#if !m.is_active}<span class="muted">(inactive)</span>{/if}
							</div>
							<div><RoleChip role={m.instance_role} size="sm" /></div>
							<div><RoleChip appRole={m.role} size="sm" /></div>
							<div>{shortDate(m.created_at)}</div>
							<div class="actions-col">
								<ActionMenu
									label="Member actions for {m.username}"
									items={[
										{
											label: 'Make app admin',
											disabled:
												m.role === 'app_admin' || roleChangeBusy === m.user_id,
											onClick: () => changeMemberRole(m, 'app_admin')
										},
										{
											label: 'Make editor',
											disabled:
												m.role === 'editor' || roleChangeBusy === m.user_id,
											onClick: () => changeMemberRole(m, 'editor')
										},
										{
											label: 'Make viewer',
											disabled:
												m.role === 'viewer' || roleChangeBusy === m.user_id,
											onClick: () => changeMemberRole(m, 'viewer')
										},
										{
											label: 'Remove from app',
											danger: true,
											onClick: () => askRemoveMember(m)
										}
									]}
								/>
							</div>
						</div>
					{/each}
				</div>
			{/if}
		</section>
	{:else if activeTab === 'triggers' && canAdmin}
		<section>
			<h2>Cron triggers</h2>
			<p class="muted">
				Run an endpoint script on a schedule. Schedules are 6-field cron
				expressions (with seconds): <code>sec min hour day-of-month month day-of-week</code>.
				The timezone disambiguates schedules like "every weekday at 9am".
			</p>

			<form class="create-form" onsubmit={submitCreateCron}>
				<div class="row">
					<label>
						<span>Target script</span>
						<select bind:value={createCronScriptId} required>
							<option value="" disabled>Select an endpoint script…</option>
							{#each endpointScripts as s (s.id)}
								<option value={s.id}>{s.name}</option>
							{/each}
						</select>
					</label>
					<label>
						<span>Schedule</span>
						<input
							bind:value={createCronSchedule}
							required
							placeholder="0 0 9 * * MON-FRI"
						/>
					</label>
					<label>
						<span>Timezone</span>
						<select bind:value={createCronTimezone}>
							{#each COMMON_TIMEZONES as tz (tz)}
								<option value={tz}>{tz}</option>
							{/each}
						</select>
					</label>
				</div>
				{#if endpointScripts.length === 0}
					<p class="muted small">
						This app has no endpoint scripts yet — create one first (modules
						can't be trigger targets).
					</p>
				{/if}
				{#if createCronError}
					<div class="error">{createCronError}</div>
				{/if}
				<div class="actions">
					<button type="submit" disabled={creatingCron || !createCronScriptId}>
						{creatingCron ? 'Creating…' : 'Create cron trigger'}
					</button>
				</div>
			</form>

			<h2>Pub/Sub triggers</h2>
			<p class="muted">
				Subscribe an endpoint script to durable pub/sub messages. Topic
				patterns are an exact topic (<code>user.created</code>), a prefix
				wildcard (<code>user.*</code>), or <code>*</code> for every topic.
			</p>

			<form class="create-form" onsubmit={submitCreatePubsub}>
				<div class="row">
					<label>
						<span>Target script</span>
						<select bind:value={createPubsubScriptId} required>
							<option value="" disabled>Select an endpoint script…</option>
							{#each endpointScripts as s (s.id)}
								<option value={s.id}>{s.name}</option>
							{/each}
						</select>
					</label>
					<label>
						<span>Topic pattern</span>
						<input bind:value={createPubsubTopic} required placeholder="user.*" />
					</label>
				</div>
				{#if createPubsubError}
					<div class="error">{createPubsubError}</div>
				{/if}
				<div class="actions">
					<button
						type="submit"
						disabled={creatingPubsub || !createPubsubScriptId || !createPubsubTopic.trim()}
					>
						{creatingPubsub ? 'Creating…' : 'Create pub/sub trigger'}
					</button>
				</div>
			</form>


			<h2>Email triggers</h2>
			<p class="muted">
				Fire an endpoint script when your email provider POSTs an inbound
				message to PiCloud. Configure your provider (Mailgun / Postmark /
				SendGrid / SES) to POST the generic JSON shape below to the trigger's
				webhook URL. Set a shared secret to require an
				<code>X-Picloud-Signature</code> HMAC-SHA256 (hex of the request body);
				leave it blank to accept unsigned POSTs (URL secrecy only).
			</p>
			<details class="muted small chevron">
				<summary>Expected inbound JSON shape</summary>
				<pre>{`{
  "from": "sender@external.com",
  "to": ["alice@myapp.com"],
  "cc": [],
  "subject": "...",
  "text": "...",
  "html": "...",
  "message_id": "<abc@external.com>"
}`}</pre>
			</details>

			<form class="create-form" onsubmit={submitCreateEmail}>
				<div class="row">
					<label>
						<span>Target script</span>
						<select bind:value={createEmailScriptId} required>
							<option value="" disabled>Select an endpoint script…</option>
							{#each endpointScripts as s (s.id)}
								<option value={s.id}>{s.name}</option>
							{/each}
						</select>
					</label>
					<label class="grow">
						<span>Inbound HMAC secret (optional)</span>
						<input
							type="password"
							bind:value={createEmailSecret}
							placeholder="leave blank to accept unsigned POSTs"
						/>
					</label>
				</div>
				{#if createEmailError}
					<div class="error">{createEmailError}</div>
				{/if}
				<div class="actions">
					<button type="submit" disabled={creatingEmail || !createEmailScriptId}>
						{creatingEmail ? 'Creating…' : 'Create email trigger'}
					</button>
				</div>
			</form>

			<h3>Queue:receive trigger (v1.1.9)</h3>
			<p class="muted">
				Register a script as the consumer for a per-app durable queue. Exactly
				one consumer per queue is allowed — the registration will be rejected
				if another consumer already exists. Use the
				<a href="{base}/apps/{slug}/queues">Queues</a> tab for a read-only view
				of message depths.
			</p>
			<form class="create-form" onsubmit={submitCreateQueue}>
				<div class="row">
					<label>
						<span>Target script</span>
						<select bind:value={createQueueScriptId} required>
							<option value="" disabled>Select an endpoint script…</option>
							{#each endpointScripts as s (s.id)}
								<option value={s.id}>{s.name}</option>
							{/each}
						</select>
					</label>
					<label class="grow">
						<span>Queue name</span>
						<input
							type="text"
							bind:value={createQueueName}
							placeholder="payments.process"
							required
						/>
					</label>
				</div>
				<div class="row">
					<label>
						<span>Visibility timeout (5–3600s)</span>
						<input
							type="number"
							bind:value={createQueueVisibilityTimeout}
							min="5"
							max="3600"
							required
						/>
					</label>
					<label>
						<span>Max attempts (1–20)</span>
						<input
							type="number"
							bind:value={createQueueMaxAttempts}
							min="1"
							max="20"
							required
						/>
					</label>
				</div>
				{#if createQueueError}
					<div class="error">{createQueueError}</div>
				{/if}
				<div class="actions">
					<button
						type="submit"
						disabled={creatingQueue || !createQueueScriptId || !createQueueName}
					>
						{creatingQueue ? 'Creating…' : 'Create queue:receive trigger'}
					</button>
				</div>
			</form>

			<!-- F-U-001: KV / Docs / Files / Dead-letter trigger create forms. -->
			<h3>Create kv:event trigger</h3>
			<form class="create-form" onsubmit={submitCreateKv}>
				<div class="row">
					<label>
						<span>Target script</span>
						<select bind:value={createKvScriptId} required>
							<option value="" disabled>Select an endpoint script…</option>
							{#each endpointScripts as s (s.id)}
								<option value={s.id}>{s.name}</option>
							{/each}
						</select>
					</label>
					<label class="grow">
						<span>Collection glob</span>
						<input
							type="text"
							bind:value={createKvCollectionGlob}
							placeholder="users.* or *"
							required
						/>
					</label>
				</div>
				<div class="row">
					<span>Ops:</span>
					<label><input type="checkbox" bind:checked={createKvOps.insert} /> insert</label>
					<label><input type="checkbox" bind:checked={createKvOps.update} /> update</label>
					<label><input type="checkbox" bind:checked={createKvOps.delete} /> delete</label>
				</div>
				{#if createKvError}<div class="error">{createKvError}</div>{/if}
				<div class="actions">
					<button type="submit" disabled={creatingKv || !createKvScriptId || !createKvCollectionGlob}>
						{creatingKv ? 'Creating…' : 'Create kv:event trigger'}
					</button>
				</div>
			</form>

			<h3>Create docs:event trigger</h3>
			<form class="create-form" onsubmit={submitCreateDocs}>
				<div class="row">
					<label>
						<span>Target script</span>
						<select bind:value={createDocsScriptId} required>
							<option value="" disabled>Select an endpoint script…</option>
							{#each endpointScripts as s (s.id)}
								<option value={s.id}>{s.name}</option>
							{/each}
						</select>
					</label>
					<label class="grow">
						<span>Collection glob</span>
						<input
							type="text"
							bind:value={createDocsCollectionGlob}
							placeholder="users.* or *"
							required
						/>
					</label>
				</div>
				<div class="row">
					<span>Ops:</span>
					<label><input type="checkbox" bind:checked={createDocsOps.create} /> create</label>
					<label><input type="checkbox" bind:checked={createDocsOps.update} /> update</label>
					<label><input type="checkbox" bind:checked={createDocsOps.delete} /> delete</label>
				</div>
				{#if createDocsError}<div class="error">{createDocsError}</div>{/if}
				<div class="actions">
					<button
						type="submit"
						disabled={creatingDocs || !createDocsScriptId || !createDocsCollectionGlob}
					>
						{creatingDocs ? 'Creating…' : 'Create docs:event trigger'}
					</button>
				</div>
			</form>

			<h3>Create files:event trigger</h3>
			<form class="create-form" onsubmit={submitCreateFiles}>
				<div class="row">
					<label>
						<span>Target script</span>
						<select bind:value={createFilesScriptId} required>
							<option value="" disabled>Select an endpoint script…</option>
							{#each endpointScripts as s (s.id)}
								<option value={s.id}>{s.name}</option>
							{/each}
						</select>
					</label>
					<label class="grow">
						<span>Collection glob</span>
						<input
							type="text"
							bind:value={createFilesCollectionGlob}
							placeholder="avatars.* or *"
							required
						/>
					</label>
				</div>
				<div class="row">
					<span>Ops:</span>
					<label><input type="checkbox" bind:checked={createFilesOps.create} /> create</label>
					<label><input type="checkbox" bind:checked={createFilesOps.update} /> update</label>
					<label><input type="checkbox" bind:checked={createFilesOps.delete} /> delete</label>
				</div>
				{#if createFilesError}<div class="error">{createFilesError}</div>{/if}
				<div class="actions">
					<button
						type="submit"
						disabled={creatingFiles || !createFilesScriptId || !createFilesCollectionGlob}
					>
						{creatingFiles ? 'Creating…' : 'Create files:event trigger'}
					</button>
				</div>
			</form>

			<h3>Create dead_letter trigger</h3>
			<p class="hint">
				Fires when any other trigger or route dead-letters. All filter fields are optional;
				leaving them blank routes every dead-letter to this script.
			</p>
			<form class="create-form" onsubmit={submitCreateDeadLetter}>
				<div class="row">
					<label>
						<span>Target script</span>
						<select bind:value={createDeadLetterScriptId} required>
							<option value="" disabled>Select an endpoint script…</option>
							{#each endpointScripts as s (s.id)}
								<option value={s.id}>{s.name}</option>
							{/each}
						</select>
					</label>
					<label class="grow">
						<span>Source filter (optional)</span>
						<input
							type="text"
							bind:value={createDeadLetterSourceFilter}
							placeholder="kv, docs, queue, pubsub, …"
						/>
					</label>
				</div>
				<div class="row">
					<label class="grow">
						<span>Trigger ID filter (optional)</span>
						<input type="text" bind:value={createDeadLetterTriggerIdFilter} />
					</label>
					<label class="grow">
						<span>Script ID filter (optional)</span>
						<input type="text" bind:value={createDeadLetterScriptIdFilter} />
					</label>
				</div>
				{#if createDeadLetterError}<div class="error">{createDeadLetterError}</div>{/if}
				<div class="actions">
					<button type="submit" disabled={creatingDeadLetter || !createDeadLetterScriptId}>
						{creatingDeadLetter ? 'Creating…' : 'Create dead_letter trigger'}
					</button>
				</div>
			</form>

			{#if triggers.length === 0}
				<p class="muted">No triggers in this app yet.</p>
			{:else}
				<ul class="list">
					{#each triggers as t (t.id)}
						<li class="domain-row">
							<div>
								<span class="kind-badge">{t.kind}</span>
								{#if !t.enabled}
									<!-- F-U-006: surface the trigger.enabled flag. No PATCH endpoint
									     yet, so this is read-only; toggle UI tracked for a follow-up. -->
									<span class="muted small" title="Trigger is disabled — events are not dispatched.">• disabled</span>
								{/if}
								{#if t.details.kind === 'cron'}
									<code>{t.details.schedule}</code>
									<span class="muted">— {t.details.timezone}</span>
									<span class="muted small">
										last fired: {t.details.last_fired_at ?? 'never'}
									</span>
								{:else if t.details.kind === 'kv' || t.details.kind === 'docs' || t.details.kind === 'files'}
									<code>{t.details.collection_glob}</code>
									<span class="muted">— {t.details.ops.join(', ') || 'any op'}</span>
								{:else if t.details.kind === 'pubsub'}
									<code>{t.details.topic_pattern}</code>
									{:else if t.details.kind === 'email'}
										<span class="muted">
											{t.details.has_inbound_secret ? 'signed (HMAC)' : 'unsigned'}
										</span>
										<code class="webhook-url">{emailInboundUrl(t.id)}</code>
								{:else if t.details.kind === 'queue'}
										<code>{t.details.queue_name}</code>
										<span class="muted">— vt {t.details.visibility_timeout_secs}s</span>
										<span class="muted small">
											last fired: {t.details.last_fired_at ?? 'never'}
										</span>
								{/if}
								<span class="muted small">→ {t.script_id}</span>
							</div>
							<button
								type="button"
								class="secondary danger"
								onclick={() => (triggerToRemove = t)}
							>
								Delete
							</button>
						</li>
					{/each}
				</ul>
			{/if}
		</section>
	{:else if activeTab === 'topics' && canAdmin}
		<section>
			<h2>Realtime topics</h2>
			<p class="muted">
				Pub/sub topics are <strong>internal-only</strong> by default — scripts
				subscribe via triggers, browsers can't. Register a topic here and mark it
				<strong>externally subscribable</strong> to let frontend clients connect over
				SSE at <code>/realtime/topics/&lt;name&gt;</code>. <code>public</code> topics
				need no auth; <code>token</code> topics require a subscriber token minted by a
				script via <code>pubsub::subscriber_token</code>.
			</p>

			<form class="create-form" onsubmit={submitCreateTopic}>
				<div class="row">
					<label class="grow">
						<span>Topic name</span>
						<input bind:value={createTopicName} required placeholder="chat-room-updates" />
					</label>
				</div>
				<label class="checkbox-row">
					<input type="checkbox" bind:checked={createTopicExternal} />
					<span>Externally subscribable (allow browser SSE clients to subscribe)</span>
				</label>
				{#if createTopicExternal}
					<fieldset class="auth-mode">
						<legend>Auth mode</legend>
						<label class="radio-row">
							<input type="radio" value="public" bind:group={createTopicAuthMode} />
							<span><strong>public</strong> — anyone with the URL can subscribe</span>
						</label>
						<label class="radio-row">
							<input type="radio" value="token" bind:group={createTopicAuthMode} />
							<span><strong>token</strong> — requires a valid subscriber token</span>
						</label>
						<label class="radio-row">
							<input type="radio" value="session" bind:group={createTopicAuthMode} />
							<span>
								<strong>session</strong> — requires a per-app user session
								minted by <code>users::login</code> (v1.1.8)
							</span>
						</label>
					</fieldset>
				{/if}
				{#if createTopicError}
					<div class="error">{createTopicError}</div>
				{/if}
				<div class="actions">
					<button type="submit" disabled={creatingTopic || !createTopicName.trim()}>
						{creatingTopic ? 'Creating…' : 'Register topic'}
					</button>
				</div>
			</form>

			{#if topics.length === 0}
				<p class="muted">No registered topics in this app yet.</p>
			{:else}
				<ul class="list">
					{#each topics as t (t.name)}
						<li class="domain-row">
							<div>
								<code>{t.name}</code>
								{#if t.external_subscribable}
									<span class="badge badge-external" title="Browser clients can subscribe over SSE">
										external
									</span>
									<span class="badge badge-auth">{t.auth_mode}</span>
								{:else}
									<span class="badge badge-internal" title="Internal-only: scripts subscribe via triggers">
										internal
									</span>
								{/if}
								<span class="muted small">· {shortDate(t.created_at)}</span>
							</div>
							<div class="topic-actions">
								<button type="button" class="secondary" onclick={() => openEditTopic(t)}>
									Edit
								</button>
								<button
									type="button"
									class="secondary danger"
									onclick={() => (topicToRemove = t)}
								>
									Delete
								</button>
							</div>
						</li>
					{/each}
				</ul>
			{/if}
		</section>
	{:else if activeTab === 'secrets' && canAdmin}
		<section>
			<h2>Secrets</h2>
			<p class="muted">
				Encrypted per-app configuration (API keys, OAuth tokens, webhook signing
				keys), available to scripts as <code>secrets::get("name")</code>. Values are
				encrypted at rest with the process master key and
				<strong>never leave the server</strong> — this list shows names and
				last-modified times only.
			</p>

			<form class="create-form" onsubmit={submitCreateSecret}>
				<div class="row">
					<label class="grow">
						<span>Name</span>
						<input bind:value={createSecretName} required placeholder="stripe_key" />
					</label>
				</div>
				<label class="grow">
					<span>Value</span>
					{#if showSecretValue}
						<input type="text" bind:value={createSecretValue} placeholder="sk_live_…" />
					{:else}
						<input type="password" bind:value={createSecretValue} placeholder="sk_live_…" />
					{/if}
				</label>
				<label class="checkbox-row">
					<input type="checkbox" checked={showSecretValue} onchange={toggleShowSecretValue} />
					<span>Show value</span>
				</label>
				{#if secretNameExists && createSecretName.trim()}
					<p class="muted small">
						A secret named <code>{createSecretName.trim()}</code> already exists — saving
						overwrites it.
					</p>
				{/if}
				{#if createSecretError}
					<div class="error">{createSecretError}</div>
				{/if}
				<div class="actions">
					<button type="submit" disabled={creatingSecret || !createSecretName.trim()}>
						{creatingSecret ? 'Saving…' : secretNameExists ? 'Overwrite secret' : 'Save secret'}
					</button>
				</div>
			</form>

			{#if secrets.length === 0}
				<p class="muted">No secrets in this app yet.</p>
			{:else}
				<ul class="list">
					{#each secrets as s (s.name)}
						<li class="domain-row">
							<div>
								<code>{s.name}</code>
								<span class="muted small">· updated {shortDate(s.updated_at)}</span>
							</div>
							<div class="topic-actions">
								<button
									type="button"
									class="secondary danger"
									onclick={() => (secretToRemove = s)}
								>
									Delete
								</button>
							</div>
						</li>
					{/each}
				</ul>
			{/if}
		</section>
	{:else if activeTab === 'settings' && canAdmin}
		<section>
			<h2>Settings</h2>
			<form class="create-form" onsubmit={(e) => saveSettings(e)}>
				<label>
					<span>Name</span>
					<input bind:value={editName} required />
				</label>
				<label>
					<span>Description</span>
					<input bind:value={editDescription} />
				</label>
				<label>
					<span>Slug</span>
					<input
						bind:value={editSlug}
						required
						pattern="[a-z0-9][a-z0-9-]*"
					/>
					<small class="muted">
						Renaming records the old slug as a permanent 301 redirect.
					</small>
				</label>
				{#if slugTakeoverNeeded}
					<div class="warning">
						<strong>Slug previously redirected.</strong>
						<p>
							<code>{editSlug}</code> currently redirects to
							<code>{slugTakeoverNeeded.slug}</code>. Renaming to it will break old
							links.
						</p>
						<div class="actions">
							<button
								type="button"
								class="secondary"
								onclick={() => (slugTakeoverNeeded = null)}
							>
								Cancel
							</button>
							<button
								type="button"
								onclick={(e) => saveSettings(e, true)}
								disabled={savingSettings}
							>
								{savingSettings ? 'Renaming…' : 'Rename anyway'}
							</button>
						</div>
					</div>
				{:else if settingsError}
					<div class="error">{settingsError}</div>
				{/if}
				{#if !slugTakeoverNeeded}
					<div class="actions">
						<button type="submit" disabled={savingSettings}>
							{savingSettings ? 'Saving…' : 'Save changes'}
						</button>
					</div>
				{/if}
			</form>

			<div class="danger-zone">
				<h3>Delete app</h3>
				<p class="muted">
					Permanently removes the app along with all its scripts, routes,
					execution logs, and domain claims.
				</p>
				<button type="button" class="danger" onclick={askDeleteApp}>Delete app</button>
			</div>
		</section>
	{/if}

	{#if confirmingDeleteApp}
		<ConfirmModal
			title="Delete app “{app.name}”"
			variant="danger"
			confirmLabel="Delete app"
			busyLabel="Deleting…"
			confirmPhrase={app.slug}
			confirmPhrasePrompt="Type the app slug to confirm:"
			busy={deletingApp}
			onConfirm={confirmDeleteApp}
			onCancel={() => (confirmingDeleteApp = false)}
		>
			<p>
				This will <strong>permanently delete</strong> everything inside
				<strong>{app.name}</strong>. There is no undo.
			</p>
			<ul class="impact-list">
				<li>
					<span>Scripts</span><strong>{scripts.length}</strong>
				</li>
				<li>
					<span>Domain claims</span><strong>{domains.length}</strong>
				</li>
				<li>
					<span>Routes &amp; execution logs</span><strong>all</strong>
				</li>
			</ul>
			{#if domains.length > 0}
				<p>The following hosts will stop pointing at this app:</p>
				<ul class="impact-list">
					{#each domains as d (d.id)}
						<li>
							<code>{d.pattern}</code><span class="muted">{d.shape}</span>
						</li>
					{/each}
				</ul>
			{/if}
			{#if deleteAppError}
				<p class="modal-error">{deleteAppError}</p>
			{/if}
		</ConfirmModal>
	{/if}

	{#if domainToRemove}
		<ConfirmModal
			title="Delete domain claim"
			variant="danger"
			confirmLabel="Delete claim"
			busyLabel="Deleting…"
			busy={removingDomain}
			onConfirm={confirmRemoveDomain}
			onCancel={() => (domainToRemove = null)}
		>
			<p>
				<strong>{app.name}</strong> will stop answering on
				<code>{domainToRemove.pattern}</code>.
			</p>
			<p class="muted">
				Routes already bound to this host are blocked from deletion by the
				API; if so, you’ll see an error here.
			</p>
			{#if removeDomainError}
				<p class="modal-error">{removeDomainError}</p>
			{/if}
		</ConfirmModal>
	{/if}

	{#if memberToRemove}
		<ConfirmModal
			title="Remove {memberToRemove.username} from {app.name}"
			variant="danger"
			confirmLabel="Remove member"
			busyLabel="Removing…"
			busy={removingMember}
			onConfirm={confirmRemoveMember}
			onCancel={() => (memberToRemove = null)}
		>
			<p>
				<strong>{memberToRemove.username}</strong> will lose access to this
				app. Their other app memberships and account are untouched.
			</p>
			{#if removeMemberError}
				<p class="modal-error">{removeMemberError}</p>
			{/if}
		</ConfirmModal>
	{/if}

	{#if triggerToRemove}
		<ConfirmModal
			title="Delete trigger"
			variant="danger"
			confirmLabel="Delete trigger"
			busyLabel="Deleting…"
			busy={removingTrigger}
			onConfirm={confirmRemoveTrigger}
			onCancel={() => (triggerToRemove = null)}
		>
			<p>
				This {triggerToRemove.kind} trigger will stop firing. The target
				script is not affected.
			</p>
		</ConfirmModal>
	{/if}

	{#if topicToEdit}
		<ConfirmModal
			title="Edit topic “{topicToEdit.name}”"
			confirmLabel="Save changes"
			busyLabel="Saving…"
			busy={savingTopic}
			onConfirm={confirmEditTopic}
			onCancel={() => (topicToEdit = null)}
		>
			<label class="checkbox-row">
				<input type="checkbox" bind:checked={editTopicExternal} />
				<span>Externally subscribable</span>
			</label>
			{#if editTopicExternal}
				<fieldset class="auth-mode">
					<legend>Auth mode</legend>
					<label class="radio-row">
						<input type="radio" value="public" bind:group={editTopicAuthMode} />
						<span><strong>public</strong> — anyone with the URL can subscribe</span>
					</label>
					<label class="radio-row">
						<input type="radio" value="token" bind:group={editTopicAuthMode} />
						<span><strong>token</strong> — requires a valid subscriber token</span>
					</label>
					<label class="radio-row">
						<input type="radio" value="session" bind:group={editTopicAuthMode} />
						<span>
							<strong>session</strong> — requires a per-app user session minted
							by <code>users::login</code> (v1.1.8)
						</span>
					</label>
				</fieldset>
			{/if}
			{#if editFlipToExternal}
				<div class="warning">
					Marking <strong>{topicToEdit.name}</strong> externally-subscribable means
					anyone with the URL can subscribe to this topic (if auth_mode is
					<code>public</code>) or anyone with a valid token can subscribe (if
					auth_mode is <code>token</code>). Are you sure?
				</div>
			{/if}
			{#if editTopicError}
				<p class="modal-error">{editTopicError}</p>
			{/if}
		</ConfirmModal>
	{/if}

	{#if topicToRemove}
		<ConfirmModal
			title="Delete topic “{topicToRemove.name}”"
			variant="danger"
			confirmLabel="Delete topic"
			busyLabel="Deleting…"
			busy={removingTopic}
			onConfirm={confirmRemoveTopic}
			onCancel={() => (topicToRemove = null)}
		>
			<p>
				Unregistering <code>{topicToRemove.name}</code> disconnects any live SSE
				subscribers immediately. Scripts can still <code>publish_durable</code> to
				it (internal triggers keep working) — it just won't be externally
				subscribable.
			</p>
		</ConfirmModal>
	{/if}

	{#if revealConfirm}
		<ConfirmModal
			title="Reveal secret value?"
			confirmLabel="Show value"
			onConfirm={confirmRevealSecret}
			onCancel={() => (revealConfirm = false)}
		>
			<p>
				The value you type will be shown in plain text on screen. Make sure no one
				is looking over your shoulder and that screen-sharing is off.
			</p>
		</ConfirmModal>
	{/if}

	{#if secretToRemove}
		<ConfirmModal
			title="Delete secret “{secretToRemove.name}”"
			variant="danger"
			confirmLabel="Delete secret"
			busyLabel="Deleting…"
			busy={removingSecret}
			onConfirm={confirmRemoveSecret}
			onCancel={() => (secretToRemove = null)}
		>
			<p>
				Deleting <code>{secretToRemove.name}</code> is permanent. Any script calling
				<code>secrets::get("{secretToRemove.name}")</code> will get <code>()</code>
				until you set it again.
			</p>
		</ConfirmModal>
	{/if}
{/if}

<style>
	h2 {
		font-size: 1.125rem;
		margin: 0 0 1rem;
	}

	h3 {
		font-size: 1rem;
		margin: 0 0 0.5rem;
	}

	button {
		background: #38bdf8;
		color: #0b1220;
		border: none;
		padding: 0.5rem 1rem;
		border-radius: 0.375rem;
		font-weight: 600;
		cursor: pointer;
	}

	button.secondary {
		background: transparent;
		color: #94a3b8;
		border: 1px solid #334155;
	}

	button.danger {
		background: #7f1d1d;
		color: #fecaca;
	}

	button.secondary.danger {
		background: transparent;
		color: #fca5a5;
		border-color: #7f1d1d;
	}

	button:disabled {
		opacity: 0.5;
		cursor: not-allowed;
	}

	.muted {
		color: #64748b;
	}

	.error {
		border: 1px solid #b91c1c;
		background: #450a0a;
		color: #fecaca;
		padding: 1rem;
		border-radius: 0.5rem;
		margin: 1rem 0;
	}

	.warning {
		border: 1px solid #ca8a04;
		background: #3f2e07;
		color: #fde68a;
		padding: 1rem;
		border-radius: 0.5rem;
		margin: 1rem 0;
	}

	.warning code {
		background: #1e293b;
		padding: 0.1rem 0.3rem;
		border-radius: 0.25rem;
	}

	.create-form {
		background: #1e293b;
		border-radius: 0.5rem;
		padding: 1.25rem;
		margin-bottom: 1.5rem;
		display: flex;
		flex-direction: column;
		gap: 0.75rem;
	}

	.create-form.inline {
		flex-direction: row;
		align-items: center;
		gap: 0.5rem;
	}

	.create-form .row {
		display: grid;
		grid-template-columns: 1fr 2fr;
		gap: 0.75rem;
	}

	.create-form label {
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
		font-size: 0.85rem;
		color: #cbd5e1;
	}

	.create-form label.full {
		grid-column: 1 / -1;
	}

	.create-form input {
		background: #0b1220;
		color: #e2e8f0;
		border: 1px solid #334155;
		border-radius: 0.375rem;
		padding: 0.5rem 0.75rem;
		font: inherit;
		flex: 1;
	}

	.actions {
		display: flex;
		justify-content: flex-end;
		gap: 0.5rem;
	}

	.row {
		display: flex;
		align-items: center;
		justify-content: space-between;
		margin-bottom: 1rem;
	}

	.list {
		list-style: none;
		padding: 0;
		margin: 0;
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
	}

	.list a {
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
		padding: 0.85rem 1rem;
		background: #1e293b;
		border-radius: 0.375rem;
		text-decoration: none;
		color: inherit;
	}

	.list a:hover {
		background: #283549;
	}

	.domain-row {
		display: flex;
		justify-content: space-between;
		align-items: center;
		padding: 0.85rem 1rem;
		background: #1e293b;
		border-radius: 0.375rem;
	}

	.domain-row code {
		background: #0b1220;
		padding: 0.15rem 0.4rem;
		border-radius: 0.25rem;
	}

	.primary {
		display: flex;
		gap: 0.5rem;
		align-items: baseline;
	}

	.secondary {
		font-size: 0.875rem;
	}

	.danger-zone {
		margin-top: 2rem;
		padding: 1rem;
		border: 1px solid #7f1d1d;
		border-radius: 0.5rem;
		background: #1e0a0a;
	}

	.create-form select {
		background: #0b1220;
		color: #e2e8f0;
		border: 1px solid #334155;
		border-radius: 0.375rem;
		padding: 0.5rem 0.75rem;
		font: inherit;
	}

	.create-form .row > label.grow {
		grid-column: span 2;
	}

	.table {
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
	}

	.table .row {
		display: grid;
		grid-template-columns: 2fr 1fr 1fr 1fr 3rem;
		gap: 0.75rem;
		padding: 0.85rem 1rem;
		background: #1e293b;
		border-radius: 0.375rem;
		align-items: center;
		margin: 0;
	}

	.table .head-row {
		background: transparent;
		padding: 0.25rem 1rem;
		color: #64748b;
		font-size: 0.75rem;
		text-transform: uppercase;
		letter-spacing: 0.05em;
	}

	.table .member-row.inactive {
		opacity: 0.55;
	}

	.table .member-row strong {
		margin-right: 0.4rem;
	}

	.table .member-row .muted {
		font-size: 0.8rem;
	}

	.table .actions-col {
		display: flex;
		justify-content: flex-end;
	}

	.kind-badge {
		display: inline-block;
		padding: 0 0.45rem;
		margin-left: 0.5rem;
		border-radius: 0.25rem;
		font-size: 0.7rem;
		font-weight: 600;
		text-transform: uppercase;
		letter-spacing: 0.05em;
		line-height: 1.5;
	}

	.kind-endpoint {
		background: #1e3a5f;
		color: #93c5fd;
	}

	.kind-module {
		background: #3f2e7d;
		color: #c4b5fd;
	}

	.small {
		font-size: 0.85rem;
	}

	.checkbox-row,
	.radio-row {
		flex-direction: row;
		align-items: center;
		gap: 0.5rem;
	}

	.checkbox-row input,
	.radio-row input {
		flex: none;
		width: auto;
	}

	.auth-mode {
		border: 1px solid #334155;
		border-radius: 0.375rem;
		padding: 0.5rem 0.75rem;
		display: flex;
		flex-direction: column;
		gap: 0.4rem;
	}

	.auth-mode legend {
		font-size: 0.8rem;
		color: #94a3b8;
		padding: 0 0.3rem;
	}

	.topic-actions {
		display: flex;
		gap: 0.5rem;
	}

	.badge {
		display: inline-block;
		padding: 0 0.45rem;
		margin-left: 0.4rem;
		border-radius: 0.25rem;
		font-size: 0.7rem;
		font-weight: 600;
		text-transform: uppercase;
		letter-spacing: 0.05em;
		line-height: 1.5;
	}

	.badge-external {
		background: #064e3b;
		color: #6ee7b7;
	}

	.badge-internal {
		background: #334155;
		color: #cbd5e1;
	}

	.badge-auth {
		background: #1e3a5f;
		color: #93c5fd;
	}
</style>
