/**
 * Splat module — Gaussian splat viewer with x402 gated uploads.
 *
 * Routes are relative to mount point (/:space/splat in unified).
 * Three.js + GaussianSplats3D loaded via CDN importmap.
 */

import { Hono } from "hono";
import { resolve } from "node:path";
import { mkdir, readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { sql } from "../../shared/db/pool";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module";
import { renderLanding } from "./landing";
import {
	verifyEncryptIDToken,
	extractToken,
} from "@encryptid/sdk/server";
import { setupX402FromEnv } from "../../shared/x402/hono-middleware";

const SPLATS_DIR = process.env.SPLATS_DIR || "/data/splats";
const SOURCES_DIR = resolve(SPLATS_DIR, "sources");
const VALID_FORMATS = ["ply", "splat", "spz"];
const VALID_MEDIA_TYPES = [
	"image/jpeg", "image/png", "image/heic",
	"video/mp4", "video/quicktime", "video/webm",
];
const VALID_MEDIA_EXTS = [".jpg", ".jpeg", ".png", ".heic", ".mp4", ".mov", ".webm"];
const MAX_PHOTOS = 100;
const MAX_MEDIA_SIZE = 2 * 1024 * 1024 * 1024; // 2GB per file

// ── Types ──

export interface SplatRow {
	id: string;
	slug: string;
	title: string;
	description: string | null;
	file_path: string;
	file_format: string;
	file_size_bytes: number;
	tags: string[];
	space_slug: string;
	contributor_id: string | null;
	contributor_name: string | null;
	source: string;
	status: string;
	view_count: number;
	payment_tx: string | null;
	payment_network: string | null;
	processing_status: string;
	processing_error: string | null;
	source_file_count: number;
	created_at: string;
}

// ── Helpers ──

function slugify(text: string): string {
	return text
		.toLowerCase()
		.replace(/[^a-z0-9]+/g, "-")
		.replace(/^-|-$/g, "")
		.slice(0, 80);
}

function escapeAttr(s: string): string {
	return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function getFileFormat(filename: string): string | null {
	const ext = filename.split(".").pop()?.toLowerCase();
	return ext && VALID_FORMATS.includes(ext) ? ext : null;
}

function getMimeType(format: string): string {
	switch (format) {
		case "ply": return "application/x-ply";
		case "splat": return "application/octet-stream";
		case "spz": return "application/octet-stream";
		default: return "application/octet-stream";
	}
}

// ── CDN importmap for Three.js + GaussianSplats3D ──

const IMPORTMAP = `<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.169.0/examples/jsm/",
    "@mkkellogg/gaussian-splats-3d": "https://cdn.jsdelivr.net/npm/@mkkellogg/gaussian-splats-3d@0.4.6/build/gaussian-splats-3d.module.js"
  }
}
</script>`;

// ── x402 middleware ──

const x402Middleware = setupX402FromEnv({
	description: "Upload Gaussian splat file",
	resource: "/api/splats",
});

// ── Routes ──

const routes = new Hono();

// ── API: List splats ──
routes.get("/api/splats", async (c) => {
	const spaceSlug = c.req.param("space") || "demo";
	const tag = c.req.query("tag");
	const limit = Math.min(parseInt(c.req.query("limit") || "50"), 100);
	const offset = parseInt(c.req.query("offset") || "0");

	let query = `SELECT id, slug, title, description, file_format, file_size_bytes,
		tags, contributor_name, view_count, processing_status, source_file_count, created_at
		FROM rsplat.splats WHERE status = 'published' AND space_slug = $1`;
	const params: (string | number)[] = [spaceSlug];

	if (tag) {
		params.push(tag);
		query += ` AND $${params.length} = ANY(tags)`;
	}

	query += ` ORDER BY created_at DESC`;
	params.push(limit);
	query += ` LIMIT $${params.length}`;
	params.push(offset);
	query += ` OFFSET $${params.length}`;

	const rows = await sql.unsafe(query, params);
	return c.json({ splats: rows });
});

// ── API: Get splat details ──
routes.get("/api/splats/:id", async (c) => {
	const id = c.req.param("id");

	const rows = await sql.unsafe(
		`SELECT * FROM rsplat.splats WHERE (slug = $1 OR id::text = $1) AND status = 'published'`,
		[id]
	);

	if (rows.length === 0) return c.json({ error: "Splat not found" }, 404);

	// Increment view count
	await sql.unsafe(
		`UPDATE rsplat.splats SET view_count = view_count + 1 WHERE id = $1`,
		[rows[0].id]
	);

	return c.json(rows[0]);
});

// ── API: Serve splat file ──
// Matches both /api/splats/:id/file and /api/splats/:id/:filename (e.g. rainbow-sphere.splat)
routes.get("/api/splats/:id/:filename", async (c) => {
	const id = c.req.param("id");

	const rows = await sql.unsafe(
		`SELECT id, slug, file_path, file_format FROM rsplat.splats WHERE (slug = $1 OR id::text = $1) AND status = 'published'`,
		[id]
	);

	if (rows.length === 0) return c.json({ error: "Splat not found" }, 404);

	const splat = rows[0];
	const filepath = resolve(SPLATS_DIR, splat.file_path);
	const file = Bun.file(filepath);

	if (!(await file.exists())) {
		return c.json({ error: "Splat file not found on disk" }, 404);
	}

	return new Response(file, {
		headers: {
			"Content-Type": getMimeType(splat.file_format),
			"Content-Disposition": `inline; filename="${splat.slug}.${splat.file_format}"`,
			"Content-Length": String(file.size),
			"Access-Control-Allow-Origin": "*",
			"Cache-Control": "public, max-age=86400",
		},
	});
});

// ── API: Upload splat (EncryptID auth + optional x402) ──
routes.post("/api/splats", async (c) => {
	// Auth check
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);

	let claims;
	try {
		claims = await verifyEncryptIDToken(token);
	} catch {
		return c.json({ error: "Invalid token" }, 401);
	}

	// x402 check (if enabled)
	if (x402Middleware) {
		const paymentResult = await new Promise<Response | null>((resolve) => {
			const fakeNext = async () => { resolve(null); };
			x402Middleware(c, fakeNext).then((res) => {
				if (res instanceof Response) resolve(res);
			});
		});
		if (paymentResult) return paymentResult;
	}

	const spaceSlug = c.req.param("space") || "demo";
	const formData = await c.req.formData();
	const file = formData.get("file") as File | null;
	const title = (formData.get("title") as string || "").trim();
	const description = (formData.get("description") as string || "").trim() || null;
	const tagsRaw = (formData.get("tags") as string || "").trim();

	if (!file) {
		return c.json({ error: "Splat file required" }, 400);
	}

	const format = getFileFormat(file.name);
	if (!format) {
		return c.json({ error: "Invalid file format. Accepted: .ply, .splat, .spz" }, 400);
	}

	if (!title) {
		return c.json({ error: "Title required" }, 400);
	}

	// 500MB limit
	if (file.size > 500 * 1024 * 1024) {
		return c.json({ error: "File too large. Maximum 500MB." }, 400);
	}

	const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()).filter(Boolean) : [];
	const shortId = randomUUID().slice(0, 8);
	let slug = slugify(title);

	// Check slug collision
	const existing = await sql.unsafe(
		`SELECT 1 FROM rsplat.splats WHERE slug = $1`, [slug]
	);
	if (existing.length > 0) {
		slug = `${slug}-${shortId}`;
	}

	// Save file to disk
	await mkdir(SPLATS_DIR, { recursive: true });
	const filename = `${slug}.${format}`;
	const filepath = resolve(SPLATS_DIR, filename);
	const buffer = Buffer.from(await file.arrayBuffer());
	await Bun.write(filepath, buffer);

	// Insert into DB
	const paymentTx = (c as any).get("x402Payment") || null;
	const rows = await sql.unsafe(
		`INSERT INTO rsplat.splats (slug, title, description, file_path, file_format, file_size_bytes, tags, space_slug, contributor_id, contributor_name, payment_tx)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
		 RETURNING id, slug, title, description, file_format, file_size_bytes, tags, created_at`,
		[slug, title, description, filename, format, buffer.length, tags, spaceSlug, claims.sub, claims.username || null, paymentTx] as any[]
	);

	return c.json(rows[0], 201);
});

// ── API: Upload photos/video for splatting ──
routes.post("/api/splats/from-media", async (c) => {
	// Auth check
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);

	let claims;
	try {
		claims = await verifyEncryptIDToken(token);
	} catch {
		return c.json({ error: "Invalid token" }, 401);
	}

	// x402 check (if enabled)
	if (x402Middleware) {
		const paymentResult = await new Promise<Response | null>((resolve) => {
			const fakeNext = async () => { resolve(null); };
			x402Middleware(c, fakeNext).then((res) => {
				if (res instanceof Response) resolve(res);
			});
		});
		if (paymentResult) return paymentResult;
	}

	const spaceSlug = c.req.param("space") || "demo";
	const formData = await c.req.formData();
	const title = (formData.get("title") as string || "").trim();
	const description = (formData.get("description") as string || "").trim() || null;
	const tagsRaw = (formData.get("tags") as string || "").trim();

	if (!title) {
		return c.json({ error: "Title required" }, 400);
	}

	// Collect all files from formdata
	const files: File[] = [];
	for (const [key, value] of formData.entries()) {
		if (key === "files" && (value as unknown) instanceof File) {
			files.push(value as unknown as File);
		}
	}

	if (files.length === 0) {
		return c.json({ error: "At least one photo or video file required" }, 400);
	}

	// Validate file types
	const hasVideo = files.some((f) => f.type.startsWith("video/"));
	if (hasVideo && files.length > 1) {
		return c.json({ error: "Only 1 video file allowed per upload" }, 400);
	}
	if (!hasVideo && files.length > MAX_PHOTOS) {
		return c.json({ error: `Maximum ${MAX_PHOTOS} photos per upload` }, 400);
	}

	for (const f of files) {
		const ext = "." + f.name.split(".").pop()?.toLowerCase();
		if (!VALID_MEDIA_EXTS.includes(ext)) {
			return c.json({ error: `Invalid file type: ${f.name}. Accepted: ${VALID_MEDIA_EXTS.join(", ")}` }, 400);
		}
		if (f.size > MAX_MEDIA_SIZE) {
			return c.json({ error: `File too large: ${f.name}. Maximum 2GB.` }, 400);
		}
	}

	const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()).filter(Boolean) : [];
	const shortId = randomUUID().slice(0, 8);
	let slug = slugify(title);

	// Check slug collision
	const existing = await sql.unsafe(
		`SELECT 1 FROM rsplat.splats WHERE slug = $1`, [slug]
	);
	if (existing.length > 0) {
		slug = `${slug}-${shortId}`;
	}

	// Save source files to disk
	const sourceDir = resolve(SOURCES_DIR, slug);
	await mkdir(sourceDir, { recursive: true });

	const sourceRows: { path: string; name: string; mime: string; size: number }[] = [];
	for (const f of files) {
		const safeName = f.name.replace(/[^a-zA-Z0-9._-]/g, "_");
		const filepath = resolve(sourceDir, safeName);
		const buffer = Buffer.from(await f.arrayBuffer());
		await Bun.write(filepath, buffer);
		sourceRows.push({
			path: `sources/${slug}/${safeName}`,
			name: f.name,
			mime: f.type,
			size: buffer.length,
		});
	}

	// Insert splat record (pending processing)
	const paymentTx = (c as any).get("x402Payment") || null;
	const splatRows = await sql.unsafe(
		`INSERT INTO rsplat.splats (slug, title, description, file_path, file_format, file_size_bytes, tags, space_slug, contributor_id, contributor_name, source, processing_status, source_file_count, payment_tx)
		 VALUES ($1, $2, $3, '', 'ply', 0, $4, $5, $6, $7, 'media', 'pending', $8, $9)
		 RETURNING id, slug, title, description, file_format, tags, processing_status, source_file_count, created_at`,
		[slug, title, description, tags, spaceSlug, claims.sub, claims.username || null, files.length, paymentTx] as any[]
	);

	const splatId = splatRows[0].id;

	// Insert source file records
	for (const sf of sourceRows) {
		await sql.unsafe(
			`INSERT INTO rsplat.source_files (splat_id, file_path, file_name, mime_type, file_size_bytes)
			 VALUES ($1, $2, $3, $4, $5)`,
			[splatId, sf.path, sf.name, sf.mime, sf.size]
		);
	}

	return c.json(splatRows[0], 201);
});

// ── API: Delete splat (owner only) ──
routes.delete("/api/splats/:id", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);

	let claims;
	try {
		claims = await verifyEncryptIDToken(token);
	} catch {
		return c.json({ error: "Invalid token" }, 401);
	}

	const id = c.req.param("id");
	const rows = await sql.unsafe(
		`SELECT id, contributor_id FROM rsplat.splats WHERE (slug = $1 OR id::text = $1) AND status = 'published'`,
		[id]
	);

	if (rows.length === 0) return c.json({ error: "Splat not found" }, 404);
	if (rows[0].contributor_id !== claims.sub) {
		return c.json({ error: "Not authorized" }, 403);
	}

	await sql.unsafe(
		`UPDATE rsplat.splats SET status = 'removed' WHERE id = $1`,
		[rows[0].id]
	);

	return c.json({ ok: true });
});

// ── Page: Gallery ──
routes.get("/", async (c) => {
	const spaceSlug = c.req.param("space") || "demo";

	const rows = await sql.unsafe(
		`SELECT id, slug, title, description, file_format, file_size_bytes,
			tags, contributor_name, view_count, processing_status, source_file_count, created_at
		 FROM rsplat.splats WHERE status = 'published' AND space_slug = $1
		 ORDER BY created_at DESC LIMIT 50`,
		[spaceSlug]
	);

	const splatsJSON = JSON.stringify(rows);

	const html = renderShell({
		title: `${spaceSlug} — rSplat | rSpace`,
		moduleId: "rsplat",
		spaceSlug,
		body: `<folk-splat-viewer id="gallery" mode="gallery"></folk-splat-viewer>`,
		modules: getModuleInfoList(),
		theme: "dark",
		head: `
			<link rel="stylesheet" href="/modules/rsplat/splat.css">
			${IMPORTMAP}
		`,
		scripts: `
			<script type="module">
				import { FolkSplatViewer } from '/modules/rsplat/folk-splat-viewer.js';
				const gallery = document.getElementById('gallery');
				gallery.splats = ${splatsJSON};
				gallery.spaceSlug = '${spaceSlug}';
			</script>
		`,
	});

	return c.html(html);
});

// ── Page: Viewer ──
routes.get("/view/:id", async (c) => {
	const spaceSlug = c.req.param("space") || "demo";
	const id = c.req.param("id");

	const rows = await sql.unsafe(
		`SELECT * FROM rsplat.splats WHERE (slug = $1 OR id::text = $1) AND status = 'published'`,
		[id]
	);

	if (rows.length === 0) {
		const html = renderShell({
			title: "Splat not found | rSpace",
			moduleId: "rsplat",
			spaceSlug,
			body: `<div style="padding:3rem;text-align:center;color:#94a3b8;"><h2>Splat not found</h2><p><a href="/${spaceSlug}/rsplat" style="color:#818cf8;">Back to gallery</a></p></div>`,
			modules: getModuleInfoList(),
			theme: "dark",
		});
		return c.html(html, 404);
	}

	const splat = rows[0];

	// Increment view count
	await sql.unsafe(
		`UPDATE rsplat.splats SET view_count = view_count + 1 WHERE id = $1`,
		[splat.id]
	);

	const fileUrl = `/${spaceSlug}/rsplat/api/splats/${splat.slug}/${splat.slug}.${splat.file_format}`;

	const html = renderShell({
		title: `${splat.title} | rSplat`,
		moduleId: "rsplat",
		spaceSlug,
		body: `
			<folk-splat-viewer
				id="viewer"
				mode="viewer"
				splat-url="${escapeAttr(fileUrl)}"
				splat-title="${escapeAttr(splat.title)}"
				splat-desc="${escapeAttr(splat.description || '')}"
				space-slug="${escapeAttr(spaceSlug)}"
			></folk-splat-viewer>
		`,
		modules: getModuleInfoList(),
		theme: "dark",
		head: `
			<link rel="stylesheet" href="/modules/rsplat/splat.css">
			${IMPORTMAP}
		`,
		scripts: `
			<script type="module">
				import { FolkSplatViewer } from '/modules/rsplat/folk-splat-viewer.js';
			</script>
		`,
	});

	return c.html(html);
});

// ── Initialize DB schema ──
async function initDB(): Promise<void> {
	try {
		const schemaPath = resolve(import.meta.dir, "db/schema.sql");
		const schemaSql = await readFile(schemaPath, "utf-8");
		await sql.unsafe(`SET search_path TO rsplat, public`);
		await sql.unsafe(schemaSql);
		// Migration: add new columns to existing table
		await sql.unsafe(`ALTER TABLE rsplat.splats ADD COLUMN IF NOT EXISTS processing_status TEXT DEFAULT 'ready'`);
		await sql.unsafe(`ALTER TABLE rsplat.splats ADD COLUMN IF NOT EXISTS processing_error TEXT`);
		await sql.unsafe(`ALTER TABLE rsplat.splats ADD COLUMN IF NOT EXISTS source_file_count INTEGER DEFAULT 0`);
		await sql.unsafe(`SET search_path TO public`);
		console.log("[Splat] Database schema initialized");
	} catch (e) {
		console.error("[Splat] Schema init failed:", e);
	}
}

// ── Module export ──

export const splatModule: RSpaceModule = {
	id: "rsplat",
	name: "rSplat",
	icon: "🔮",
	description: "3D Gaussian splat viewer",
	scoping: { defaultScope: 'global', userConfigurable: true },
	routes,
	landingPage: renderLanding,
	standaloneDomain: "rsplat.online",
	hidden: true,
	outputPaths: [
		{ path: "drawings", name: "Drawings", icon: "🔮", description: "3D Gaussian splat drawings" },
	],

	async onSpaceCreate(ctx: SpaceLifecycleContext) {
		// Splats are scoped by space_slug column. No per-space setup needed.
	},
};

// Run schema init on import
initDB();
