<script lang="ts">
	import { base } from '$app/paths';
	import { goto } from '$app/navigation';
	import { page } from '$app/state';
	import {
		api,
		ApiError,
		type AdminDto,
		type Group,
		type GroupDetail,
		type GroupMember,
		type AppRole
	} from '$lib/api';
	import ConfirmModal from '$lib/ConfirmModal.svelte';
	import ActionMenu from '$lib/ActionMenu.svelte';
	import RoleChip from '$lib/RoleChip.svelte';
	import { currentUser } from '$lib/auth';
	import { canCreateApp } from '$lib/capabilities';

	const me = $derived($currentUser);
	// Mirror app-admin gating: instance owner/admin manage groups, their
	// members, structure, and deletion. The backend is the ground truth.
	const canManage = $derived(canCreateApp(me));

	type Tab = 'overview' | 'members' | 'settings';
	const isValidTab = (s: string | null): s is Tab =>
		s === 'overview' || s === 'members' || s === 'settings';
	let activeTab = $derived.by<Tab>(() => {
		const qp = page.url.searchParams.get('tab');
		return isValidTab(qp) ? qp : 'overview';
	});

	let slug = $derived(page.params.slug ?? '');
	let group = $state<GroupDetail | null>(null);
	let loadError = $state<string | null>(null);
	let loading = $state(true);

	// All groups — used for the reparent + rename forms and to offer a
	// parent picker that excludes the node itself.
	let allGroups = $state<Group[]>([]);

	// Settings (rename — name/description only; slug is frozen).
	let editName = $state('');
	let editDescription = $state('');
	let savingSettings = $state(false);
	let settingsError = $state<string | null>(null);

	// Reparent.
	let reparentTarget = $state('');
	let reparenting = $state(false);
	let reparentError = $state<string | null>(null);

	// Delete.
	let confirmingDelete = $state(false);
	let deleting = $state(false);
	let deleteError = $state<string | null>(null);

	// Members.
	let members = $state<GroupMember[]>([]);
	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<GroupMember | 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 loadGroup() {
		loading = true;
		loadError = null;
		try {
			const fetched = await api.groups.get(slug);
			group = fetched;
			editName = fetched.name;
			editDescription = fetched.description ?? '';
			reparentTarget = fetched.parent_id ?? '';
			const loaders: Promise<unknown>[] = [loadAllGroups()];
			if (canManage) {
				loaders.push(loadMembers(slug), loadEligibleUsers());
			}
			await Promise.all(loaders);
		} catch (e) {
			loadError = e instanceof Error ? e.message : String(e);
		} finally {
			loading = false;
		}
	}

	async function loadAllGroups() {
		try {
			allGroups = await api.groups.list();
		} catch {
			allGroups = [];
		}
	}

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

	async function loadEligibleUsers() {
		eligibleLoadError = null;
		try {
			const all = await api.admins.list();
			eligibleUsers = all.filter((u) => u.is_active && u.instance_role === 'member');
		} catch (e) {
			eligibleUsers = [];
			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))
	);

	// Reparent picker options: every group except this node (a group
	// can't parent itself; the backend also rejects descendant cycles
	// with a 409, surfaced below).
	const reparentOptions = $derived(allGroups.filter((g) => g.id !== group?.id));

	async function saveSettings(event: Event) {
		event.preventDefault();
		if (!group) return;
		savingSettings = true;
		settingsError = null;
		try {
			const updated = await api.groups.update(group.id, {
				name: editName.trim() !== group.name ? editName.trim() : undefined,
				description:
					editDescription !== (group.description ?? '')
						? editDescription || null
						: undefined
			});
			group = { ...group, name: updated.name, description: updated.description };
		} catch (e) {
			settingsError = e instanceof Error ? e.message : String(e);
		} finally {
			savingSettings = false;
		}
	}

	async function submitReparent(event: Event) {
		event.preventDefault();
		if (!group) return;
		reparenting = true;
		reparentError = null;
		try {
			await api.groups.reparent(group.id, reparentTarget.trim() || null);
			await loadGroup();
		} catch (e) {
			reparentError =
				e instanceof ApiError && e.status === 409
					? e.message
					: e instanceof Error
						? e.message
						: String(e);
		} finally {
			reparenting = false;
		}
	}

	function askDelete() {
		deleteError = null;
		confirmingDelete = true;
	}

	async function confirmDelete() {
		if (!group) return;
		deleting = true;
		deleteError = null;
		try {
			await api.groups.delete(group.id);
			await goto(`${base}/groups`);
		} catch (e) {
			// 409 RESTRICT: the group still has subgroups or apps. Surface
			// the server's message cleanly rather than a bare status code.
			deleteError = e instanceof Error ? e.message : String(e);
		} finally {
			deleting = false;
		}
	}

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

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

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

	async function confirmRemoveMember() {
		if (!group || !memberToRemove) return;
		removingMember = true;
		removeMemberError = null;
		try {
			await api.groups.members.remove(group.id, memberToRemove.user_id);
			memberToRemove = null;
			await loadMembers(group.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;
		}
	}

	$effect(() => {
		void slug;
		void loadGroup();
	});

	// A viewer following a stale link to a manage-only tab gets bounced
	// to the overview. The backend still 403s the underlying calls.
	$effect(() => {
		if (!canManage && (activeTab === 'settings' || activeTab === 'members')) {
			void goto(`${base}/groups/${slug}?tab=overview`, { replaceState: true });
		}
	});
</script>

<header class="page-head">
	<div class="breadcrumb">
		<a href="{base}/groups">Groups</a>
		{#if group}
			{#each group.path as p (p.id)}
				<span aria-hidden="true">/</span>
				{#if p.id === group.id}
					<code>{p.slug}</code>
				{:else}
					<a href="{base}/groups/{p.slug}">{p.name}</a>
				{/if}
			{/each}
		{:else}
			<span aria-hidden="true">/</span>
			<code>{slug}</code>
		{/if}
	</div>
	{#if group}
		<h1>{group.name}</h1>
		{#if group.description}<p class="muted">{group.description}</p>{/if}
	{/if}
</header>

{#if group}
	<nav class="tabbar">
		<a href="{base}/groups/{slug}?tab=overview" class:active={activeTab === 'overview'}>Overview</a>
		{#if canManage}
			<a href="{base}/groups/{slug}?tab=members" class:active={activeTab === 'members'}>Members</a>
			<a href="{base}/groups/{slug}?tab=settings" class:active={activeTab === 'settings'}>Settings</a>
		{/if}
	</nav>

	{#if activeTab === 'overview'}
		<section>
			<h2>Subgroups</h2>
			{#if group.subgroups.length === 0}
				<p class="muted">No subgroups.</p>
			{:else}
				<ul class="list">
					{#each group.subgroups as sub (sub.id)}
						<li>
							<a href="{base}/groups/{sub.slug}">
								<div class="primary">
									<strong>{sub.name}</strong>
									<span class="muted">/{sub.slug}</span>
								</div>
								<div class="secondary muted">{sub.description ?? '—'}</div>
							</a>
						</li>
					{/each}
				</ul>
			{/if}

			<h2>Apps</h2>
			{#if group.apps.length === 0}
				<p class="muted">No apps in this group.</p>
			{:else}
				<ul class="list">
					{#each group.apps as app (app.id)}
						<li>
							<a href="{base}/apps/{app.slug}">
								<div class="primary">
									<strong>{app.name}</strong>
									<span class="muted">/{app.slug}</span>
								</div>
								<div class="secondary muted">{app.description ?? '—'}</div>
							</a>
						</li>
					{/each}
				</ul>
			{/if}
		</section>
	{:else if activeTab === 'members' && canManage}
		<section>
			<h2>Members</h2>
			<p class="muted">
				Users with explicit access to this group. 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 group 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>Group 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 group',
											danger: true,
											onClick: () => askRemoveMember(m)
										}
									]}
								/>
							</div>
						</div>
					{/each}
				</div>
			{/if}
		</section>
	{:else if activeTab === 'settings' && canManage}
		<section>
			<h2>Settings</h2>
			<form class="create-form" onsubmit={saveSettings}>
				<label>
					<span>Name</span>
					<input bind:value={editName} required />
				</label>
				<label>
					<span>Description</span>
					<input bind:value={editDescription} />
				</label>
				<p class="muted small">
					The slug <code>/{group.slug}</code> is frozen and cannot be changed.
				</p>
				{#if settingsError}
					<div class="error">{settingsError}</div>
				{/if}
				<div class="actions">
					<button type="submit" disabled={savingSettings}>
						{savingSettings ? 'Saving…' : 'Save changes'}
					</button>
				</div>
			</form>

			<h2>Move group</h2>
			<form class="create-form" onsubmit={submitReparent}>
				<label>
					<span>Parent group</span>
					<select bind:value={reparentTarget}>
						<option value="">— root —</option>
						{#each reparentOptions as g (g.id)}
							<option value={g.id}>{g.name} (/{g.slug})</option>
						{/each}
					</select>
				</label>
				{#if reparentError}
					<div class="error">{reparentError}</div>
				{/if}
				<div class="actions">
					<button type="submit" disabled={reparenting}>
						{reparenting ? 'Moving…' : 'Move group'}
					</button>
				</div>
			</form>

			<div class="danger-zone">
				<h3>Delete group</h3>
				<p class="muted">
					Removes this group. The group must be empty first — it can't have any
					subgroups or apps.
				</p>
				<button type="button" class="danger" onclick={askDelete}>Delete group</button>
			</div>
		</section>
	{/if}

	{#if confirmingDelete}
		<ConfirmModal
			title="Delete group “{group.name}”"
			variant="danger"
			confirmLabel="Delete group"
			busyLabel="Deleting…"
			busy={deleting}
			onConfirm={confirmDelete}
			onCancel={() => (confirmingDelete = false)}
		>
			<p>
				This permanently removes the group <strong>{group.name}</strong>. The
				group must be empty — if it still has subgroups or apps, the delete is
				rejected and the reason appears below.
			</p>
			{#if deleteError}
				<p class="modal-error">{deleteError}</p>
			{/if}
		</ConfirmModal>
	{/if}

	{#if memberToRemove}
		<ConfirmModal
			title="Remove {memberToRemove.username} from {group.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
				group. Their other memberships and account are untouched.
			</p>
			{#if removeMemberError}
				<p class="modal-error">{removeMemberError}</p>
			{/if}
		</ConfirmModal>
	{/if}
{:else if loading}
	<p class="muted">Loading…</p>
{:else if loadError}
	<p class="error">{loadError}</p>
{/if}

<style>
	.page-head {
		margin-bottom: 1rem;
	}
	.breadcrumb {
		display: flex;
		flex-wrap: wrap;
		gap: 0.4rem;
		align-items: baseline;
		font-size: 0.875rem;
		color: var(--text-muted);
	}
	.breadcrumb a {
		color: var(--color-link);
		text-decoration: none;
	}
	.breadcrumb code {
		background: var(--bg-elevated);
		padding: 0.1rem 0.35rem;
		border-radius: var(--radius-sm);
	}
	h1 {
		margin: 0.25rem 0 0.25rem;
		font-size: 1.5rem;
	}

	h2 {
		font-size: 1.125rem;
		margin: 1.5rem 0 1rem;
	}

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

	.tabbar {
		display: flex;
		gap: 1.25rem;
		border-bottom: 1px solid #1e293b;
		margin-bottom: 1.5rem;
	}

	.tabbar a {
		color: #94a3b8;
		text-decoration: none;
		font-size: 0.9rem;
		padding: 0.5rem 0;
		border-bottom: 2px solid transparent;
	}

	.tabbar a:hover {
		color: #e2e8f0;
	}

	.tabbar a.active {
		color: #e2e8f0;
		border-bottom-color: #38bdf8;
	}

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

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

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

	.muted {
		color: #64748b;
	}

	.small {
		font-size: 0.85rem;
	}

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

	.modal-error {
		color: #fca5a5;
	}

	.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 .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 .row > label.grow {
		grid-column: span 2;
	}

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

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

	.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;
	}

	.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;
	}

	.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;
	}
</style>
