<script lang="ts">
	import { getContext, onMount } from 'svelte';

	const i18n = getContext('i18n');

	import { getGroups, getGroupById, getGroupInfoById } from '$lib/apis/groups';
	import { getUserInfoById } from '$lib/apis/users';
	import { WEBUI_API_BASE_URL } from '$lib/constants';
	import XMark from '$lib/components/icons/XMark.svelte';
	import Badge from '$lib/components/common/Badge.svelte';
	import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
	import Plus from '$lib/components/icons/Plus.svelte';
	import AddAccessModal from './AddAccessModal.svelte';
	import Tooltip from '$lib/components/common/Tooltip.svelte';
	import Switch from '$lib/components/common/Switch.svelte';

	type AccessGrant = {
		id?: string;
		principal_type: 'user' | 'group';
		principal_id: string;
		permission: 'read' | 'write';
	};

	type LegacyAccessControl = {
		read: { group_ids: string[]; user_ids: string[] };
		write: { group_ids: string[]; user_ids: string[] };
	};

	export let onChange: Function = () => {};

	export let accessRoles = ['read'];
	export let accessGrants: AccessGrant[] | any = [];
	export let accessControl: any = undefined;

	export let share = true;
	export let sharePublic = true;
	export let shareUsers = true;

	let groups: any[] = [];
	const resolvingGroupIds = new Set<string>();
	let userById: Record<string, any> = {};
	const resolvingUserIds = new Set<string>();

	let showAddAccessModal = false;

	const dedupeAccessGrants = (grants: AccessGrant[] | null | undefined): AccessGrant[] => {
		if (!Array.isArray(grants)) return [];
		const map = new Map<string, AccessGrant>();
		for (const grant of grants) {
			if (!grant) continue;
			const key = `${grant.principal_type}:${grant.principal_id}:${grant.permission}`;
			if (!grant.principal_type || !grant.principal_id || !grant.permission) continue;
			map.set(key, {
				id: grant.id,
				principal_type: grant.principal_type,
				principal_id: grant.principal_id,
				permission: grant.permission
			});
		}
		return Array.from(map.values());
	};

	const legacyAccessControlToGrants = (accessControl: any): AccessGrant[] => {
		if (accessControl === null) {
			return [
				{
					principal_type: 'user',
					principal_id: '*',
					permission: 'read'
				}
			];
		}

		if (!accessControl || typeof accessControl !== 'object') {
			return [];
		}

		const grants: AccessGrant[] = [];
		for (const permission of ['read', 'write'] as const) {
			const entry = accessControl?.[permission] ?? {};
			for (const groupId of entry?.group_ids ?? []) {
				grants.push({
					principal_type: 'group',
					principal_id: groupId,
					permission
				});
			}
			for (const userId of entry?.user_ids ?? []) {
				grants.push({
					principal_type: 'user',
					principal_id: userId,
					permission
				});
			}
		}

		return dedupeAccessGrants(grants);
	};

	const grantsToLegacyAccessControl = (grants: AccessGrant[]): null | LegacyAccessControl => {
		const normalized = dedupeAccessGrants(grants);
		if (hasPublicReadGrant(normalized)) {
			return null;
		}

		const result: LegacyAccessControl = {
			read: { group_ids: [], user_ids: [] },
			write: { group_ids: [], user_ids: [] }
		};

		for (const grant of normalized) {
			if (!['read', 'write'].includes(grant.permission)) {
				continue;
			}

			if (grant.principal_type === 'group') {
				if (!result[grant.permission].group_ids.includes(grant.principal_id)) {
					result[grant.permission].group_ids = [
						...result[grant.permission].group_ids,
						grant.principal_id
					];
				}
			} else if (grant.principal_type === 'user' && grant.principal_id !== '*') {
				if (!result[grant.permission].user_ids.includes(grant.principal_id)) {
					result[grant.permission].user_ids = [
						...result[grant.permission].user_ids,
						grant.principal_id
					];
				}
			}
		}

		return result;
	};

	const normalizeInputToGrants = (value: any): AccessGrant[] => {
		if (value === null) {
			return legacyAccessControlToGrants(null);
		}
		if (Array.isArray(value)) {
			return dedupeAccessGrants(value);
		}
		if (value && typeof value === 'object' && ('read' in value || 'write' in value)) {
			return legacyAccessControlToGrants(value);
		}
		return [];
	};

	const stableStringify = (value: any): string => {
		try {
			return JSON.stringify(value ?? null);
		} catch {
			return '';
		}
	};

	const hasPublicReadGrant = (grants: AccessGrant[]): boolean =>
		grants.some(
			(grant) =>
				grant.principal_type === 'user' && grant.principal_id === '*' && grant.permission === 'read'
		);

	const hasPublicWriteGrant = (grants: AccessGrant[]): boolean =>
		grants.some(
			(grant) =>
				grant.principal_type === 'user' &&
				grant.principal_id === '*' &&
				grant.permission === 'write'
		);

	const currentGrants = (): AccessGrant[] =>
		Array.isArray(accessGrants) ? (accessGrants as AccessGrant[]) : [];

	const getPrincipalIdsByPermission = (
		principalType: 'user' | 'group',
		permission: 'read' | 'write'
	): string[] =>
		Array.from(
			new Set(
				currentGrants()
					.filter(
						(grant) => grant.principal_type === principalType && grant.permission === permission
					)
					.map((grant) => grant.principal_id)
			)
		);

	const hasPrincipalGrant = (
		principalType: 'user' | 'group',
		principalId: string,
		permission: 'read' | 'write'
	): boolean =>
		currentGrants().some(
			(grant) =>
				grant.principal_type === principalType &&
				grant.principal_id === principalId &&
				grant.permission === permission
		);

	const commitAccessGrants = (nextGrants: AccessGrant[]) => {
		accessGrants = dedupeAccessGrants(nextGrants);
		onChange(accessGrants);
	};

	const setPublic = (isPublic: boolean) => {
		// Remove all user:* grants
		const filtered = currentGrants().filter(
			(grant) => !(grant.principal_type === 'user' && grant.principal_id === '*')
		);
		if (isPublic) {
			filtered.push({
				principal_type: 'user',
				principal_id: '*',
				permission: 'read'
			});
		}
		commitAccessGrants(filtered);
	};

	const togglePublicWrite = () => {
		let next = [...currentGrants()];
		if (hasPublicWriteGrant(next)) {
			next = next.filter(
				(grant) =>
					!(
						grant.principal_type === 'user' &&
						grant.principal_id === '*' &&
						grant.permission === 'write'
					)
			);
		} else {
			next = upsertPrincipalGrant('user', '*', 'write', next);
		}
		commitAccessGrants(next);
	};

	const upsertPrincipalGrant = (
		principalType: 'user' | 'group',
		principalId: string,
		permission: 'read' | 'write',
		grants: AccessGrant[]
	): AccessGrant[] => {
		if (
			grants.some(
				(grant) =>
					grant.principal_type === principalType &&
					grant.principal_id === principalId &&
					grant.permission === permission
			)
		) {
			return grants;
		}
		return [
			...grants,
			{
				principal_type: principalType,
				principal_id: principalId,
				permission
			}
		];
	};

	const removePrincipalGrant = (
		principalType: 'user' | 'group',
		principalId: string,
		permission: 'read' | 'write',
		grants: AccessGrant[]
	): AccessGrant[] =>
		grants.filter(
			(grant) =>
				!(
					grant.principal_type === principalType &&
					grant.principal_id === principalId &&
					grant.permission === permission
				)
		);

	const removePrincipal = (principalType: 'user' | 'group', principalId: string) => {
		let next = [...currentGrants()];
		next = removePrincipalGrant(principalType, principalId, 'read', next);
		next = removePrincipalGrant(principalType, principalId, 'write', next);
		commitAccessGrants(next);
	};

	const togglePrincipalWrite = (principalType: 'user' | 'group', principalId: string) => {
		let next = [...currentGrants()];
		const hasWrite = hasPrincipalGrant(principalType, principalId, 'write');
		if (hasWrite) {
			next = removePrincipalGrant(principalType, principalId, 'write', next);
		} else {
			next = upsertPrincipalGrant(principalType, principalId, 'read', next);
			next = upsertPrincipalGrant(principalType, principalId, 'write', next);
		}
		commitAccessGrants(next);
	};

	const ensureUsersByIds = async (userIds: string[]) => {
		const pendingIds = userIds.filter((id) => !userById[id] && !resolvingUserIds.has(id));
		if (!pendingIds.length) return;

		for (const id of pendingIds) {
			resolvingUserIds.add(id);
		}

		const fetched = await Promise.all(
			pendingIds.map(async (id) => {
				const user = await getUserInfoById(localStorage.token, id).catch((error) => {
					console.error(error);
					return null;
				});
				return { id, user };
			})
		);

		const nextUserById = { ...userById };
		for (const item of fetched) {
			if (item.user?.id) {
				nextUserById[item.id] = item.user;
			}
			resolvingUserIds.delete(item.id);
		}
		userById = nextUserById;
	};

	const handleAddAccess = ({ userIds, groupIds }: { userIds: string[]; groupIds: string[] }) => {
		let next = [...currentGrants()];

		for (const groupId of groupIds) {
			next = upsertPrincipalGrant('group', groupId, 'read', next);
		}
		for (const userId of userIds) {
			next = upsertPrincipalGrant('user', userId, 'read', next);
		}
		commitAccessGrants(next);
	};

	// NOTE: We must reference `accessGrants` directly in each reactive
	// expression so Svelte tracks the dependency.
	const ensureGroupsByIds = async (groupIds: string[]) => {
		const pendingIds = groupIds.filter(
			(id) => !groups.find((g) => g.id === id) && !resolvingGroupIds.has(id)
		);
		if (!pendingIds.length) return;

		for (const id of pendingIds) {
			resolvingGroupIds.add(id);
		}

		const fetched = await Promise.all(
			pendingIds.map(async (id) => {
				const group = await getGroupInfoById(localStorage.token, id).catch((error) => {
					console.error(error);
					return null;
				});
				return group;
			})
		);

		const newGroups = fetched.filter((g) => g);
		if (newGroups.length > 0) {
			groups = [...groups, ...newGroups].filter(
				(g, index, self) => index === self.findIndex((t) => t.id === g.id)
			);
		}

		for (const id of pendingIds) {
			resolvingGroupIds.delete(id);
		}
	};

	$: if (readGroupIds.length > 0 || writeGroupIds.length > 0) {
		void ensureGroupsByIds([...readGroupIds, ...writeGroupIds]);
	}
	$: readGroupIds = (accessGrants, getPrincipalIdsByPermission('group', 'read'));
	$: writeGroupIds = (accessGrants, getPrincipalIdsByPermission('group', 'write'));
	$: readUserIds =
		(accessGrants, getPrincipalIdsByPermission('user', 'read').filter((id) => id !== '*'));
	$: writeUserIds =
		(accessGrants, getPrincipalIdsByPermission('user', 'write').filter((id) => id !== '*'));

	$: selectedUserIds = Array.from(new Set([...readUserIds, ...writeUserIds]));

	$: selectedUsers = selectedUserIds
		.map((id) => {
			return userById[id] ?? { id, name: id, email: '' };
		})
		.sort((a, b) => a.name.localeCompare(b.name));

	$: accessGroups = groups
		.filter((group) => readGroupIds.includes(group.id) || writeGroupIds.includes(group.id))
		.sort((a, b) => a.name.localeCompare(b.name));

	$: if (selectedUserIds.length > 0) {
		void ensureUsersByIds(selectedUserIds);
	}

	$: {
		if (accessControl !== undefined) {
			const normalizedGrants = normalizeInputToGrants(accessControl);
			if (stableStringify(normalizedGrants) !== stableStringify(accessGrants)) {
				accessGrants = normalizedGrants;
			}
		}
	}

	$: {
		const normalizedGrants = normalizeInputToGrants(accessGrants);
		if (stableStringify(normalizedGrants) !== stableStringify(accessGrants)) {
			accessGrants = normalizedGrants;
		}

		if (accessControl !== undefined) {
			const nextAccessControl = grantsToLegacyAccessControl(normalizedGrants);
			if (stableStringify(nextAccessControl) !== stableStringify(accessControl)) {
				accessControl = nextAccessControl;
			}
		}
	}

	onMount(async () => {
		console.log('AccessControl mounted', { accessGrants, accessControl });
		const res = await getGroups(localStorage.token, true).catch((error) => {
			console.error(error);
			return [];
		});

		console.log('getGroups res', res);

		groups = [...groups, ...res].filter(
			(g, index, self) => index === self.findIndex((t) => t.id === g.id)
		);
	});

	$: console.log('AccessControl state', {
		accessGrants,
		readGroupIds,
		writeGroupIds,
		selectedUserIds,
		groups,
		accessGroups,
		selectedUsers
	});
</script>

<AddAccessModal bind:show={showAddAccessModal} {shareUsers} onAdd={handleAddAccess} />

<div class=" rounded-lg flex flex-col gap-1">
	<div class="py-2">
		<div class="flex gap-2.5 items-center">
			<div>
				<div class=" p-2 bg-black/5 dark:bg-white/5 rounded-full">
					{#if !hasPublicReadGrant(accessGrants ?? [])}
						<svg
							xmlns="http://www.w3.org/2000/svg"
							fill="none"
							viewBox="0 0 24 24"
							stroke-width="1.5"
							stroke="currentColor"
							class="w-5 h-5"
						>
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
								d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
							/>
						</svg>
					{:else}
						<svg
							xmlns="http://www.w3.org/2000/svg"
							fill="none"
							viewBox="0 0 24 24"
							stroke-width="1.5"
							stroke="currentColor"
							class="w-5 h-5"
						>
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
								d="M6.115 5.19l.319 1.913A6 6 0 008.11 10.36L9.75 12l-.387.775c-.217.433-.132.956.21 1.298l1.348 1.348c.21.21.329.497.329.795v1.089c0 .426.24.815.622 1.006l.153.076c.433.217.956.132 1.298-.21l.723-.723a8.7 8.7 0 002.288-4.042 1.087 1.087 0 00-.358-1.099l-1.33-1.108c-.251-.21-.582-.299-.905-.245l-1.17.195a1.125 1.125 0 01-.98-.314l-.295-.295a1.125 1.125 0 010-1.591l.13-.132a1.125 1.125 0 011.3-.21l.603.302a.809.809 0 001.086-1.086L14.25 7.5l1.256-.837a4.5 4.5 0 001.528-1.732l.146-.292M6.115 5.19A9 9 0 1017.18 4.64M6.115 5.19A8.965 8.965 0 0112 3c1.929 0 3.716.607 5.18 1.64"
							/>
						</svg>
					{/if}
				</div>
			</div>

			<div>
				<Tooltip
					content={!(share && sharePublic) && !hasPublicReadGrant(accessGrants ?? [])
						? $i18n.t('You do not have permission to make this public')
						: ''}
				>
					<select
						id="models"
						class="outline-none bg-transparent text-sm font-medium block w-fit pr-10 max-w-full placeholder-gray-400"
						value={!hasPublicReadGrant(accessGrants ?? []) ? 'private' : 'public'}
						on:change={(e) => {
							setPublic((e.target as HTMLSelectElement).value === 'public');
						}}
					>
						<option class=" text-gray-700" value="private">{$i18n.t('Private')}</option>
						{#if (share && sharePublic) || hasPublicReadGrant(accessGrants ?? [])}
							<option class=" text-gray-700" value="public">{$i18n.t('Public')}</option>
						{/if}
					</select>
				</Tooltip>

				<div class=" text-xs text-gray-400 font-medium">
					{#if !hasPublicReadGrant(accessGrants ?? [])}
						{$i18n.t('Only select users and groups with permission can access')}
					{:else}
						{$i18n.t('Accessible to all users')}
					{/if}
				</div>
			</div>
		</div>

		{#if hasPublicReadGrant(accessGrants ?? []) && accessRoles.includes('write')}
			<div class="flex w-full justify-between mt-2 ml-0.5">
				<div class="self-center text-xs">
					{$i18n.t('Allow public write access')}
				</div>
				<Switch
					state={hasPublicWriteGrant(accessGrants ?? [])}
					on:change={() => {
						togglePublicWrite();
					}}
				/>
			</div>
		{/if}
	</div>

	{#if share}
		<div class="flex items-center justify-between text-xs font-medium text-gray-500 my-1">
			<div>
				{$i18n.t('Access List')}
			</div>
			<div class="flex gap-1">
				<button
					class="px-2 py-1 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition text-xs font-medium flex items-center gap-1"
					type="button"
					on:click={() => {
						showAddAccessModal = true;
					}}
				>
					<Plus className="size-3" />
					{$i18n.t('Add Access')}
				</button>
			</div>
		</div>

		<!-- List -->
		<div class="flex flex-col gap-2">
			<!-- Groups -->
			{#each accessGroups as group}
				<div class="flex items-center gap-3 justify-between text-sm w-full transition pb-1">
					<div class="flex items-center gap-2 w-full flex-1">
						<!-- Placeholder for group icon vs user icon -->
						<div
							class="size-5 rounded-full bg-gray-100 dark:bg-gray-850 flex items-center justify-center text-xs"
						>
							{group.name.charAt(0).toUpperCase()}
						</div>

						<div class="truncate text-sm flex items-center gap-2">
							{group.name}
							<span class="text-xs text-gray-400 font-normal"
								>{group?.member_count} {$i18n.t('members')}</span
							>
						</div>
					</div>

					<div class="w-full flex justify-end items-center gap-2">
						<button
							type="button"
							on:click={() => {
								if (accessRoles.includes('write')) {
									togglePrincipalWrite('group', group.id);
								}
							}}
						>
							{#if writeGroupIds.includes(group.id)}
								<Badge type={'success'} content={$i18n.t('Write')} />
							{:else}
								<Badge type={'info'} content={$i18n.t('Read')} />
							{/if}
						</button>

						<button
							class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
							type="button"
							on:click={() => {
								removePrincipal('group', group.id);
							}}
						>
							<XMark className="size-4" />
						</button>
					</div>
				</div>
			{/each}

			<!-- Users -->
			{#if shareUsers}
				{#each selectedUsers as user}
					<div
						class="flex items-center gap-3 justify-between text-sm w-full transition border-b border-gray-50 dark:border-gray-850 pb-2 last:border-0"
					>
						<div class="flex items-center gap-2 w-full flex-1">
							<img
								class="rounded-full size-5 object-cover"
								src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
								alt={user.name ?? user.id}
							/>
							<div class="w-full">
								<Tooltip content={user.email} placement="top-start">
									<div class="truncate text-sm">{user.name ?? user.id}</div>
								</Tooltip>
							</div>
						</div>

						<div class="w-full flex justify-end items-center gap-2">
							<button
								type="button"
								on:click={() => {
									if (accessRoles.includes('write')) {
										togglePrincipalWrite('user', user.id);
									}
								}}
							>
								{#if writeUserIds.includes(user.id)}
									<Badge type={'success'} content={$i18n.t('Write')} />
								{:else}
									<Badge type={'info'} content={$i18n.t('Read')} />
								{/if}
							</button>

							<button
								class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
								type="button"
								on:click={() => {
									removePrincipal('user', user.id);
								}}
							>
								<XMark className="size-4" />
							</button>
						</div>
					</div>
				{/each}
			{/if}

			{#if !hasPublicReadGrant(accessGrants ?? []) && accessGroups.length === 0 && selectedUsers.length === 0}
				<div class="text-xs text-gray-500 text-center py-4">
					{$i18n.t('No access grants. Private to you.')}
				</div>
			{/if}
		</div>
	{/if}
</div>
