import { mkdir, readdir, unlink } from "node:fs/promises";
import * as Automerge from "@automerge/automerge";
import {
	deriveSpaceKey,
	encryptBinary,
	decryptBinary,
	isEncryptedFile,
	packEncrypted,
	unpackEncrypted,
} from "./local-first/encryption-utils";
import type { ConnectionPolicy, SpaceConnection } from "../lib/connection-types";
import {
	DEFAULT_PERSONAL_CONNECTION_POLICY,
	DEFAULT_COMMUNITY_CONNECTION_POLICY,
} from "../lib/connection-types";

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

export type SpaceVisibility = 'public' | 'permissioned' | 'private';

/** Normalize legacy visibility values to the current 3-type model. */
export function normalizeVisibility(v: string): SpaceVisibility {
	if (v === 'public_read' || v === 'public') return 'public';
	if (v === 'authenticated' || v === 'permissioned') return 'permissioned';
	if (v === 'members_only' || v === 'private') return 'private';
	return 'private'; // sovereign by default — unknown values treated as private
}

// ── 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;
	discoverable?: boolean;
	avatar?: string;
	nestPolicy?: NestPolicy;
	connectionPolicy?: ConnectionPolicy;
	encrypted?: boolean;
	encryptionKeyId?: string;
	moduleSettings?: Record<string, Record<string, string | boolean>>;
}

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' | 'member' | 'moderator' | 'admin';
	joinedAt: number;
	displayName?: string;
}

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

// 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 });

/**
 * Runtime migration: rename 'participant' → 'member' in Automerge doc members.
 * Returns the (possibly updated) doc. Only mutates if a participant role is found.
 */
function migrateParticipantToMember(
	doc: Automerge.Doc<CommunityDoc>,
	slug: string,
): Automerge.Doc<CommunityDoc> {
	if (!doc.members) return doc;
	const needsMigration = Object.values(doc.members).some(
		(m) => (m as any).role === 'participant',
	);
	if (!needsMigration) return doc;
	console.log(`[Store] Migrating participant→member roles in ${slug}`);
	return Automerge.change(doc, 'Migrate participant→member', (d) => {
		for (const did of Object.keys(d.members)) {
			if ((d.members[did] as any).role === 'participant') {
				d.members[did].role = 'member';
			}
		}
	});
}

function migrateVisibility(
	doc: Automerge.Doc<CommunityDoc>,
	slug: string,
): Automerge.Doc<CommunityDoc> {
	const v = doc.meta?.visibility as string;
	if (!v) {
		// Missing visibility — default to private (sovereign by default)
		console.log(`[Store] Migrating missing visibility→private in ${slug}`);
		return Automerge.change(doc, `Set default visibility to private in ${slug}`, (d) => {
			d.meta.visibility = 'private';
		});
	}
	const normalized = normalizeVisibility(v);
	if (v === normalized) return doc;
	console.log(`[Store] Migrating visibility ${v}→${normalized} in ${slug}`);
	return Automerge.change(doc, `Migrate visibility ${v}→${normalized}`, (d) => {
		d.meta.visibility = normalized;
	});
}

/**
 * Runtime migration: ensure `connections` map exists on docs created before SpaceConnections.
 */
function migrateAddConnections(
	doc: Automerge.Doc<CommunityDoc>,
	slug: string,
): Automerge.Doc<CommunityDoc> {
	if (doc.connections) return doc;
	console.log(`[Store] Migrating: adding connections map to ${slug}`);
	return Automerge.change(doc, 'Add connections map', (d) => {
		d.connections = {};
	});
}

/**
 * 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 (isEncryptedFile(bytes)) {
				const { keyId, ciphertext } = unpackEncrypted(bytes);
				const key = await deriveSpaceKey(keyId);
				bytes = new Uint8Array(await decryptBinary(ciphertext, key));
				console.log(`[Store] Decrypted ${slug} (keyId: ${keyId})`);
			}

			let doc = Automerge.load<CommunityDoc>(bytes);
			// Runtime migration: participant → member
			doc = migrateParticipantToMember(doc, slug);
			// Runtime migration: normalize legacy visibility values
			doc = migrateVisibility(doc, slug);
			// Runtime migration: add connections map
			doc = migrateAddConnections(doc, slug);
			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
			let doc = jsonToAutomerge(data);
			// Runtime migration: participant → member
			doc = migrateParticipantToMember(doc, slug);
			// Runtime migration: normalize legacy visibility values
			doc = migrateVisibility(doc, slug);
			// Runtime migration: add connections map
			doc = migrateAddConnections(doc, slug);
			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 };
		}
		d.connections = {};
		for (const [connId, conn] of Object.entries((data as any).connections || {})) {
			d.connections[connId] = { ...(conn as SpaceConnection) };
		}
	});
	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 packed = packEncrypted(keyId, ciphertext);
				await Bun.write(path, packed);
				console.log(`[Store] Saved ${slug} encrypted (${packed.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 = 'private',
	options?: {
		enabledModules?: string[];
		nestPolicy?: NestPolicy;
		connectionPolicy?: ConnectionPolicy;
		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?.connectionPolicy) {
			d.meta.connectionPolicy = options.connectionPolicy;
		}
		if (options?.description) {
			d.meta.description = options.description;
		}
		d.shapes = {};
		d.members = {};
		d.nestedSpaces = {};
		d.connections = {};
		// 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;
		discoverable?: boolean;
		enabledModules?: string[];
		moduleScopeOverrides?: Record<string, 'space' | 'global'>;
		moduleSettings?: Record<string, Record<string, string | boolean>>;
	},
): 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.discoverable !== undefined) d.meta.discoverable = fields.discoverable;
		if (fields.enabledModules !== undefined) d.meta.enabledModules = fields.enabledModules;
		if (fields.moduleScopeOverrides !== undefined) d.meta.moduleScopeOverrides = fields.moduleScopeOverrides;
		if (fields.moduleSettings !== undefined) d.meta.moduleSettings = fields.moduleSettings;
	});
	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);

	// Snapshot server-authoritative fields before sync merge
	const prevVisibility = doc.meta?.visibility;
	const prevOwnerDID = doc.meta?.ownerDID;

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

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

	// Pin server-authoritative fields — clients must not overwrite these via sync.
	// Visibility and ownership can only be changed through the authenticated API.
	if (newDoc !== doc) {
		const visChanged = newDoc.meta?.visibility !== prevVisibility;
		const ownerChanged = newDoc.meta?.ownerDID !== prevOwnerDID;
		if (visChanged || ownerChanged) {
			console.warn(`[Store] Sync tried to change authoritative fields in ${slug} — reverting (vis: ${prevVisibility}→${newDoc.meta?.visibility}, owner: ${prevOwnerDID}→${newDoc.meta?.ownerDID})`);
			newDoc = Automerge.change(newDoc, 'Pin server-authoritative fields', (d) => {
				if (visChanged && prevVisibility) d.meta.visibility = prevVisibility;
				if (ownerChanged && prevOwnerDID) d.meta.ownerDID = prevOwnerDID;
			});
		}
	}

	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 = {};
			// Reset seeded flag so demo reset can re-seed
			(d.meta as any).shapesSeeded = false;
		});
		communities.set(slug, newDoc);
		saveCommunity(slug);
	}
}

/**
 * Mark a community as having been seeded with shapes.
 * Prevents re-seeding after users delete all content.
 */
export function markShapesSeeded(slug: string): void {
	const doc = communities.get(slug);
	if (doc) {
		const newDoc = Automerge.change(doc, "Mark shapes seeded", (d) => {
			(d.meta as any).shapesSeeded = true;
		});
		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;
}

/**
 * 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;
}

// ── Space Connections CRUD ──

/**
 * Add a SpaceConnection to a space's doc
 */
export function addConnection(slug: string, conn: SpaceConnection): void {
	const doc = communities.get(slug);
	if (!doc) return;

	const newDoc = Automerge.change(doc, `Add connection ${conn.id} → ${conn.remoteSlug}`, (d) => {
		if (!d.connections) d.connections = {};
		d.connections[conn.id] = { ...conn };
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
}

/**
 * Update fields on an existing SpaceConnection
 */
export function updateConnection(
	slug: string,
	connId: string,
	fields: Partial<SpaceConnection>,
): boolean {
	const doc = communities.get(slug);
	if (!doc || !doc.connections?.[connId]) return false;

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

/**
 * Remove a SpaceConnection from a space's doc
 */
export function removeConnection(slug: string, connId: string): boolean {
	const doc = communities.get(slug);
	if (!doc || !doc.connections?.[connId]) return false;

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

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

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

	const newDoc = Automerge.change(doc, `Update connection policy`, (d) => {
		const current = d.meta.connectionPolicy || { ...DEFAULT_COMMUNITY_CONNECTION_POLICY };
		d.meta.connectionPolicy = {
			...current,
			...policy,
			defaultPermissions: { ...current.defaultPermissions, ...(policy.defaultPermissions || {}) },
			acceptedFlowKinds: policy.acceptedFlowKinds || current.acceptedFlowKinds,
		};
	});
	communities.set(slug, newDoc);
	saveCommunity(slug);
	return true;
}
