import { App } from "@tinyhttp/app";
import { cors } from "@tinyhttp/cors";
import { cookieParser } from "@tinyhttp/cookie-parser";
import dotenv from "dotenv";
import { inspect } from "util";
import admin from "./routes/admin.js";
import alliance from "./routes/alliance.js";
import auth from "./routes/auth.js";
import experiments from "./routes/experiments.js";
import favoriteLocation from "./routes/favorite-location.js";
import leaderboard from "./routes/leaderboard.js";
import me from "./routes/me.js";
import moderator from "./routes/moderator.js";
import pixel from "./routes/pixel.js";
import store from "./routes/store.js";
import { setupSiteContentRoutes } from "./routes/site-content.js";
import { setupRulesRoutes } from "./routes/rules.js";
import { setupPluginRoutes } from "./routes/plugins.js";
import { addPrismaToRequest } from "./config/database.js";
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";

dotenv.config();

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const frontendDir = process.env["USE_FRONTEND_BACKUP"] === "true" ? "frontend-backup" : "frontend";
const frontendPath = path.join(__dirname, "..", frontendDir);
const normalizedFrontendPath = path.resolve(frontendPath);

const app = new App({
	noMatchHandler: async (_req, res) => {
		const html = await fs.readFile(path.join(frontendPath, "404.html"), "utf8");
		return res.status(404)
			.setHeader("Content-Type", "text/html")
			.send(html);
	}
});

app.use(cors());
app.use(cookieParser());

const MAX_JSON_BODY_BYTES = Number(process.env["MAX_JSON_BODY_BYTES"] ?? 1_048_576);
const METHODS_WITH_BODY = new Set(["POST", "PUT", "PATCH", "DELETE"]);

app.use((req, res, next) => {
	req.body = {};

	if (!METHODS_WITH_BODY.has(req.method ?? "")) {
		return next?.();
	}

	const rawContentType = req.headers["content-type"] ?? "";
	const contentType = Array.isArray(rawContentType)
		? rawContentType[0] ?? ""
		: rawContentType;
	const isJson = contentType.toLowerCase().startsWith("application/json");

	if (!isJson) {
		return next?.();
	}

	let totalBytes = 0;
	let body = "";
	let requestClosed = false;

	const handleTooLarge = () => {
		if (requestClosed) {
			return;
		}
		requestClosed = true;
		res.status(413)
			.json({ error: "Payload too large", status: 413 });
		req.destroy();
	};

	req.on("data", chunk => {
		if (requestClosed) {
			return;
		}
		totalBytes += chunk.length;
		if (totalBytes > MAX_JSON_BODY_BYTES) {
			handleTooLarge();
			return;
		}
		body += chunk.toString("utf8");
	});

	req.on("end", () => {
		if (requestClosed) {
			return;
		}

		if (!body) {
			return next?.();
		}

		try {
			req.body = JSON.parse(body);
		} catch {
			requestClosed = true;
			return res.status(400)
				.json({ error: "Invalid JSON payload", status: 400 });
		}

		return next?.();
	});

	req.on("error", () => {
		if (requestClosed) {
			return;
		}
		requestClosed = true;
		return res.status(400)
			.json({ error: "Failed to read request body", status: 400 });
	});
});

const SENSITIVE_HEADER_KEYS = new Set(["authorization", "cookie", "set-cookie"]);

function sanitizeHeaders(headers: Record<string, unknown>) {
	const result: Record<string, unknown> = {};
	for (const [key, value] of Object.entries(headers)) {
		if (SENSITIVE_HEADER_KEYS.has(key.toLowerCase())) {
			result[key] = "[REDACTED]";
		} else {
			result[key] = value;
		}
	}
	return result;
}

app.use((req, res, next) => {
	const startTime = Date.now();
	const requestId = req.get("x-forwarded-for") || req.ip || req.socket.remoteAddress || "unknown";
	const inspectOptions = { colors: false, compact: true, breakLength: Number.POSITIVE_INFINITY } as const;

	console.log(`[${requestId}] ${req.method} ${req.url}`);
	console.log(`[${requestId}] Headers:`, inspect(sanitizeHeaders(req.headers as Record<string, unknown>), inspectOptions));

	if (req.body && typeof req.body === "object" && Object.keys(req.body).length > 0) {
		console.log(`[${requestId}] Body keys:`, Object.keys(req.body));
	}

	res.on("finish", () => {
		const duration = Date.now() - startTime;
		console.log(`[${requestId}] Response ${res.statusCode} [${duration}ms]`);
	});

	return next?.();
});

app.use(addPrismaToRequest);

const STATIC_ROUTE_PREFIXES = [
	"/api",
	"/auth",
	"/s0",
	"/me",
	"/alliance",
	"/leaderboard",
	"/favorite-location",
	"/purchase",
	"/flag",
	"/report-user",
	"/experiments",
	"/moderator",
	"/admin"
];

app.use(async (req, res, next) => {
	const method = req.method?.toUpperCase() ?? "GET";
	if (!["GET", "HEAD"].includes(method)) {
		return next?.();
	}

	const requestUrl = req.url ?? "/";
	if (STATIC_ROUTE_PREFIXES.some(prefix => requestUrl.startsWith(prefix))) {
		return next?.();
	}

	let pathname: string;
	try {
		const parsedUrl = new URL(requestUrl, "http://localhost");
		pathname = decodeURIComponent(parsedUrl.pathname);
	} catch {
		return next?.();
	}

	const trimmedPath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
	const resolvedPath = path.resolve(normalizedFrontendPath, `.${path.sep}${trimmedPath}`);
	const relativeResolvedPath = path.relative(normalizedFrontendPath, resolvedPath);

	if (relativeResolvedPath.startsWith("..") || path.isAbsolute(relativeResolvedPath)) {
		return next?.();
	}

	let filePath = resolvedPath;

	try {
		let stats = await fs.stat(filePath);

		if (stats.isDirectory()) {
			filePath = path.join(filePath, "index.html");
			stats = await fs.stat(filePath);
		}

		if (!stats.isFile()) {
			return next?.();
		}

		const ext = path.extname(filePath).toLowerCase();
		const mimeTypes: Record<string, string> = {
			".html": "text/html",
			".js": "application/javascript",
			".css": "text/css",
			".json": "application/json",
			".png": "image/png",
			".jpg": "image/jpeg",
			".jpeg": "image/jpeg",
			".svg": "image/svg+xml",
			".ico": "image/x-icon",
			".woff": "font/woff",
			".woff2": "font/woff2",
			".webmanifest": "application/manifest+json"
		};

		const content = await fs.readFile(filePath);
		res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream");
		res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
		res.setHeader("Pragma", "no-cache");
		res.setHeader("Expires", "0");

		// Add CSP header for HTML files to allow captcha scripts
		if (ext === ".html") {
			res.setHeader("Content-Security-Policy",
				"script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://challenges.cloudflare.com https://js.hcaptcha.com https://*.hcaptcha.com https://www.google.com https://www.gstatic.com blob:; " +
				"frame-src 'self' https://challenges.cloudflare.com https://*.hcaptcha.com https://www.google.com https://www.gstatic.com https://www.youtube.com; " +
				"style-src 'self' 'unsafe-inline' https://*.hcaptcha.com https://www.gstatic.com; " +
				"connect-src 'self' https://*.hcaptcha.com https://www.google.com https://www.gstatic.com https://tiles.openfreemap.org;"
			);
		}

		if (method === "HEAD") {
			res.setHeader("Content-Length", content.length.toString());
			return res.status(200).end();
		}

		return res.send(content);
	} catch {
		return next?.();
	}
});

// Serve admin and moderation HTML pages
app.get("/admin", async (_req, res) => {
	try {
		const html = await fs.readFile(path.join(frontendPath, "admin.html"), "utf8");
		return res.setHeader("Content-Type", "text/html").send(html);
	} catch (error) {
		console.error("Error serving admin.html:", error);
		return res.status(404).send("Page not found");
	}
});

app.get("/moderation", async (_req, res) => {
	try {
		const html = await fs.readFile(path.join(frontendPath, "moderation.html"), "utf8");
		return res.setHeader("Content-Type", "text/html").send(html);
	} catch (error) {
		console.error("Error serving moderation.html:", error);
		return res.status(404).send("Page not found");
	}
});

admin(app);
alliance(app);
auth(app);
experiments(app);
favoriteLocation(app);
leaderboard(app);
me(app);
moderator(app);
pixel(app);
store(app);
setupSiteContentRoutes(app);
setupRulesRoutes(app);
setupPluginRoutes(app);

const PORT = Number(process.env["PORT"]) || 3000;

app.listen(PORT, () => {
	console.log(`Server running on port ${PORT}`);
});

