import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import { Readable } from "node:stream";

import { getSession } from "@/lib/auth/session";
import { canAccessBranch } from "@/lib/auth/permissions";
import {
	withErrorHandling,
	badRequest,
	unauthorized,
	forbidden,
	notFound,
	ApiError,
} from "@/lib/api/errors";
import { mapStorageReadError } from "@/lib/api/storageErrors";

export const dynamic = "force-dynamic";
export const runtime = "nodejs";

const BRANCH_RE = /^NL\d+$/;
const YEAR_RE = /^\d{4}$/;
const MONTH_RE = /^(0[1-9]|1[0-2])$/;
const DAY_RE = /^(0[1-9]|[12]\d|3[01])$/;

function getNasRootOrThrow() {
	const root = process.env.NAS_ROOT_PATH;

	if (!root) {
		throw new ApiError({
			status: 500,
			code: "FS_STORAGE_ERROR",
			message: "Internal server error",
		});
	}

	return root;
}

function isSafeFilename(name) {
	if (typeof name !== "string") return false;

	const trimmed = name.trim();
	if (!trimmed) return false;

	// Reject special path segments
	if (trimmed === "." || trimmed === "..") return false;

	// Reject any path separators (defense-in-depth)
	if (trimmed.includes("/") || trimmed.includes("\\")) return false;

	// Reject control chars (header injection)
	if (/[\r\n\t]/.test(trimmed)) return false;

	// Reject quotes to keep Content-Disposition predictable/safe
	if (trimmed.includes('"')) return false;

	// Ensure it's a basename (no sneaky segments)
	if (path.basename(trimmed) !== trimmed) return false;

	return true;
}

function isPdfFilename(name) {
	return typeof name === "string" && name.toLowerCase().endsWith(".pdf");
}

function validateParamsOrThrow({ branch, year, month, day, filename }) {
	if (!BRANCH_RE.test(branch)) {
		throw badRequest("VALIDATION_BRANCH", "Invalid branch parameter", {
			branch,
		});
	}

	if (!YEAR_RE.test(year)) {
		throw badRequest("VALIDATION_YEAR", "Invalid year parameter", { year });
	}

	if (!MONTH_RE.test(month)) {
		throw badRequest("VALIDATION_MONTH", "Invalid month parameter", { month });
	}

	if (!DAY_RE.test(day)) {
		throw badRequest("VALIDATION_DAY", "Invalid day parameter", { day });
	}

	if (!isSafeFilename(filename)) {
		throw badRequest("VALIDATION_FILENAME", "Invalid filename parameter", {
			filename,
		});
	}

	if (!isPdfFilename(filename)) {
		throw badRequest(
			"VALIDATION_FILE_EXTENSION",
			"Only PDF files are allowed",
			{ filename }
		);
	}
}

function resolvePdfPathOrThrow({ root, branch, year, month, day, filename }) {
	const rootAbs = path.resolve(root);
	const absPath = path.resolve(rootAbs, branch, year, month, day, filename);

	// Ensure the resolved path stays within NAS_ROOT_PATH
	const rel = path.relative(rootAbs, absPath);
	if (rel.startsWith("..") || path.isAbsolute(rel)) {
		throw badRequest("VALIDATION_PATH_TRAVERSAL", "Invalid file path", {
			branch,
			year,
			month,
			day,
			filename,
		});
	}

	return absPath;
}

/**
 * Content-Disposition helper (Unicode-safe).
 *
 * Problem:
 * - Node's Web Response headers require ByteString-compatible values.
 * - Unicode characters (e.g. "€") in `filename="..."` can crash the response creation.
 *
 * Solution:
 * - Provide an ASCII fallback via `filename="..."`.
 * - Provide the real UTF-8 name via RFC 5987: `filename*=UTF-8''...`.
 */
function stripDiacritics(input) {
	return String(input)
		.normalize("NFKD")
		.replace(/[\u0300-\u036f]/g, "");
}

function toAsciiFallbackFilename(filename) {
	// Keep it predictable and safe for headers: ASCII only.
	// We also keep the .pdf extension if possible.
	const raw = stripDiacritics(filename);

	const ascii = raw
		.replace(/[^\x20-\x7E]/g, "_") // replace non-ASCII with underscore
		.replace(/\s+/g, " ") // collapse whitespace
		.replace(/_+/g, "_") // collapse underscores
		.trim();

	if (!ascii) return "download.pdf";
	if (!ascii.toLowerCase().endsWith(".pdf")) return `${ascii}.pdf`;

	return ascii;
}

function encodeRFC5987ValueChars(str) {
	// RFC 5987 encoding for header parameters:
	// Use percent-encoded UTF-8 bytes and additionally encode a few chars that
	// encodeURIComponent leaves as-is but can be problematic in headers.
	return encodeURIComponent(str)
		.replace(/['()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
		.replace(/\*/g, "%2A");
}

function buildContentDisposition(filename, asAttachment) {
	const type = asAttachment ? "attachment" : "inline";
	const fallback = toAsciiFallbackFilename(filename);
	const encoded = encodeRFC5987ValueChars(filename);

	return `${type}; filename="${fallback}"; filename*=UTF-8''${encoded}`;
}

/**
 * GET /api/files/:branch/:year/:month/:day/:filename
 *
 * Query (optional):
 * - download=1 | download=true  => Content-Disposition: attachment
 * - default => inline
 */
export const GET = withErrorHandling(
	async function GET(request, ctx) {
		const session = await getSession();

		if (!session) {
			throw unauthorized("AUTH_UNAUTHENTICATED", "Unauthorized");
		}

		const { branch, year, month, day, filename } = await ctx.params;

		const missing = [];
		if (!branch) missing.push("branch");
		if (!year) missing.push("year");
		if (!month) missing.push("month");
		if (!day) missing.push("day");
		if (!filename) missing.push("filename");

		if (missing.length > 0) {
			throw badRequest(
				"VALIDATION_MISSING_PARAM",
				"Missing required route parameter(s)",
				{ params: missing }
			);
		}

		if (!canAccessBranch(session, branch)) {
			throw forbidden("AUTH_FORBIDDEN_BRANCH", "Forbidden");
		}

		validateParamsOrThrow({ branch, year, month, day, filename });

		const root = getNasRootOrThrow();
		const absPath = resolvePdfPathOrThrow({
			root,
			branch,
			year,
			month,
			day,
			filename,
		});

		const details = { branch, year, month, day, filename };

		let stat;
		try {
			stat = await fsp.stat(absPath);
		} catch (err) {
			throw await mapStorageReadError(err, { details });
		}

		if (!stat.isFile()) {
			throw notFound("FS_NOT_FOUND", "Not found", details);
		}

		const { searchParams } = new URL(request.url);
		const download = (searchParams.get("download") || "").toLowerCase();
		const asAttachment = download === "1" || download === "true";

		const contentDisposition = buildContentDisposition(filename, asAttachment);

		const nodeStream = fs.createReadStream(absPath);
		const webStream = Readable.toWeb(nodeStream);

		return new Response(webStream, {
			status: 200,
			headers: {
				"Content-Type": "application/pdf",
				"Content-Disposition": contentDisposition,
				"Content-Length": String(stat.size),
				"Cache-Control": "no-store",
				"X-Content-Type-Options": "nosniff",
			},
		});
	},
	{ logPrefix: "[api/files/[branch]/[year]/[month]/[day]/[filename]]" }
);
