/**
 * Files module — file sharing, public share links, memory cards.
 * Ported from rfiles-online (Django → Bun/Hono).
 *
 * All metadata is stored in Automerge documents via SyncServer.
 * Binary files remain on the filesystem.
 */

import { Hono } from "hono";
import { resolve } from "node:path";
import { mkdir, writeFile, unlink } from "node:fs/promises";
import { createHash, randomBytes } from "node:crypto";
import * as Automerge from "@automerge/automerge";
import { renderShell, renderExternalAppShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import { filesSchema, filesDocId } from './schemas';
import type { FilesDoc, MediaFile, MemoryCard } from './schemas';

// ── Extended doc types (shares + access logs live alongside files/cards) ──

interface PublicShare {
	id: string;
	token: string;
	mediaFileId: string;
	createdBy: string | null;
	expiresAt: number | null;   // epoch ms, null = never
	maxDownloads: number | null;
	downloadCount: number;
	isActive: boolean;
	isPasswordProtected: boolean;
	passwordHash: string | null;
	note: string | null;
	createdAt: number;
}

interface AccessLog {
	id: string;
	mediaFileId: string;
	shareId: string | null;
	ipAddress: string | null;
	userAgent: string | null;
	accessType: string;
	accessedAt: number;
}

/**
 * Extended doc shape — supplements FilesDoc with shares and access logs.
 * The base FilesDoc from schemas.ts defines files + memoryCards;
 * we add shares and accessLogs as additional top-level maps.
 */
interface FilesDocExt extends FilesDoc {
	shares: Record<string, PublicShare>;
	accessLogs: Record<string, AccessLog>;
}

let _syncServer: SyncServer | null = null;

const routes = new Hono();

const FILES_DIR = process.env.FILES_DIR || "/data/files";

// ── Automerge document helpers ──

function ensureDoc(space: string, sharedSpace: string = "default"): FilesDocExt {
	const docId = filesDocId(space, sharedSpace);
	let doc = _syncServer!.getDoc<FilesDocExt>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<FilesDocExt>(), 'init files doc', (d) => {
			const init = filesSchema.init();
			d.meta = init.meta;
			d.meta.spaceSlug = space;
			d.meta.sharedSpace = sharedSpace;
			d.files = {};
			d.memoryCards = {};
			d.shares = {};
			d.accessLogs = {};
		});
		_syncServer!.setDoc(docId, doc);
	}
	// Ensure shares/accessLogs exist on legacy docs that predate these fields
	if (!doc.shares || !doc.accessLogs) {
		doc = _syncServer!.changeDoc<FilesDocExt>(docId, 'add shares+logs maps', (d) => {
			if (!(d as any).shares) (d as any).shares = {};
			if (!(d as any).accessLogs) (d as any).accessLogs = {};
		})!;
	}
	return doc;
}

// ── Cleanup timers (replace Celery) ──
// Deactivate expired shares every hour
setInterval(() => {
	if (!_syncServer) return;
	try {
		const now = Date.now();
		for (const docId of _syncServer.getDocIds()) {
			if (!docId.includes(':files:cards:')) continue;
			const doc = _syncServer.getDoc<FilesDocExt>(docId);
			if (!doc?.shares) continue;
			const toDeactivate = Object.values(doc.shares).filter(
				(s) => s.isActive && s.expiresAt !== null && s.expiresAt < now
			);
			if (toDeactivate.length > 0) {
				_syncServer.changeDoc<FilesDocExt>(docId, 'deactivate expired shares', (d) => {
					for (const s of toDeactivate) {
						if (d.shares[s.id]) d.shares[s.id].isActive = false;
					}
				});
				console.log(`[Files] Deactivated ${toDeactivate.length} expired shares in ${docId}`);
			}
		}
	} catch (e: any) { console.error("[Files] Cleanup error:", e.message); }
}, 3600_000);

// Delete access logs older than 90 days, daily
setInterval(() => {
	if (!_syncServer) return;
	try {
		const cutoff = Date.now() - 90 * 86400_000;
		for (const docId of _syncServer.getDocIds()) {
			if (!docId.includes(':files:cards:')) continue;
			const doc = _syncServer.getDoc<FilesDocExt>(docId);
			if (!doc?.accessLogs) continue;
			const toDelete = Object.values(doc.accessLogs).filter(
				(l) => l.accessedAt < cutoff
			);
			if (toDelete.length > 0) {
				_syncServer.changeDoc<FilesDocExt>(docId, 'prune old access logs', (d) => {
					for (const l of toDelete) {
						delete d.accessLogs[l.id];
					}
				});
			}
		}
	} catch (e: any) { console.error("[Files] Log cleanup error:", e.message); }
}, 86400_000);

// ── Helpers ──
function generateToken(): string {
	return randomBytes(24).toString("base64url");
}

async function hashPassword(pw: string): Promise<string> {
	const hasher = new Bun.CryptoHasher("sha256");
	hasher.update(pw + "rfiles-salt");
	return hasher.digest("hex");
}

async function computeFileHash(buffer: ArrayBuffer): Promise<string> {
	const hash = createHash("sha256");
	hash.update(Buffer.from(buffer));
	return hash.digest("hex");
}

/** Serialize a doc-sourced object for JSON responses (strip Automerge proxies). */
function toPlain<T>(obj: T): T {
	return JSON.parse(JSON.stringify(obj));
}

// ── File upload ──
routes.post("/api/files", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const formData = await c.req.formData();
	const file = formData.get("file") as File | null;
	if (!file) return c.json({ error: "file is required" }, 400);

	const space = c.req.param("space") || formData.get("space")?.toString() || "default";
	const title = formData.get("title")?.toString() || file.name.replace(/\.[^.]+$/, "");
	const description = formData.get("description")?.toString() || "";
	const tagsRaw = formData.get("tags")?.toString() || "[]";
	let tags: string[] = [];
	try { tags = JSON.parse(tagsRaw); } catch { tags = []; }
	const uploadedBy = claims.sub;

	const buffer = await file.arrayBuffer();
	const fileHash = await computeFileHash(buffer);
	const now = new Date();
	const datePath = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")}`;
	const fileId = crypto.randomUUID();
	const storagePath = `uploads/${datePath}/${fileId}/${file.name}`;
	const fullPath = resolve(FILES_DIR, storagePath);

	await mkdir(resolve(fullPath, ".."), { recursive: true });
	await writeFile(fullPath, Buffer.from(buffer));

	const docId = filesDocId(space, space);
	ensureDoc(space, space);

	const mediaFile: MediaFile = {
		id: fileId,
		originalFilename: file.name,
		title,
		description,
		mimeType: file.type || "application/octet-stream",
		fileSize: file.size,
		fileHash,
		storagePath,
		tags,
		isProcessed: false,
		processingError: null,
		uploadedBy,
		sharedSpace: space,
		createdAt: Date.now(),
		updatedAt: Date.now(),
	};

	_syncServer!.changeDoc<FilesDocExt>(docId, `upload file ${fileId}`, (d) => {
		d.files[fileId] = mediaFile;
	});

	return c.json({ file: toPlain(mediaFile) }, 201);
});

// ── File listing ──
routes.get("/api/files", async (c) => {
	const space = c.req.param("space") || c.req.query("space") || "default";
	const mimeType = c.req.query("mime_type");
	const limit = Math.min(Number(c.req.query("limit")) || 50, 200);
	const offset = Number(c.req.query("offset")) || 0;

	const doc = ensureDoc(space, space);

	let files = Object.values(doc.files)
		.filter((f) => f.sharedSpace === space);

	if (mimeType) {
		files = files.filter((f) => f.mimeType && f.mimeType.startsWith(mimeType));
	}

	// Sort by createdAt descending
	files.sort((a, b) => b.createdAt - a.createdAt);

	const total = files.length;
	const paged = files.slice(offset, offset + limit);

	return c.json({ files: toPlain(paged), total, limit, offset });
});

// ── File download ──
routes.get("/api/files/:id/download", async (c) => {
	const fileId = c.req.param("id");
	const space = c.req.param("space") || c.req.query("space") || "default";
	const doc = ensureDoc(space, space);
	const file = doc.files[fileId];
	if (!file) return c.json({ error: "File not found" }, 404);

	const fullPath = resolve(FILES_DIR, file.storagePath);
	const bunFile = Bun.file(fullPath);
	if (!await bunFile.exists()) return c.json({ error: "File missing from storage" }, 404);

	return new Response(bunFile, {
		headers: {
			"Content-Type": file.mimeType || "application/octet-stream",
			"Content-Disposition": `attachment; filename="${file.originalFilename}"`,
			"Content-Length": String(file.fileSize),
		},
	});
});

// ── File detail ──
routes.get("/api/files/:id", async (c) => {
	const fileId = c.req.param("id");
	const space = c.req.param("space") || c.req.query("space") || "default";
	const doc = ensureDoc(space, space);
	const file = doc.files[fileId];
	if (!file) return c.json({ error: "File not found" }, 404);
	return c.json({ file: toPlain(file) });
});

// ── File delete ──
routes.delete("/api/files/:id", async (c) => {
	const fileId = c.req.param("id");
	const space = c.req.param("space") || c.req.query("space") || "default";
	const docId = filesDocId(space, space);
	const doc = ensureDoc(space, space);
	const file = doc.files[fileId];
	if (!file) return c.json({ error: "File not found" }, 404);

	try { await unlink(resolve(FILES_DIR, file.storagePath)); } catch {}
	_syncServer!.changeDoc<FilesDocExt>(docId, `delete file ${fileId}`, (d) => {
		delete d.files[fileId];
		// Also remove any shares referencing this file
		for (const [sid, share] of Object.entries(d.shares)) {
			if (share.mediaFileId === fileId) delete d.shares[sid];
		}
	});
	return c.json({ message: "Deleted" });
});

// ── Create share link ──
routes.post("/api/files/:id/share", async (c) => {
	const authToken = extractToken(c.req.raw.headers);
	if (!authToken) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }

	const fileId = c.req.param("id");
	const space = c.req.param("space") || c.req.query("space") || "default";
	const docId = filesDocId(space, space);
	const doc = ensureDoc(space, space);

	const file = doc.files[fileId];
	if (!file) return c.json({ error: "File not found" }, 404);
	if (file.uploadedBy && file.uploadedBy !== claims.sub) return c.json({ error: "Not authorized" }, 403);

	const body = await c.req.json<{ expires_in_hours?: number; max_downloads?: number; password?: string; note?: string }>();
	const shareToken = generateToken();
	const expiresAt = body.expires_in_hours ? Date.now() + body.expires_in_hours * 3600_000 : null;
	const createdBy = claims.sub;

	let passwordHash: string | null = null;
	let isPasswordProtected = false;
	if (body.password) {
		passwordHash = await hashPassword(body.password);
		isPasswordProtected = true;
	}

	const shareId = crypto.randomUUID();
	const logId = crypto.randomUUID();
	const now = Date.now();

	const share: PublicShare = {
		id: shareId,
		token: shareToken,
		mediaFileId: fileId,
		createdBy,
		expiresAt,
		maxDownloads: body.max_downloads || null,
		downloadCount: 0,
		isActive: true,
		isPasswordProtected,
		passwordHash,
		note: body.note || null,
		createdAt: now,
	};

	_syncServer!.changeDoc<FilesDocExt>(docId, `create share for file ${fileId}`, (d) => {
		d.shares[shareId] = share;
		d.accessLogs[logId] = {
			id: logId,
			mediaFileId: fileId,
			shareId,
			ipAddress: null,
			userAgent: null,
			accessType: 'share_created',
			accessedAt: now,
		};
	});

	return c.json({ share: { ...toPlain(share), url: `/s/${shareToken}` } }, 201);
});

// ── List shares for a file ──
routes.get("/api/files/:id/shares", async (c) => {
	const fileId = c.req.param("id");
	const space = c.req.param("space") || c.req.query("space") || "default";
	const doc = ensureDoc(space, space);

	const shares = Object.values(doc.shares)
		.filter((s) => s.mediaFileId === fileId)
		.sort((a, b) => b.createdAt - a.createdAt);

	return c.json({ shares: toPlain(shares) });
});

// ── Revoke share ──
routes.post("/api/shares/:shareId/revoke", async (c) => {
	const authToken = extractToken(c.req.raw.headers);
	if (!authToken) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }

	const shareId = c.req.param("shareId");
	const space = c.req.param("space") || c.req.query("space") || "default";
	const docId = filesDocId(space, space);
	const doc = ensureDoc(space, space);

	const share = doc.shares[shareId];
	if (!share) return c.json({ error: "Share not found" }, 404);

	// Check authorization via the linked file
	const file = doc.files[share.mediaFileId];
	if (file?.uploadedBy && file.uploadedBy !== claims.sub) return c.json({ error: "Not authorized" }, 403);

	_syncServer!.changeDoc<FilesDocExt>(docId, `revoke share ${shareId}`, (d) => {
		d.shares[shareId].isActive = false;
	});

	const updated = _syncServer!.getDoc<FilesDocExt>(docId)!;
	return c.json({ message: "Revoked", share: toPlain(updated.shares[shareId]) });
});

// ── Public share download ──
routes.get("/s/:token", async (c) => {
	const shareToken = c.req.param("token");

	// Find the share across all files docs
	let foundDocId: string | null = null;
	let foundShare: PublicShare | null = null;
	let foundFile: MediaFile | null = null;

	for (const docId of _syncServer!.getDocIds()) {
		if (!docId.includes(':files:cards:')) continue;
		const doc = _syncServer!.getDoc<FilesDocExt>(docId);
		if (!doc?.shares) continue;
		for (const s of Object.values(doc.shares)) {
			if (s.token === shareToken) {
				foundDocId = docId;
				foundShare = s;
				foundFile = doc.files[s.mediaFileId] || null;
				break;
			}
		}
		if (foundShare) break;
	}

	if (!foundShare || !foundFile) return c.json({ error: "Share not found" }, 404);
	if (!foundShare.isActive) return c.json({ error: "Share has been revoked" }, 410);
	if (foundShare.expiresAt && foundShare.expiresAt < Date.now()) return c.json({ error: "Share has expired" }, 410);
	if (foundShare.maxDownloads && foundShare.downloadCount >= foundShare.maxDownloads) return c.json({ error: "Download limit reached" }, 410);

	if (foundShare.isPasswordProtected) {
		const pw = c.req.query("password");
		if (!pw) return c.json({ error: "Password required", is_password_protected: true }, 401);
		const hash = await hashPassword(pw);
		if (hash !== foundShare.passwordHash) return c.json({ error: "Invalid password" }, 401);
	}

	const logId = crypto.randomUUID();
	const ip = c.req.header("X-Forwarded-For")?.split(",")[0]?.trim() || c.req.header("X-Real-IP") || null;
	const ua = c.req.header("User-Agent") || "";

	_syncServer!.changeDoc<FilesDocExt>(foundDocId!, `download via share ${foundShare.id}`, (d) => {
		d.shares[foundShare!.id].downloadCount += 1;
		d.accessLogs[logId] = {
			id: logId,
			mediaFileId: foundShare!.mediaFileId,
			shareId: foundShare!.id,
			ipAddress: ip,
			userAgent: ua.slice(0, 500),
			accessType: 'download',
			accessedAt: Date.now(),
		};
	});

	const fullPath = resolve(FILES_DIR, foundFile.storagePath);
	const bunFile = Bun.file(fullPath);
	if (!await bunFile.exists()) return c.json({ error: "File missing" }, 404);

	return new Response(bunFile, {
		headers: {
			"Content-Type": foundFile.mimeType || "application/octet-stream",
			"Content-Disposition": `attachment; filename="${foundFile.originalFilename}"`,
			"Content-Length": String(foundFile.fileSize),
		},
	});
});

// ── Share info (public) ──
routes.get("/s/:token/info", async (c) => {
	const shareToken = c.req.param("token");

	let foundShare: PublicShare | null = null;
	let foundFile: MediaFile | null = null;

	for (const docId of _syncServer!.getDocIds()) {
		if (!docId.includes(':files:cards:')) continue;
		const doc = _syncServer!.getDoc<FilesDocExt>(docId);
		if (!doc?.shares) continue;
		for (const s of Object.values(doc.shares)) {
			if (s.token === shareToken) {
				foundShare = s;
				foundFile = doc.files[s.mediaFileId] || null;
				break;
			}
		}
		if (foundShare) break;
	}

	if (!foundShare || !foundFile) return c.json({ error: "Share not found" }, 404);

	const isValid = foundShare.isActive &&
		(!foundShare.expiresAt || foundShare.expiresAt > Date.now()) &&
		(!foundShare.maxDownloads || foundShare.downloadCount < foundShare.maxDownloads);

	return c.json({
		is_password_protected: foundShare.isPasswordProtected,
		is_valid: isValid,
		expires_at: foundShare.expiresAt ? new Date(foundShare.expiresAt).toISOString() : null,
		downloads_remaining: foundShare.maxDownloads ? foundShare.maxDownloads - foundShare.downloadCount : null,
		file_info: { filename: foundFile.originalFilename, mime_type: foundFile.mimeType, size: foundFile.fileSize },
		note: foundShare.note,
	});
});

// ── Memory Cards CRUD ──
routes.post("/api/cards", async (c) => {
	const authToken = extractToken(c.req.raw.headers);
	if (!authToken) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }

	const body = await c.req.json<{ title: string; body?: string; card_type?: string; tags?: string[]; shared_space?: string }>();
	const space = c.req.param("space") || body.shared_space || "default";
	const createdBy = claims.sub;
	const docId = filesDocId(space, space);
	ensureDoc(space, space);

	const cardId = crypto.randomUUID();
	const now = Date.now();

	const card: MemoryCard = {
		id: cardId,
		sharedSpace: space,
		title: body.title,
		body: body.body || "",
		cardType: body.card_type || "note",
		tags: body.tags || [],
		position: 0,
		createdBy,
		createdAt: now,
		updatedAt: now,
	};

	_syncServer!.changeDoc<FilesDocExt>(docId, `create card ${cardId}`, (d) => {
		d.memoryCards[cardId] = card;
	});

	return c.json({ card: toPlain(card) }, 201);
});

routes.get("/api/cards", async (c) => {
	const space = c.req.param("space") || c.req.query("space") || "default";
	const cardType = c.req.query("type");
	const limit = Math.min(Number(c.req.query("limit")) || 50, 200);

	const doc = ensureDoc(space, space);

	let cards = Object.values(doc.memoryCards)
		.filter((card) => card.sharedSpace === space);

	if (cardType) {
		cards = cards.filter((card) => card.cardType === cardType);
	}

	// Sort by position ascending, then createdAt descending
	cards.sort((a, b) => a.position - b.position || b.createdAt - a.createdAt);
	cards = cards.slice(0, limit);

	return c.json({ cards: toPlain(cards), total: cards.length });
});

routes.patch("/api/cards/:id", async (c) => {
	const cardId = c.req.param("id");
	const space = c.req.param("space") || c.req.query("space") || "default";
	const docId = filesDocId(space, space);
	const doc = ensureDoc(space, space);

	const card = doc.memoryCards[cardId];
	if (!card) return c.json({ error: "Card not found" }, 404);

	const body = await c.req.json<{ title?: string; body?: string; card_type?: string; tags?: string[]; position?: number }>();
	if (body.title === undefined && body.body === undefined && body.card_type === undefined && body.tags === undefined && body.position === undefined) {
		return c.json({ error: "No fields to update" }, 400);
	}

	_syncServer!.changeDoc<FilesDocExt>(docId, `update card ${cardId}`, (d) => {
		const c = d.memoryCards[cardId];
		if (body.title !== undefined) c.title = body.title;
		if (body.body !== undefined) c.body = body.body;
		if (body.card_type !== undefined) c.cardType = body.card_type;
		if (body.tags !== undefined) c.tags = body.tags;
		if (body.position !== undefined) c.position = body.position;
		c.updatedAt = Date.now();
	});

	const updated = _syncServer!.getDoc<FilesDocExt>(docId)!;
	return c.json({ card: toPlain(updated.memoryCards[cardId]) });
});

routes.delete("/api/cards/:id", async (c) => {
	const cardId = c.req.param("id");
	const space = c.req.param("space") || c.req.query("space") || "default";
	const docId = filesDocId(space, space);
	const doc = ensureDoc(space, space);

	if (!doc.memoryCards[cardId]) return c.json({ error: "Card not found" }, 404);

	_syncServer!.changeDoc<FilesDocExt>(docId, `delete card ${cardId}`, (d) => {
		delete d.memoryCards[cardId];
	});
	return c.json({ message: "Deleted" });
});

// ── Page route ──
routes.get("/", (c) => {
	const spaceSlug = c.req.param("space") || "demo";
	const view = c.req.query("view");

	if (view === "app") {
		return c.html(renderExternalAppShell({
			title: `${spaceSlug} — Seafile | rSpace`,
			moduleId: "rfiles",
			spaceSlug,
			modules: getModuleInfoList(),
			appUrl: "https://files.rfiles.online",
			appName: "Seafile",
			theme: "dark",
		}));
	}

	return c.html(renderShell({
		title: `${spaceSlug} — Files | rSpace`,
		moduleId: "rfiles",
		spaceSlug,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<div class="rapp-nav" style="padding:0 1rem;margin-top:8px"><span class="rapp-nav__title"></span><a href="?view=app" class="rapp-nav__btn--app-toggle">Open Full App</a></div>
<folk-file-browser space="${spaceSlug}"></folk-file-browser>`,
		scripts: `<script type="module" src="/modules/rfiles/folk-file-browser.js?v=2"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rfiles/files.css">`,
	}));
});

// ── Seed template data ──

function seedTemplateFiles(space: string) {
	if (!_syncServer) return;
	const doc = ensureDoc(space, 'default');
	if (Object.keys(doc.files).length > 0 || Object.keys(doc.memoryCards).length > 0) return;

	const docId = filesDocId(space, 'default');
	const now = Date.now();

	_syncServer.changeDoc<FilesDocExt>(docId, 'seed template files', (d) => {
		const files: Array<{ name: string; title: string; mime: string; size: number; tags: string[] }> = [
			{ name: 'project-charter.pdf', title: 'Project Charter', mime: 'application/pdf', size: 245000, tags: ['governance', 'founding'] },
			{ name: 'logo.svg', title: 'Community Logo', mime: 'image/svg+xml', size: 12400, tags: ['branding', 'design'] },
			{ name: 'budget-2026.xlsx', title: 'Budget 2026', mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size: 89000, tags: ['finance', 'planning'] },
		];
		for (const f of files) {
			const id = crypto.randomUUID();
			d.files[id] = {
				id, originalFilename: f.name, title: f.title, description: '',
				mimeType: f.mime, fileSize: f.size, fileHash: null, storagePath: '',
				tags: f.tags, isProcessed: true, processingError: null,
				uploadedBy: 'did:demo:seed', sharedSpace: 'default',
				createdAt: now, updatedAt: now,
			};
		}
		const mcId = crypto.randomUUID();
		d.memoryCards[mcId] = {
			id: mcId, sharedSpace: 'default', title: 'Welcome',
			body: 'This is your shared file space. Upload documents, images, and other files to collaborate with your community.',
			cardType: 'note', tags: ['onboarding'], position: 0,
			createdBy: 'did:demo:seed', createdAt: now, updatedAt: now,
		};
	});

	console.log(`[Files] Template seeded for "${space}": 3 files, 1 memory card`);
}

export const filesModule: RSpaceModule = {
	id: "rfiles",
	name: "rFiles",
	icon: "📁",
	description: "File sharing, share links, and memory cards",
	scoping: { defaultScope: 'space', userConfigurable: false },
	docSchemas: [{ pattern: '{space}:files:cards:{sharedSpace}', description: 'Files and memory cards', init: filesSchema.init }],
	routes,
	landingPage: renderLanding,
	seedTemplate: seedTemplateFiles,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
	},
	standaloneDomain: "rfiles.online",
	externalApp: { url: "https://files.rfiles.online", name: "Seafile" },
	feeds: [
		{
			id: "file-activity",
			name: "File Activity",
			kind: "data",
			description: "Upload, download, and share events across the file library",
			filterable: true,
		},
		{
			id: "shared-files",
			name: "Shared Files",
			kind: "resource",
			description: "Files available via public share links",
		},
	],
	acceptsFeeds: ["data", "resource"],
	outputPaths: [
		{ path: "files", name: "Files", icon: "📁", description: "Uploaded files and documents" },
		{ path: "shares", name: "Shares", icon: "🔗", description: "Public share links" },
	],
	onboardingActions: [
		{ label: "Upload Files", icon: "⬆️", description: "Add files to your space", type: 'create', href: '/{space}/rfiles' },
	],
};

// ── MI Data Export ──

const FILES_PREFIX_MI = ":files:cards:";

export interface MIFileItem {
	id: string;
	title: string | null;
	originalFilename: string;
	mimeType: string | null;
	fileSize: number;
	tags: string[];
	updatedAt: number;
}

export function getRecentFilesForMI(space: string, limit = 5): MIFileItem[] {
	if (!_syncServer) return [];
	const prefix = `${space}${FILES_PREFIX_MI}`;
	const all: MIFileItem[] = [];

	for (const docId of _syncServer.getDocIds()) {
		if (!docId.startsWith(prefix)) continue;
		const doc = _syncServer.getDoc<FilesDoc>(docId);
		if (!doc?.files) continue;
		for (const f of Object.values(doc.files)) {
			all.push({
				id: f.id,
				title: f.title,
				originalFilename: f.originalFilename,
				mimeType: f.mimeType,
				fileSize: f.fileSize,
				tags: f.tags ? Array.from(f.tags) : [],
				updatedAt: f.updatedAt,
			});
		}
	}

	return all
		.sort((a, b) => b.updatedAt - a.updatedAt)
		.slice(0, limit);
}
