/**
 * Files module — file sharing, public share links, memory cards.
 * Ported from rfiles-online (Django → Bun/Hono).
 */

import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { mkdir, writeFile, unlink } from "node:fs/promises";
import { createHash, randomBytes } from "node:crypto";
import { sql } from "../../shared/db/pool";
import { renderShell, renderExternalAppShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";

const routes = new Hono();

const FILES_DIR = process.env.FILES_DIR || "/data/files";
const SCHEMA_SQL = readFileSync(resolve(import.meta.dir, "db/schema.sql"), "utf-8");

// ── DB initialization ──
async function initDB() {
	try {
		await sql.unsafe(SCHEMA_SQL);
		console.log("[Files] DB schema initialized");
	} catch (e: any) {
		console.error("[Files] DB init error:", e.message);
	}
}
initDB();

// ── Cleanup timers (replace Celery) ──
// Deactivate expired shares every hour
setInterval(async () => {
	try {
		const result = await sql.unsafe(
			"UPDATE rfiles.public_shares SET is_active = FALSE WHERE is_active = TRUE AND expires_at IS NOT NULL AND expires_at < NOW()"
		);
		if ((result as any).count > 0) console.log(`[Files] Deactivated ${(result as any).count} expired shares`);
	} catch (e: any) { console.error("[Files] Cleanup error:", e.message); }
}, 3600_000);

// Delete access logs older than 90 days, daily
setInterval(async () => {
	try {
		await sql.unsafe("DELETE FROM rfiles.access_logs WHERE accessed_at < NOW() - INTERVAL '90 days'");
	} 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");
}

// ── 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 verifyEncryptIDToken(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 tags = formData.get("tags")?.toString() || "[]";
	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 [row] = await sql.unsafe(
		`INSERT INTO rfiles.media_files (original_filename, title, description, mime_type, file_size, file_hash, storage_path, tags, uploaded_by, shared_space)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10) RETURNING *`,
		[file.name, title, description, file.type || "application/octet-stream", file.size, fileHash, storagePath, tags, uploadedBy, space]
	);

	return c.json({ file: row }, 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;

	let query = "SELECT * FROM rfiles.media_files WHERE shared_space = $1";
	const params: any[] = [space];
	let paramIdx = 2;

	if (mimeType) {
		query += ` AND mime_type LIKE $${paramIdx}`;
		params.push(`${mimeType}%`);
		paramIdx++;
	}

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

	const rows = await sql.unsafe(query, params);
	const [{ count }] = await sql.unsafe(
		"SELECT COUNT(*) as count FROM rfiles.media_files WHERE shared_space = $1",
		[space]
	);

	return c.json({ files: rows, total: Number(count), limit, offset });
});

// ── File download ──
routes.get("/api/files/:id/download", async (c) => {
	const [file] = await sql.unsafe("SELECT * FROM rfiles.media_files WHERE id = $1", [c.req.param("id")]);
	if (!file) return c.json({ error: "File not found" }, 404);

	const fullPath = resolve(FILES_DIR, file.storage_path);
	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.mime_type || "application/octet-stream",
			"Content-Disposition": `attachment; filename="${file.original_filename}"`,
			"Content-Length": String(file.file_size),
		},
	});
});

// ── File detail ──
routes.get("/api/files/:id", async (c) => {
	const [file] = await sql.unsafe("SELECT * FROM rfiles.media_files WHERE id = $1", [c.req.param("id")]);
	if (!file) return c.json({ error: "File not found" }, 404);
	return c.json({ file });
});

// ── File delete ──
routes.delete("/api/files/:id", async (c) => {
	const [file] = await sql.unsafe("SELECT * FROM rfiles.media_files WHERE id = $1", [c.req.param("id")]);
	if (!file) return c.json({ error: "File not found" }, 404);

	try { await unlink(resolve(FILES_DIR, file.storage_path)); } catch {}
	await sql.unsafe("DELETE FROM rfiles.media_files WHERE id = $1", [c.req.param("id")]);
	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 verifyEncryptIDToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }

	const [file] = await sql.unsafe("SELECT * FROM rfiles.media_files WHERE id = $1", [c.req.param("id")]);
	if (!file) return c.json({ error: "File not found" }, 404);
	if (file.uploaded_by && file.uploaded_by !== 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 token = generateToken();
	const expiresAt = body.expires_in_hours ? new Date(Date.now() + body.expires_in_hours * 3600_000).toISOString() : null;
	const createdBy = claims.sub;

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

	const [share] = await sql.unsafe(
		`INSERT INTO rfiles.public_shares (token, media_file_id, created_by, expires_at, max_downloads, is_password_protected, password_hash, note)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
		[token, file.id, createdBy, expiresAt, body.max_downloads || null, isPasswordProtected, passwordHash, body.note || null]
	);

	await sql.unsafe(
		"INSERT INTO rfiles.access_logs (media_file_id, share_id, access_type) VALUES ($1, $2, 'share_created')",
		[file.id, share.id]
	);

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

// ── List shares for a file ──
routes.get("/api/files/:id/shares", async (c) => {
	const rows = await sql.unsafe(
		"SELECT * FROM rfiles.public_shares WHERE media_file_id = $1 ORDER BY created_at DESC",
		[c.req.param("id")]
	);
	return c.json({ shares: rows });
});

// ── 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 verifyEncryptIDToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }

	const [share] = await sql.unsafe(
		"SELECT s.*, f.uploaded_by FROM rfiles.public_shares s JOIN rfiles.media_files f ON s.media_file_id = f.id WHERE s.id = $1",
		[c.req.param("shareId")]
	);
	if (!share) return c.json({ error: "Share not found" }, 404);
	if (share.uploaded_by && share.uploaded_by !== claims.sub) return c.json({ error: "Not authorized" }, 403);

	const [revoked] = await sql.unsafe(
		"UPDATE rfiles.public_shares SET is_active = FALSE WHERE id = $1 RETURNING *",
		[c.req.param("shareId")]
	);
	return c.json({ message: "Revoked", share: revoked });
});

// ── Public share download ──
routes.get("/s/:token", async (c) => {
	const [share] = await sql.unsafe(
		`SELECT s.*, f.storage_path, f.mime_type, f.original_filename, f.file_size
		 FROM rfiles.public_shares s JOIN rfiles.media_files f ON s.media_file_id = f.id
		 WHERE s.token = $1`,
		[c.req.param("token")]
	);
	if (!share) return c.json({ error: "Share not found" }, 404);
	if (!share.is_active) return c.json({ error: "Share has been revoked" }, 410);
	if (share.expires_at && new Date(share.expires_at) < new Date()) return c.json({ error: "Share has expired" }, 410);
	if (share.max_downloads && share.download_count >= share.max_downloads) return c.json({ error: "Download limit reached" }, 410);

	if (share.is_password_protected) {
		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 !== share.password_hash) return c.json({ error: "Invalid password" }, 401);
	}

	await sql.unsafe("UPDATE rfiles.public_shares SET download_count = download_count + 1 WHERE id = $1", [share.id]);
	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") || "";
	await sql.unsafe(
		"INSERT INTO rfiles.access_logs (media_file_id, share_id, ip_address, user_agent, access_type) VALUES ($1, $2, $3, $4, 'download')",
		[share.media_file_id, share.id, ip, ua.slice(0, 500)]
	);

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

	return new Response(bunFile, {
		headers: {
			"Content-Type": share.mime_type || "application/octet-stream",
			"Content-Disposition": `attachment; filename="${share.original_filename}"`,
			"Content-Length": String(share.file_size),
		},
	});
});

// ── Share info (public) ──
routes.get("/s/:token/info", async (c) => {
	const [share] = await sql.unsafe(
		`SELECT s.is_password_protected, s.is_active, s.expires_at, s.max_downloads, s.download_count, s.note,
		        f.original_filename, f.mime_type, f.file_size
		 FROM rfiles.public_shares s JOIN rfiles.media_files f ON s.media_file_id = f.id
		 WHERE s.token = $1`,
		[c.req.param("token")]
	);
	if (!share) return c.json({ error: "Share not found" }, 404);

	const isValid = share.is_active &&
		(!share.expires_at || new Date(share.expires_at) > new Date()) &&
		(!share.max_downloads || share.download_count < share.max_downloads);

	return c.json({
		is_password_protected: share.is_password_protected,
		is_valid: isValid,
		expires_at: share.expires_at,
		downloads_remaining: share.max_downloads ? share.max_downloads - share.download_count : null,
		file_info: { filename: share.original_filename, mime_type: share.mime_type, size: share.file_size },
		note: share.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 verifyEncryptIDToken(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 [card] = await sql.unsafe(
		`INSERT INTO rfiles.memory_cards (shared_space, title, body, card_type, tags, created_by)
		 VALUES ($1, $2, $3, $4, $5::jsonb, $6) RETURNING *`,
		[space, body.title, body.body || "", body.card_type || "note", JSON.stringify(body.tags || []), createdBy]
	);
	return c.json({ 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);

	let query = "SELECT * FROM rfiles.memory_cards WHERE shared_space = $1";
	const params: any[] = [space];
	if (cardType) { query += " AND card_type = $2"; params.push(cardType); }
	query += " ORDER BY position, created_at DESC LIMIT $" + (params.length + 1);
	params.push(limit);

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

routes.patch("/api/cards/:id", async (c) => {
	const body = await c.req.json<{ title?: string; body?: string; card_type?: string; tags?: string[]; position?: number }>();
	const sets: string[] = [];
	const params: any[] = [];
	let idx = 1;

	if (body.title !== undefined) { sets.push(`title = $${idx}`); params.push(body.title); idx++; }
	if (body.body !== undefined) { sets.push(`body = $${idx}`); params.push(body.body); idx++; }
	if (body.card_type !== undefined) { sets.push(`card_type = $${idx}`); params.push(body.card_type); idx++; }
	if (body.tags !== undefined) { sets.push(`tags = $${idx}::jsonb`); params.push(JSON.stringify(body.tags)); idx++; }
	if (body.position !== undefined) { sets.push(`position = $${idx}`); params.push(body.position); idx++; }

	if (sets.length === 0) return c.json({ error: "No fields to update" }, 400);
	sets.push(`updated_at = NOW()`);
	params.push(c.req.param("id"));

	const [card] = await sql.unsafe(
		`UPDATE rfiles.memory_cards SET ${sets.join(", ")} WHERE id = $${idx} RETURNING *`,
		params
	);
	if (!card) return c.json({ error: "Card not found" }, 404);
	return c.json({ card });
});

routes.delete("/api/cards/:id", async (c) => {
	const [card] = await sql.unsafe("DELETE FROM rfiles.memory_cards WHERE id = $1 RETURNING id", [c.req.param("id")]);
	if (!card) return c.json({ error: "Card not found" }, 404);
	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"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rfiles/files.css">`,
	}));
});

export const filesModule: RSpaceModule = {
	id: "rfiles",
	name: "rFiles",
	icon: "📁",
	description: "File sharing, share links, and memory cards",
	scoping: { defaultScope: 'space', userConfigurable: false },
	routes,
	landingPage: renderLanding,
	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" },
	],
};
