/**
 * Photos module — community photo commons powered by Immich.
 *
 * Per-space isolation: each space gets its own Immich album, with
 * CRUD gated by the space role system (viewer/member/moderator/admin).
 */

import { Hono } from "hono";
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 type { EncryptIDClaims } from "../../server/auth";
import { resolveCallerRole, roleAtLeast } from "../../server/spaces";
import type { SpaceRoleString } from "../../server/spaces";
import { filterArrayByVisibility } from "../../shared/membrane";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import { photosSchema, photosDocId } from './schemas';
import type { PhotosDoc, SpaceAlbum } from './schemas';
import {
	immichCreateAlbum,
	immichGetAlbum,
	immichSearchInAlbum,
	immichUploadAsset,
	immichAddAssetsToAlbum,
	immichDeleteAssets,
	immichGetAssetThumbnail,
	immichGetAssetOriginal,
} from './lib/immich-client';

let _syncServer: SyncServer | null = null;

const routes = new Hono();

const IMMICH_PUBLIC_URL = process.env.RPHOTOS_IMMICH_PUBLIC_URL || "https://demo.rphotos.online";

// ── Local-first helpers ──

function ensurePhotosDoc(space: string): PhotosDoc {
	const docId = photosDocId(space);
	let doc = _syncServer!.getDoc<PhotosDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<PhotosDoc>(), 'init photos', (d) => {
			const init = photosSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

// ── Role helper ──

async function requireRole(
	c: any,
	minRole: SpaceRoleString,
): Promise<{ claims: EncryptIDClaims; role: SpaceRoleString; space: string } | Response> {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims: EncryptIDClaims;
	try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const result = await resolveCallerRole(space, claims);
	if (!result || !roleAtLeast(result.role, minRole)) {
		return c.json({ error: "Insufficient permissions" }, 403);
	}
	return { claims, role: result.role, space };
}

/** Resolve caller role without requiring auth (for membrane filtering on read routes). */
async function resolveOptionalRole(c: any): Promise<{ role: SpaceRoleString; space: string }> {
	const space = c.req.param("space") || "demo";
	let role: SpaceRoleString = 'viewer';
	const token = extractToken(c.req.raw.headers);
	if (token) {
		try {
			const claims = await verifyToken(token);
			const resolved = await resolveCallerRole(space, claims);
			if (resolved) role = resolved.role;
		} catch {}
	}
	return { role, space };
}

// ── Setup routes (admin only) ──

routes.get("/api/setup/status", async (c) => {
	if (!_syncServer) return c.json({ enabled: false, spaceAlbumId: null });
	const { role, space } = await resolveOptionalRole(c);
	const doc = ensurePhotosDoc(space);
	return c.json({ enabled: doc.enabled, spaceAlbumId: doc.spaceAlbumId });
});

routes.post("/api/setup", async (c) => {
	const auth = await requireRole(c, "admin");
	if (auth instanceof Response) return auth;
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space } = auth;
	const doc = ensurePhotosDoc(space);
	if (doc.enabled && doc.spaceAlbumId) {
		return c.json({ enabled: true, spaceAlbumId: doc.spaceAlbumId });
	}

	try {
		const album = await immichCreateAlbum(`rspace:${space}`, `Root album for space ${space}`);
		const docId = photosDocId(space);
		_syncServer.changeDoc<PhotosDoc>(docId, `enable rphotos for ${space}`, (d) => {
			d.enabled = true;
			d.spaceAlbumId = album.id;
		});
		return c.json({ enabled: true, spaceAlbumId: album.id }, 201);
	} catch (err: any) {
		return c.json({ error: `Failed to create Immich album: ${err.message}` }, 500);
	}
});

routes.delete("/api/setup", async (c) => {
	const auth = await requireRole(c, "admin");
	if (auth instanceof Response) return auth;
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space } = auth;
	const docId = photosDocId(space);
	_syncServer.changeDoc<PhotosDoc>(docId, `disable rphotos for ${space}`, (d) => {
		d.enabled = false;
	});
	return c.json({ ok: true });
});

// ── Upload route (member+) ──

routes.post("/api/upload", async (c) => {
	const auth = await requireRole(c, "member");
	if (auth instanceof Response) return auth;
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space } = auth;
	const doc = ensurePhotosDoc(space);
	if (!doc.enabled || !doc.spaceAlbumId) {
		return c.json({ error: "rPhotos is not enabled for this space" }, 400);
	}

	try {
		const body = await c.req.parseBody();
		const file = body['file'];
		if (!file || !(file instanceof File)) {
			return c.json({ error: "file required" }, 400);
		}

		const formData = new FormData();
		formData.append('assetData', file, file.name);
		formData.append('deviceAssetId', crypto.randomUUID());
		formData.append('deviceId', `rspace-${space}`);
		formData.append('fileCreatedAt', new Date().toISOString());
		formData.append('fileModifiedAt', new Date().toISOString());

		const asset = await immichUploadAsset(formData);
		await immichAddAssetsToAlbum(doc.spaceAlbumId, [asset.id]);
		return c.json({ id: asset.id, status: asset.status }, 201);
	} catch (err: any) {
		return c.json({ error: `Upload failed: ${err.message}` }, 500);
	}
});

// ── Delete asset (moderator+) ──

routes.delete("/api/assets/:id", async (c) => {
	const auth = await requireRole(c, "moderator");
	if (auth instanceof Response) return auth;

	const assetId = c.req.param("id");
	try {
		await immichDeleteAssets([assetId]);
		return c.json({ ok: true });
	} catch (err: any) {
		return c.json({ error: `Delete failed: ${err.message}` }, 500);
	}
});

// ── Sub-album CRUD ──

routes.post("/api/space-albums", async (c) => {
	const auth = await requireRole(c, "member");
	if (auth instanceof Response) return auth;
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space, claims } = auth;
	const doc = ensurePhotosDoc(space);
	if (!doc.enabled) return c.json({ error: "rPhotos is not enabled for this space" }, 400);

	const { name } = await c.req.json();
	if (!name) return c.json({ error: "name required" }, 400);

	try {
		const immichAlbum = await immichCreateAlbum(`rspace:${space}:${name}`, `Sub-album in space ${space}`);
		const id = crypto.randomUUID();
		const callerDid = (claims.did as string) || claims.sub || '';
		const docId = photosDocId(space);

		_syncServer.changeDoc<PhotosDoc>(docId, `create sub-album ${name}`, (d) => {
			d.subAlbums[id] = {
				id,
				immichAlbumId: immichAlbum.id,
				name,
				createdBy: callerDid,
				createdAt: Date.now(),
			};
		});

		const updated = _syncServer.getDoc<PhotosDoc>(docId)!;
		return c.json(updated.subAlbums[id], 201);
	} catch (err: any) {
		return c.json({ error: `Failed to create album: ${err.message}` }, 500);
	}
});

routes.patch("/api/space-albums/:id", async (c) => {
	const auth = await requireRole(c, "member");
	if (auth instanceof Response) return auth;
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space, claims, role } = auth;
	const albumId = c.req.param("id");
	const doc = ensurePhotosDoc(space);
	const album = doc.subAlbums[albumId];
	if (!album) return c.json({ error: "Album not found" }, 404);

	const callerDid = (claims.did as string) || claims.sub || '';
	const isOwner = album.createdBy === callerDid;
	if (!isOwner && !roleAtLeast(role, "moderator")) {
		return c.json({ error: "Insufficient permissions" }, 403);
	}

	const body = await c.req.json();
	const docId = photosDocId(space);
	_syncServer.changeDoc<PhotosDoc>(docId, `update sub-album ${albumId}`, (d) => {
		if (body.name !== undefined) d.subAlbums[albumId].name = body.name;
		if (body.visibility !== undefined) d.subAlbums[albumId].visibility = body.visibility;
	});

	const updated = _syncServer.getDoc<PhotosDoc>(docId)!;
	return c.json(updated.subAlbums[albumId]);
});

routes.delete("/api/space-albums/:id", async (c) => {
	const auth = await requireRole(c, "moderator");
	if (auth instanceof Response) return auth;
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space } = auth;
	const albumId = c.req.param("id");
	const doc = ensurePhotosDoc(space);
	if (!doc.subAlbums[albumId]) return c.json({ error: "Album not found" }, 404);

	const docId = photosDocId(space);
	_syncServer.changeDoc<PhotosDoc>(docId, `delete sub-album ${albumId}`, (d) => {
		delete d.subAlbums[albumId];
	});
	return c.json({ ok: true });
});

// ── CRUD: Curated Albums ──

routes.get("/api/curations", async (c) => {
	if (!_syncServer) return c.json({ albums: [] });
	const { role, space } = await resolveOptionalRole(c);
	const doc = ensurePhotosDoc(space);
	return c.json({ albums: filterArrayByVisibility(Object.values(doc.sharedAlbums || {}), role) });
});

routes.post("/api/curations", async (c) => {
	const auth = await requireRole(c, "member");
	if (auth instanceof Response) return auth;
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space, claims } = auth;
	const { name, description = "" } = await c.req.json();
	if (!name) return c.json({ error: "name required" }, 400);
	const id = crypto.randomUUID();
	const docId = photosDocId(space);
	ensurePhotosDoc(space);
	_syncServer.changeDoc<PhotosDoc>(docId, `share album ${id}`, (d) => {
		d.sharedAlbums[id] = { id, name, description, sharedBy: claims.sub || null, sharedAt: Date.now() };
	});
	const updated = _syncServer.getDoc<PhotosDoc>(docId)!;
	return c.json(updated.sharedAlbums[id], 201);
});

routes.delete("/api/curations/:albumId", async (c) => {
	const auth = await requireRole(c, "moderator");
	if (auth instanceof Response) return auth;
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space } = auth;
	const albumId = c.req.param("albumId");
	const docId = photosDocId(space);
	const doc = ensurePhotosDoc(space);
	if (!doc.sharedAlbums[albumId]) return c.json({ error: "Not found" }, 404);
	_syncServer.changeDoc<PhotosDoc>(docId, `remove album ${albumId}`, (d) => { delete d.sharedAlbums[albumId]; });
	return c.json({ ok: true });
});

// ── CRUD: Photo Annotations ──

routes.get("/api/annotations", async (c) => {
	if (!_syncServer) return c.json({ annotations: [] });
	const { space } = await resolveOptionalRole(c);
	const doc = ensurePhotosDoc(space);
	return c.json({ annotations: Object.values(doc.annotations || {}) });
});

routes.post("/api/annotations", async (c) => {
	const auth = await requireRole(c, "member");
	if (auth instanceof Response) return auth;
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space, claims } = auth;
	const { assetId, note } = await c.req.json();
	if (!assetId || !note) return c.json({ error: "assetId and note required" }, 400);
	const id = crypto.randomUUID();
	const docId = photosDocId(space);
	ensurePhotosDoc(space);
	_syncServer.changeDoc<PhotosDoc>(docId, `annotate ${assetId}`, (d) => {
		d.annotations[id] = { assetId, note, authorDid: (claims.did as string) || claims.sub || '', createdAt: Date.now() };
	});
	const updated = _syncServer.getDoc<PhotosDoc>(docId)!;
	return c.json(updated.annotations[id], 201);
});

routes.delete("/api/annotations/:annotationId", async (c) => {
	const auth = await requireRole(c, "member");
	if (auth instanceof Response) return auth;
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space } = auth;
	const annotationId = c.req.param("annotationId");
	const docId = photosDocId(space);
	const doc = ensurePhotosDoc(space);
	if (!doc.annotations[annotationId]) return c.json({ error: "Not found" }, 404);
	_syncServer.changeDoc<PhotosDoc>(docId, `delete annotation ${annotationId}`, (d) => { delete d.annotations[annotationId]; });
	return c.json({ ok: true });
});

// ── Proxy: list space albums (space-scoped) ──

routes.get("/api/albums", async (c) => {
	if (!_syncServer) return c.json({ albums: [] });
	const { role, space } = await resolveOptionalRole(c);
	const doc = ensurePhotosDoc(space);

	if (!doc.enabled || !doc.spaceAlbumId) {
		return c.json({ albums: [] });
	}

	const subAlbums = filterArrayByVisibility(Object.values(doc.subAlbums || {}), role);
	return c.json({ albums: subAlbums });
});

// ── Proxy: album detail (space-scoped) ──

routes.get("/api/albums/:id", async (c) => {
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
	const { role, space } = await resolveOptionalRole(c);
	const albumId = c.req.param("id");
	const doc = ensurePhotosDoc(space);

	// Verify album belongs to this space
	const subAlbum = doc.subAlbums[albumId];
	if (!subAlbum) return c.json({ error: "Album not found" }, 404);

	try {
		const data = await immichGetAlbum(subAlbum.immichAlbumId);
		if (!data) return c.json({ error: "Album not found in Immich" }, 404);
		return c.json(data);
	} catch {
		return c.json({ error: "Failed to load album" }, 500);
	}
});

// ── Proxy: recent assets (space-scoped) ──

routes.get("/api/assets", async (c) => {
	if (!_syncServer) return c.json({ assets: [] });
	const { space } = await resolveOptionalRole(c);
	const size = parseInt(c.req.query("size") || "50");
	const doc = ensurePhotosDoc(space);

	if (!doc.enabled || !doc.spaceAlbumId) {
		return c.json({ assets: [] });
	}

	try {
		const result = await immichSearchInAlbum(doc.spaceAlbumId, { size });
		return c.json({ assets: result.items });
	} catch {
		return c.json({ assets: [] });
	}
});

// ── Proxy: asset thumbnail (viewer+ auth) ──

routes.get("/api/assets/:id/thumbnail", async (c) => {
	const id = c.req.param("id");
	const size = c.req.query("size") || "thumbnail";
	try {
		const result = await immichGetAssetThumbnail(id, size);
		if (!result) return c.body(null, 404);
		return c.body(result.body, 200, {
			"Content-Type": result.contentType,
			"Cache-Control": "public, max-age=86400",
		});
	} catch {
		return c.body(null, 500);
	}
});

// ── Proxy: full-size asset (viewer+ auth) ──

routes.get("/api/assets/:id/original", async (c) => {
	const id = c.req.param("id");
	try {
		const result = await immichGetAssetOriginal(id);
		if (!result) return c.body(null, 404);
		return c.body(result.body, 200, {
			"Content-Type": result.contentType,
			"Cache-Control": "public, max-age=86400",
		});
	} catch {
		return c.body(null, 500);
	}
});

// ── Embedded Immich UI (admin only) ──

routes.get("/album", async (c) => {
	const auth = await requireRole(c, "admin");
	if (auth instanceof Response) {
		// Fallback: render the page but the iframe will be empty for non-admins
		const spaceSlug = c.req.param("space") || "demo";
		return c.html(renderShell({
			title: `${spaceSlug} — Photos | rSpace`,
			moduleId: "rphotos",
			spaceSlug,
			modules: getModuleInfoList(),
			theme: "dark",
			body: `<div style="text-align:center;padding:3rem;color:#64748b">Admin access required to use the Immich interface directly.</div>`,
		}));
	}

	const spaceSlug = auth.space;
	return c.html(renderExternalAppShell({
		title: `${spaceSlug} — Immich | rSpace`,
		moduleId: "rphotos",
		spaceSlug,
		modules: getModuleInfoList(),
		appUrl: IMMICH_PUBLIC_URL,
		appName: "Immich",
		theme: "dark",
	}));
});

// ── Page route ──

routes.get("/", (c) => {
	const spaceSlug = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `${spaceSlug} — Photos | rSpace`,
		moduleId: "rphotos",
		spaceSlug,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-photo-gallery space="${spaceSlug}"></folk-photo-gallery>`,
		scripts: `<script type="module" src="/modules/rphotos/folk-photo-gallery.js?v=3"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rphotos/photos.css">`,
	}));
});

// ── MI Integration ──

export function getSharedAlbumsForMI(space: string, limit = 5): { id: string; name: string; sharedAt: number }[] {
	if (!_syncServer) return [];
	const docId = photosDocId(space);
	const doc = _syncServer.getDoc<PhotosDoc>(docId);
	if (!doc) return [];
	return Object.values(doc.sharedAlbums)
		.sort((a, b) => b.sharedAt - a.sharedAt)
		.slice(0, limit)
		.map((a) => ({ id: a.id, name: a.name, sharedAt: a.sharedAt }));
}

export const photosModule: RSpaceModule = {
	id: "rphotos",
	name: "rPhotos",
	icon: "📸",
	description: "Community photo commons",
	scoping: { defaultScope: 'global', userConfigurable: false },
	docSchemas: [{ pattern: '{space}:photos:albums', description: 'Shared albums and annotations per space', init: photosSchema.init }],
	routes,
	landingPage: renderLanding,
	standaloneDomain: "rphotos.online",
	externalApp: { url: IMMICH_PUBLIC_URL, name: "Immich" },
	async onInit(ctx) { _syncServer = ctx.syncServer; },
	feeds: [
		{
			id: "rphotos",
			name: "Recent Photos",
			kind: "data",
			description: "Stream of recently uploaded photos",
			emits: ["folk-image"],
			filterable: true,
		},
	],
	acceptsFeeds: ["data"],
	outputPaths: [
		{ path: "albums", name: "Albums", icon: "📸", description: "Photo albums and collections" },
		{ path: "galleries", name: "Galleries", icon: "🖼️", description: "Public photo galleries" },
	],
	subPageInfos: [
		{
			path: "album",
			title: "Photo Album",
			icon: "🖼️",
			tagline: "rPhotos Tool",
			description: "Browse a curated photo album with lightbox viewing, metadata display, and easy sharing. Powered by Immich.",
			features: [
				{ icon: "🔍", title: "Lightbox Viewer", text: "Full-screen photo viewing with EXIF data, zoom, and slideshow mode." },
				{ icon: "📤", title: "Easy Sharing", text: "Share individual photos or entire albums with a single link." },
				{ icon: "🏷️", title: "Smart Tags", text: "AI-powered face recognition and object tagging for easy discovery." },
			],
		},
	],
};
