import { mkdir, readdir, unlink } from "node:fs/promises";
import * as Automerge from "@automerge/automerge";

const STORAGE_DIR = process.env.STORAGE_DIR || "./data/communities";

export type SpaceVisibility = 'public' | 'public_read' | 'authenticated' | 'members_only';

// ── Nest Permissions & Policy ──

export interface NestPermissions {
	read: boolean;
	write: boolean;
	addShapes: boolean;
	deleteShapes: boolean;
	reshare: boolean;
	expiry?: number;                    // unix timestamp — auto-revoke after this time
}

export interface NestNotifications {
	onNestRequest: boolean;
	onNestCreated: boolean;
	onNestRevoked: boolean;
	onReshare: boolean;
	channel: 'inbox' | 'email' | 'both';
}

export interface NestPolicy {
	consent: 'open' | 'members' | 'approval' | 'closed';
	notifications: NestNotifications;
	defaultPermissions: NestPermissions;
	allowlist?: string[];               // slugs that bypass consent
	blocklist?: string[];               // slugs that are always denied
}

// ── SpaceRef: Nesting Primitive ──

export interface SpaceRefFilter {
	shapeTypes?: string[];
	shapeIds?: string[];
	tags?: string[];
	moduleIds?: string[];
}

export interface SpaceRef {
	id: string;
	sourceSlug: string;
	sourceDID?: string;
	filter?: SpaceRefFilter;
	x: number;
	y: number;
	width: number;
	height: number;
	rotation: number;
	permissions: NestPermissions;
	collapsed?: boolean;
	label?: string;
	createdAt: number;
	createdBy: string;                  // DID of who created this nesting
}

export type NestRequestStatus = 'pending' | 'approved' | 'denied';

export interface PendingNestRequest {
	id: string;
	sourceSlug: string;
	targetSlug: string;
	requestedBy: string;                // DID
	requestedPermissions: NestPermissions;
	message?: string;
	status: NestRequestStatus;
	createdAt: number;
	resolvedAt?: number;
	resolvedBy?: string;
	modifiedPermissions?: NestPermissions;
}

// ── Default Nest Policies ──

export const DEFAULT_USER_NEST_POLICY: NestPolicy = {
	consent: 'approval',
	notifications: {
		onNestRequest: true,
		onNestCreated: true,
		onNestRevoked: false,
		onReshare: true,
		channel: 'inbox',
	},
	defaultPermissions: {
		read: true,
		write: false,
		addShapes: false,
		deleteShapes: false,
		reshare: false,
	},
};

export const DEFAULT_COMMUNITY_NEST_POLICY: NestPolicy = {
	consent: 'members',
	notifications: {
		onNestRequest: false,
		onNestCreated: true,
		onNestRevoked: true,
		onReshare: false,
		channel: 'inbox',
	},
	defaultPermissions: {
		read: true,
		write: true,
		addShapes: true,
		deleteShapes: false,
		reshare: true,
	},
};

export const DEFAULT_USER_MODULES = ['canvas', 'notes', 'files', 'wallet'];

// ── Core Types ──

export interface CommunityMeta {
	name: string;
	slug: string;
	createdAt: string;
	visibility: SpaceVisibility;
	ownerDID: string | null;
	enabledModules?: string[];                              // null = all enabled
	moduleScopeOverrides?: Record<string, 'space' | 'global'>;
	description?: string;
	avatar?: string;
	nestPolicy?: NestPolicy;
	encrypted?: boolean;
	encryptionKeyId?: string;
}

export interface ShapeData {
	type: string;
	id: string;
	x: number;
	y: number;
	width: number;
	height: number;
	rotation: number;
	content?: string;
	sourceId?: string;
	targetId?: string;
	[key: string]: unknown; // Allow additional shape-specific properties
}

export interface SpaceMember {
	did: string;
	role: 'viewer' | 'participant' | 'moderator' | 'admin';
	joinedAt: number;
	displayName?: string;
}

export interface CommunityDoc {
	meta: CommunityMeta;
	shapes: {
		[id: string]: ShapeData;
	};
	members: {
		[did: string]: SpaceMember;
	};
	nestedSpaces: {
		[refId: string]: SpaceRef;
	};
}

// Per-peer sync state for Automerge
interface PeerState {
	syncState: Automerge.SyncState;
	lastActivity: number;
}

// In-memory cache of Automerge documents
const communities = new Map<string, Automerge.Doc<CommunityDoc>>();

// Track sync state per peer (WebSocket connection)
const peerSyncStates = new Map<string, Map<string, PeerState>>();

// Debounce save timers
const saveTimers = new Map<string, Timer>();

// Ensure storage directory exists
await mkdir(STORAGE_DIR, { recursive: true });

/**
 * Load community document from disk
 */
export async function loadCommunity(slug: string): Promise<Automerge.Doc<CommunityDoc> | null> {
	// Check cache first
	if (communities.has(slug)) {
		return communities.get(slug)!;
	}

	// Try to load Automerge binary first
	const binaryPath = `${STORAGE_DIR}/${slug}.automerge`;
	const binaryFile = Bun.file(binaryPath);

	if (await binaryFile.exists()) {
		try {
			const buffer = await binaryFile.arrayBuffer();
			let bytes = new Uint8Array(buffer);

			// Check for encrypted magic bytes
			if (bytes.length >= ENCRYPTED_MAGIC.length &&
				bytes[0] === ENCRYPTED_MAGIC[0] &&
				bytes[1] === ENCRYPTED_MAGIC[1] &&
				bytes[2] === ENCRYPTED_MAGIC[2] &&
				bytes[3] === ENCRYPTED_MAGIC[3]) {
				// Encrypted file: extract keyId length (4 bytes), keyId, then ciphertext
				const keyIdLen = new DataView(bytes.buffer, bytes.byteOffset + 4, 4).getUint32(0);
				const keyId = new TextDecoder().decode(bytes.slice(8, 8 + keyIdLen));
				const ciphertext = bytes.slice(8 + keyIdLen);
				const key = await deriveSpaceKey(keyId);
				bytes = new Uint8Array(await decryptBinary(ciphertext, key));
				console.log(`[Store] Decrypted ${slug} (keyId: ${keyId})`);
			}

			const doc = Automerge.load<CommunityDoc>(bytes);
			communities.set(slug, doc);
			return doc;
		} catch (e) {
			console.error(`Failed to load Automerge doc for ${slug}:`, e);
		}
	}

	// Fallback: try JSON format and migrate
	const jsonPath = `${STORAGE_DIR}/${slug}.json`;
	const jsonFile = Bun.file(jsonPath);

	if (await jsonFile.exists()) {
		try {
			const data = (await jsonFile.json()) as CommunityDoc;
			// Migrate JSON to Automerge
			const doc = jsonToAutomerge(data);
			communities.set(slug, doc);
			// Save as Automerge binary
			await saveCommunity(slug);
			return doc;
		} catch (e) {
			console.error(`Failed to migrate JSON for ${slug}:`, e);
			return null;
		}
	}

	return null;
}

/**
 * Convert JSON document to Automerge document
 */
function jsonToAutomerge(data: CommunityDoc): Automerge.Doc<CommunityDoc> {
	let doc = Automerge.init<CommunityDoc>();
	doc = Automerge.change(doc, "Import from JSON", (d) => {
		d.meta = { ...data.meta };
		d.shapes = {};
		for (const [id, shape] of Object.entries(data.shapes || {})) {
			d.shapes[id] = { ...shape };
		}
		d.members = {};
		for (const [did, member] of Object.entries(data.members || {})) {
			d.members[did] = { ...member };
		}
		d.nestedSpaces = {};
		for (const [refId, ref] of Object.entries(data.nestedSpaces || {})) {
			d.nestedSpaces[refId] = { ...ref };
		}
	});
	return doc;
}

/**
 * Save community document to disk (debounced)
 */
export async function saveCommunity(slug: string): Promise<void> {
	const doc = communities.get(slug);
	if (!doc) return;

	// Clear existing timer
	const existingTimer = saveTimers.get(slug);
	if (existingTimer) {
		clearTimeout(existingTimer);
	}

	// Debounce saves to avoid excessive disk writes
	const timer = setTimeout(async () => {
		const currentDoc = communities.get(slug);
		if (!currentDoc) return;

		const binary = Automerge.save(currentDoc);
		const path = `${STORAGE_DIR}/${slug}.automerge`;

		// Encrypt at rest if the space has encryption enabled
		if (currentDoc.meta.encrypted && currentDoc.meta.encryptionKeyId) {
			try {
				const keyId = currentDoc.meta.encryptionKeyId;
				const key = await deriveSpaceKey(keyId);
				const ciphertext = await encryptBinary(binary, key);
				const keyIdBytes = new TextEncoder().encode(keyId);
				// Format: magic (4) + keyIdLen (4) + keyId + ciphertext
				const header = new Uint8Array(8 + keyIdBytes.length + ciphertext.length);
				header.set(ENCRYPTED_MAGIC, 0);
				new DataView(header.buffer).setUint32(4, keyIdBytes.length);
				header.set(keyIdBytes, 8);
				header.set(ciphertext, 8 + keyIdBytes.length);
				await Bun.write(path, header);
				console.log(`[Store] Saved ${slug} encrypted (${header.length} bytes, keyId: ${keyId})`);
			} catch (e) {
				// Fallback to unencrypted if encryption fails
				console.error(`[Store] Encryption failed for ${slug}, saving unencrypted:`, e);
				await Bun.write(path, binary);
			}
		} else {
			await Bun.write(path, binary);
			console.log(`[Store] Saved ${slug} (${binary.length} bytes)`);
		}
	}, 2000);

	saveTimers.set(slug, timer);
}

/**
 * Create a new community/space
 */
export async function createCommunity(
	name: string,
	slug: string,
	ownerDID: string | null = null,
	visibility: SpaceVisibility = 'public_read',
	options?: {
		enabledModules?: string[];
		nestPolicy?: NestPolicy;
		description?: string;
	},
): Promise<Automerge.Doc<CommunityDoc>> {
	let doc = Automerge.init<CommunityDoc>();
	doc = Automerge.change(doc, "Create community", (d) => {
		d.meta = {
			name,
			slug,
			createdAt: new Date().toISOString(),
			visibility,
			ownerDID,
		};
		if (options?.enabledModules) {
			d.meta.enabledModules = options.enabledModules;
		}
		if (options?.nestPolicy) {
			d.meta.nestPolicy = options.nestPolicy;
		}
		if (options?.description) {
			d.meta.description = options.description;
		}
		d.shapes = {};
		d.members = {};
		d.nestedSpaces = {};
		// If owner is known, add them as admin member
		if (ownerDID) {
			d.members[ownerDID] = {
				did: ownerDID,
				role: 'admin',
				joinedAt: Date.now(),
			};
		}
	});

	communities.set(slug, doc);
	await saveCommunity(slug);
	return doc;
}

/**
 * Delete a community/space — removes from memory and disk
 */
export async function deleteCommunity(slug: string): Promise<void> {
	// Cancel any pending debounce save timer
	const timer = saveTimers.get(slug);
	if (timer) {
		clearTimeout(timer);
		saveTimers.delete(slug);
	}

	// Remove from in-memory cache
	communities.delete(slug);

	// Remove all peer sync states for this slug
	peerSyncStates.delete(slug);

	// Delete files from disk
	const automerge = `${STORAGE_DIR}/${slug}.automerge`;
	const json = `${STORAGE_DIR}/${slug}.json`;
	try { await unlink(automerge); } catch { /* file may not exist */ }
	try { await unlink(json); } catch { /* file may not exist */ }

	console.log(`[Store] Deleted community ${slug}`);
}

/**
 * Update space metadata (name, visibility, description)
 */
export function updateSpaceMeta(
	slug: string,
	fields: {
		name?: string;
		visibility?: SpaceVisibility;
		description?: string;
		enabledModules?: string[];
		moduleScopeOverrides?: Record<string, 'space' | 'global'>;
	},
): boolean {
	const doc = communities.get(slug);
	if (!doc) return false;

	const newDoc = Automerge.change(doc, `Update space meta`, (d) => {
		if (fields.name !== undefined) d.meta.name = fields.name;
		if (fields.visibility !== undefined) d.meta.visibility = fields.visibility;
		if (fields.description !== undefined) d.meta.description = fields.description;
		if (fields.enabledModules !== undefined) d.meta.enabledModules = fields.enabledModules;
		if (fields.moduleScopeOverrides !== undefined) d.meta.moduleScopeOverrides = fields.moduleScopeOverrides;
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
	return true;
}

/**
 * Check if community exists
 */
export async function communityExists(slug: string): Promise<boolean> {
	if (communities.has(slug)) return true;

	const binaryPath = `${STORAGE_DIR}/${slug}.automerge`;
	const jsonPath = `${STORAGE_DIR}/${slug}.json`;

	const binaryFile = Bun.file(binaryPath);
	const jsonFile = Bun.file(jsonPath);

	return (await binaryFile.exists()) || (await jsonFile.exists());
}

/**
 * List all communities
 */
export async function listCommunities(): Promise<string[]> {
	try {
		const files = await readdir(STORAGE_DIR);
		const slugs = new Set<string>();

		for (const f of files) {
			if (f.endsWith(".automerge")) {
				slugs.add(f.replace(".automerge", ""));
			} else if (f.endsWith(".json")) {
				slugs.add(f.replace(".json", ""));
			}
		}

		return Array.from(slugs);
	} catch {
		return [];
	}
}

/**
 * Get or create sync state for a peer
 */
export function getPeerSyncState(slug: string, peerId: string): PeerState {
	if (!peerSyncStates.has(slug)) {
		peerSyncStates.set(slug, new Map());
	}

	const communityPeers = peerSyncStates.get(slug)!;

	if (!communityPeers.has(peerId)) {
		communityPeers.set(peerId, {
			syncState: Automerge.initSyncState(),
			lastActivity: Date.now(),
		});
	}

	const peerState = communityPeers.get(peerId)!;
	peerState.lastActivity = Date.now();
	return peerState;
}

/**
 * Remove peer sync state (on disconnect)
 */
export function removePeerSyncState(slug: string, peerId: string): void {
	const communityPeers = peerSyncStates.get(slug);
	if (communityPeers) {
		communityPeers.delete(peerId);
		if (communityPeers.size === 0) {
			peerSyncStates.delete(slug);
		}
	}
}

/**
 * Get all peer IDs for a community
 */
export function getCommunityPeers(slug: string): string[] {
	const communityPeers = peerSyncStates.get(slug);
	return communityPeers ? Array.from(communityPeers.keys()) : [];
}

/**
 * Process incoming sync message from a peer
 * Returns response message and messages for other peers
 */
export function receiveSyncMessage(
	slug: string,
	peerId: string,
	message: Uint8Array,
): {
	response: Uint8Array | null;
	broadcastToPeers: Map<string, Uint8Array>;
} {
	const doc = communities.get(slug);
	if (!doc) {
		return { response: null, broadcastToPeers: new Map() };
	}

	const peerState = getPeerSyncState(slug, peerId);

	// Apply incoming sync message
	const result = Automerge.receiveSyncMessage(
		doc,
		peerState.syncState,
		message
	);

	const newDoc = result[0];
	const newSyncState = result[1];

	communities.set(slug, newDoc);
	peerState.syncState = newSyncState;

	// Save if the document actually changed (Automerge 2.x receiveSyncMessage
	// returns null for patches, so detect changes via object identity instead)
	if (newDoc !== doc) {
		saveCommunity(slug);
	}

	// Generate response for this peer
	const [nextSyncState, responseMessage] = Automerge.generateSyncMessage(
		newDoc,
		peerState.syncState
	);
	peerState.syncState = nextSyncState;

	// Generate messages for other peers
	const broadcastToPeers = new Map<string, Uint8Array>();
	const communityPeers = peerSyncStates.get(slug);

	if (communityPeers && newDoc !== doc) {
		for (const [otherPeerId, otherPeerState] of communityPeers) {
			if (otherPeerId !== peerId) {
				const [newOtherSyncState, otherMessage] = Automerge.generateSyncMessage(
					newDoc,
					otherPeerState.syncState
				);
				otherPeerState.syncState = newOtherSyncState;

				if (otherMessage) {
					broadcastToPeers.set(otherPeerId, otherMessage);
				}
			}
		}
	}

	return {
		response: responseMessage || null,
		broadcastToPeers,
	};
}

/**
 * Generate initial sync message for a new peer
 */
export function generateSyncMessageForPeer(
	slug: string,
	peerId: string,
): Uint8Array | null {
	const doc = communities.get(slug);
	if (!doc) return null;

	const peerState = getPeerSyncState(slug, peerId);
	const [newSyncState, message] = Automerge.generateSyncMessage(
		doc,
		peerState.syncState
	);
	peerState.syncState = newSyncState;

	return message || null;
}

/**
 * Get document as plain object (for API responses)
 */
export function getDocumentData(slug: string): CommunityDoc | null {
	const doc = communities.get(slug);
	if (!doc) return null;

	// Convert Automerge doc to plain object
	return JSON.parse(JSON.stringify(doc));
}

/**
 * Add multiple shapes in a single Automerge change (for external API calls)
 */
export function addShapes(
	slug: string,
	shapes: Record<string, unknown>[],
): string[] {
	const doc = communities.get(slug);
	if (!doc) return [];

	const ids: string[] = [];
	const newDoc = Automerge.change(doc, `Add ${shapes.length} shapes`, (d) => {
		if (!d.shapes) d.shapes = {};
		for (const shape of shapes) {
			const id = (shape.id as string) || `shape-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
			ids.push(id);
			d.shapes[id] = {
				type: (shape.type as string) || 'geo',
				x: (shape.x as number) ?? 100,
				y: (shape.y as number) ?? 100,
				width: (shape.width as number) ?? 300,
				height: (shape.height as number) ?? 200,
				rotation: (shape.rotation as number) ?? 0,
				...shape,
				id,
			} as ShapeData;
		}
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
	return ids;
}

// Legacy functions for backward compatibility

export function updateShape(
	slug: string,
	shapeId: string,
	data: ShapeData,
): void {
	const doc = communities.get(slug);
	if (doc) {
		const newDoc = Automerge.change(doc, `Update shape ${shapeId}`, (d) => {
			if (!d.shapes) d.shapes = {};
			d.shapes[shapeId] = data;
		});
		communities.set(slug, newDoc);
		saveCommunity(slug);
	}
}

export function deleteShape(slug: string, shapeId: string): void {
	const doc = communities.get(slug);
	if (doc) {
		const newDoc = Automerge.change(doc, `Delete shape ${shapeId}`, (d) => {
			if (d.shapes && d.shapes[shapeId]) {
				delete d.shapes[shapeId];
			}
		});
		communities.set(slug, newDoc);
		saveCommunity(slug);
	}
}

/**
 * Forget a shape — add DID to forgottenBy map. Shape stays in doc but fades for all.
 * Three-state: present → forgotten (faded) → deleted
 */
export function forgetShape(slug: string, shapeId: string, forgottenBy?: string): void {
	const doc = communities.get(slug);
	if (!doc || !doc.shapes?.[shapeId]) return;

	const newDoc = Automerge.change(doc, `Forget shape ${shapeId}`, (d) => {
		if (d.shapes[shapeId]) {
			const shape = d.shapes[shapeId] as Record<string, unknown>;
			// Add DID to forgottenBy map (Automerge merges concurrent map writes cleanly)
			if (!shape.forgottenBy || typeof shape.forgottenBy !== 'object') {
				shape.forgottenBy = {};
			}
			if (forgottenBy) {
				(shape.forgottenBy as Record<string, number>)[forgottenBy] = Date.now();
			}
			// Legacy compat: keep scalar forgotten flag synced
			shape.forgotten = true;
			shape.forgottenAt = Date.now();
		}
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
}

/**
 * Remember a forgotten shape — clear forgottenBy map + deleted flag.
 * Restores shape to present state for everyone.
 */
export function rememberShape(slug: string, shapeId: string): void {
	const doc = communities.get(slug);
	if (!doc || !doc.shapes?.[shapeId]) return;

	const newDoc = Automerge.change(doc, `Remember shape ${shapeId}`, (d) => {
		if (d.shapes[shapeId]) {
			const shape = d.shapes[shapeId] as Record<string, unknown>;
			shape.forgottenBy = {};
			shape.deleted = false;
			// Legacy compat
			shape.forgotten = false;
			shape.forgottenAt = 0;
		}
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
}

/**
 * Update specific fields of a shape (for bidirectional module sync callbacks).
 * Only updates the fields provided, preserving all other shape data.
 */
export function updateShapeFields(
	slug: string,
	shapeId: string,
	fields: Record<string, unknown>,
): boolean {
	const doc = communities.get(slug);
	if (!doc || !doc.shapes?.[shapeId]) return false;

	const newDoc = Automerge.change(doc, `Update shape ${shapeId} fields`, (d) => {
		if (d.shapes[shapeId]) {
			for (const [key, value] of Object.entries(fields)) {
				(d.shapes[shapeId] as Record<string, unknown>)[key] = value;
			}
		}
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
	return true;
}

/**
 * Set a member in the community's Automerge doc
 */
export function setMember(
	slug: string,
	did: string,
	role: SpaceMember['role'],
	displayName?: string,
): void {
	const doc = communities.get(slug);
	if (!doc) return;

	const newDoc = Automerge.change(doc, `Set member ${did}`, (d) => {
		if (!d.members) d.members = {};
		d.members[did] = {
			did,
			role,
			joinedAt: d.members[did]?.joinedAt || Date.now(),
			...(displayName ? { displayName } : {}),
		};
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
}

/**
 * Remove a member from the community's Automerge doc
 */
export function removeMember(slug: string, did: string): void {
	const doc = communities.get(slug);
	if (!doc) return;

	const newDoc = Automerge.change(doc, `Remove member ${did}`, (d) => {
		if (d.members && d.members[did]) {
			delete d.members[did];
		}
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
}

/**
 * Clear all shapes from a community (for demo reset)
 */
export function clearShapes(slug: string): void {
	const doc = communities.get(slug);
	if (doc) {
		const newDoc = Automerge.change(doc, "Clear all shapes", (d) => {
			d.shapes = {};
		});
		communities.set(slug, newDoc);
		saveCommunity(slug);
	}
}

// ── Nested Spaces CRUD ──

/**
 * Add a SpaceRef (nest a space into another)
 */
export function addNestedSpace(slug: string, ref: SpaceRef): void {
	const doc = communities.get(slug);
	if (!doc) return;

	const newDoc = Automerge.change(doc, `Nest space ${ref.sourceSlug}`, (d) => {
		if (!d.nestedSpaces) d.nestedSpaces = {};
		d.nestedSpaces[ref.id] = { ...ref };
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
}

/**
 * Update a SpaceRef (permissions, filter, position)
 */
export function updateNestedSpace(
	slug: string,
	refId: string,
	fields: Partial<SpaceRef>,
): boolean {
	const doc = communities.get(slug);
	if (!doc || !doc.nestedSpaces?.[refId]) return false;

	const newDoc = Automerge.change(doc, `Update nest ${refId}`, (d) => {
		if (d.nestedSpaces[refId]) {
			for (const [key, value] of Object.entries(fields)) {
				if (key === 'id') continue; // never change ID
				(d.nestedSpaces[refId] as unknown as Record<string, unknown>)[key] = value;
			}
		}
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
	return true;
}

/**
 * Remove a SpaceRef (un-nest)
 */
export function removeNestedSpace(slug: string, refId: string): boolean {
	const doc = communities.get(slug);
	if (!doc || !doc.nestedSpaces?.[refId]) return false;

	const newDoc = Automerge.change(doc, `Remove nest ${refId}`, (d) => {
		if (d.nestedSpaces && d.nestedSpaces[refId]) {
			delete d.nestedSpaces[refId];
		}
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
	return true;
}

/**
 * Get the NestPolicy for a space, with fallback to default
 */
export function getNestPolicy(slug: string): NestPolicy | null {
	const doc = communities.get(slug);
	if (!doc) return null;
	return doc.meta.nestPolicy || DEFAULT_COMMUNITY_NEST_POLICY;
}

/**
 * Update the NestPolicy for a space
 */
export function updateNestPolicy(slug: string, policy: Partial<NestPolicy>): boolean {
	const doc = communities.get(slug);
	if (!doc) return false;

	const newDoc = Automerge.change(doc, `Update nest policy`, (d) => {
		const current = d.meta.nestPolicy || { ...DEFAULT_COMMUNITY_NEST_POLICY };
		d.meta.nestPolicy = {
			...current,
			...policy,
			notifications: { ...current.notifications, ...(policy.notifications || {}) },
			defaultPermissions: { ...current.defaultPermissions, ...(policy.defaultPermissions || {}) },
		};
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
	return true;
}

/**
 * Update enabled modules for a space
 */
export function setEnabledModules(slug: string, modules: string[]): boolean {
	const doc = communities.get(slug);
	if (!doc) return false;

	const newDoc = Automerge.change(doc, `Set enabled modules`, (d) => {
		d.meta.enabledModules = modules;
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
	return true;
}

/**
 * Cap requested permissions against a policy's defaultPermissions ceiling
 */
export function capPermissions(
	requested: NestPermissions,
	ceiling: NestPermissions,
): NestPermissions {
	return {
		read: requested.read && ceiling.read,
		write: requested.write && ceiling.write,
		addShapes: requested.addShapes && ceiling.addShapes,
		deleteShapes: requested.deleteShapes && ceiling.deleteShapes,
		reshare: requested.reshare && ceiling.reshare,
		expiry: requested.expiry
			? (ceiling.expiry ? Math.min(requested.expiry, ceiling.expiry) : requested.expiry)
			: ceiling.expiry,
	};
}

/**
 * Cascade permissions across a nesting chain (intersection — most restrictive wins)
 */
export function cascadePermissions(chain: NestPermissions[]): NestPermissions {
	return {
		read: chain.every(p => p.read),
		write: chain.every(p => p.write),
		addShapes: chain.every(p => p.addShapes),
		deleteShapes: chain.every(p => p.deleteShapes),
		reshare: chain.every(p => p.reshare),
		expiry: chain.some(p => p.expiry)
			? Math.min(...chain.filter(p => p.expiry).map(p => p.expiry!))
			: undefined,
	};
}

// ── Encryption ──

/**
 * Toggle encryption on a space and set the key identifier.
 * When encrypted: true, the Automerge binary is AES-256 encrypted at rest.
 * The server decrypts on load and serves plaintext to authorized clients
 * (this is "Approach B: plaintext projection" from the architecture spec).
 *
 * Full E2E encryption (Approach C) will be added when EncryptID Layer 2
 * client-side key delegation is implemented.
 */
export function setEncryption(
	slug: string,
	encrypted: boolean,
	encryptionKeyId?: string,
): boolean {
	const doc = communities.get(slug);
	if (!doc) return false;

	const newDoc = Automerge.change(doc, `Set encryption ${encrypted ? 'on' : 'off'}`, (d) => {
		d.meta.encrypted = encrypted;
		d.meta.encryptionKeyId = encrypted ? (encryptionKeyId || `key-${slug}-${Date.now()}`) : undefined;
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
	return true;
}

/**
 * Derive an AES-256-GCM key from a space's encryption key identifier.
 * In production this will use EncryptID Layer 2 key derivation.
 * For now, uses a deterministic HMAC-based key from a server secret.
 */
async function deriveSpaceKey(keyId: string): Promise<CryptoKey> {
	const serverSecret = process.env.ENCRYPTION_SECRET;
	if (!serverSecret) {
		throw new Error('ENCRYPTION_SECRET environment variable is required');
	}
	const encoder = new TextEncoder();
	const keyMaterial = await crypto.subtle.importKey(
		'raw',
		encoder.encode(serverSecret),
		{ name: 'HMAC', hash: 'SHA-256' },
		false,
		['sign'],
	);
	const derived = await crypto.subtle.sign('HMAC', keyMaterial, encoder.encode(keyId));
	return crypto.subtle.importKey(
		'raw',
		derived,
		{ name: 'AES-GCM', length: 256 },
		false,
		['encrypt', 'decrypt'],
	);
}

/**
 * Encrypt binary data using AES-256-GCM.
 * Returns: 12-byte IV + ciphertext + 16-byte auth tag (all concatenated).
 */
async function encryptBinary(data: Uint8Array, key: CryptoKey): Promise<Uint8Array> {
	const iv = crypto.getRandomValues(new Uint8Array(12));
	// Copy into a fresh ArrayBuffer to satisfy strict BufferSource typing
	const plainBuf = new ArrayBuffer(data.byteLength);
	new Uint8Array(plainBuf).set(data);
	const ciphertext = await crypto.subtle.encrypt(
		{ name: 'AES-GCM', iv },
		key,
		plainBuf,
	);
	const result = new Uint8Array(12 + ciphertext.byteLength);
	result.set(iv, 0);
	result.set(new Uint8Array(ciphertext), 12);
	return result;
}

/**
 * Decrypt binary data encrypted with AES-256-GCM.
 * Expects: 12-byte IV + ciphertext + 16-byte auth tag.
 */
async function decryptBinary(data: Uint8Array, key: CryptoKey): Promise<Uint8Array> {
	const iv = data.slice(0, 12);
	const ciphertext = data.slice(12);
	const plaintext = await crypto.subtle.decrypt(
		{ name: 'AES-GCM', iv },
		key,
		ciphertext,
	);
	return new Uint8Array(plaintext);
}

// Magic bytes to identify encrypted Automerge files
const ENCRYPTED_MAGIC = new Uint8Array([0x72, 0x53, 0x45, 0x4E]); // "rSEN" (rSpace ENcrypted)

/**
 * Find all spaces that a given space is nested into (reverse lookup)
 */
export async function findNestedIn(sourceSlug: string): Promise<Array<{ slug: string; refId: string; ref: SpaceRef }>> {
	const results: Array<{ slug: string; refId: string; ref: SpaceRef }> = [];
	const allSlugs = await listCommunities();

	for (const slug of allSlugs) {
		await loadCommunity(slug);
		const doc = communities.get(slug);
		if (!doc?.nestedSpaces) continue;

		for (const [refId, ref] of Object.entries(doc.nestedSpaces)) {
			if (ref.sourceSlug === sourceSlug) {
				results.push({ slug, refId, ref: JSON.parse(JSON.stringify(ref)) });
			}
		}
	}

	return results;
}
