import fs from "node:fs/promises";
import path from "node:path";

// Root directory of the NAS share, injected via environment variable.
// On the Linux app server, this is typically `/mnt/niederlassungen`.
// Do NOT cache process.env.NAS_ROOT_PATH at module load time.
// Instead, resolve it on demand so tests (and runtime) can change it.
function getRoot() {
	const root = process.env.NAS_ROOT_PATH;

	if (!root) {
		throw new Error("NAS_ROOT_PATH environment variable is not set");
	}

	return root;
}

// Build an absolute path below the NAS root from a list of segments.
function fullPath(...segments) {
	const root = getRoot();
	return path.join(root, ...segments.map(String));
}

// Compare strings that represent numbers in a numeric way.
// This ensures "2" comes before "10" (2 < 10), not after.
function sortNumericStrings(a, b) {
	const na = parseInt(a, 10);
	const nb = parseInt(b, 10);

	if (!Number.isNaN(na) && !Number.isNaN(nb)) {
		return na - nb;
	}

	// Fallback to localeCompare if parsing fails
	return a.localeCompare(b, "en");
}

// -----------------------------------------------------------------------------
// RHL-006: Storage micro-cache (process-local TTL cache)
// -----------------------------------------------------------------------------

const TTL_BRANCHES_MS = 60_000;
const TTL_YEARS_MS = 60_000;
const TTL_MONTHS_MS = 15_000;
const TTL_DAYS_MS = 15_000;
const TTL_FILES_MS = 15_000;

// Internal cache store:
// key -> { expiresAt, value } OR { expiresAt, promise }
// - value: resolved cache value
// - promise: in-flight load promise to collapse concurrent reads
const __storageCache = new Map();

/**
 * Build a stable cache key for a given listing type.
 *
 * We include NAS_ROOT_PATH in the key so tests that change the env var do not
 * accidentally reuse data from a previous test run.
 *
 * @param {string} type
 * @param  {...string} parts
 * @returns {string}
 */
function buildCacheKey(type, ...parts) {
	const root = getRoot();
	return [type, root, ...parts.map(String)].join("|");
}

/**
 * Generic TTL-cache wrapper.
 *
 * Behavior:
 * 1) If a load is already in-flight (promise exists), reuse it.
 * 2) If a cached value exists and is not expired, return it.
 * 3) Otherwise run loader(), store the result, and return it.
 *
 * Failure policy:
 * - If loader() throws, the cache entry is removed so later calls can retry.
 *
 * @template T
 * @param {string} key
 * @param {number} ttlMs
 * @param {() => Promise<T>} loader
 * @returns {Promise<T>}
 */
async function withTtlCache(key, ttlMs, loader) {
	const now = Date.now();
	const existing = __storageCache.get(key);

	// 1) Collapsing concurrent calls:
	if (existing?.promise) {
		return existing.promise;
	}

	// 2) Serve cached values while still fresh:
	if (existing && existing.value !== undefined && existing.expiresAt > now) {
		return existing.value;
	}

	// 3) Cache miss or expired: start a new load.
	const promise = (async () => {
		try {
			const value = await loader();

			__storageCache.set(key, {
				value,
				expiresAt: Date.now() + ttlMs,
			});

			return value;
		} catch (err) {
			__storageCache.delete(key);
			throw err;
		}
	})();

	__storageCache.set(key, {
		promise,
		expiresAt: now + ttlMs,
	});

	return promise;
}

/**
 * TEST-ONLY helper: clear the micro-cache.
 */
export function __clearStorageCacheForTests() {
	__storageCache.clear();
}

// -----------------------------------------------------------------------------
// 1. Branches (NL01, NL02, ...)
// -----------------------------------------------------------------------------

export async function listBranches() {
	return withTtlCache(buildCacheKey("branches"), TTL_BRANCHES_MS, async () => {
		const entries = await fs.readdir(fullPath(), { withFileTypes: true });

		return entries
			.filter(
				(entry) =>
					entry.isDirectory() &&
					entry.name !== "@Recently-Snapshot" &&
					/^NL\d+$/.test(entry.name) // strict: UI expects "NL" uppercase, Linux FS is case-sensitive
			)
			.map((entry) => entry.name)
			.sort((a, b) => sortNumericStrings(a.slice(2), b.slice(2)));
	});
}

// -----------------------------------------------------------------------------
// 2. Years (2023, 2024, ...)
// -----------------------------------------------------------------------------

export async function listYears(branch) {
	return withTtlCache(
		buildCacheKey("years", branch),
		TTL_YEARS_MS,
		async () => {
			const dir = fullPath(branch);
			const entries = await fs.readdir(dir, { withFileTypes: true });

			return entries
				.filter((entry) => entry.isDirectory() && /^\d{4}$/.test(entry.name))
				.map((entry) => entry.name)
				.sort(sortNumericStrings);
		}
	);
}

// -----------------------------------------------------------------------------
// 3. Months (01–12)
// -----------------------------------------------------------------------------

export async function listMonths(branch, year) {
	return withTtlCache(
		buildCacheKey("months", branch, year),
		TTL_MONTHS_MS,
		async () => {
			const dir = fullPath(branch, year);
			const entries = await fs.readdir(dir, { withFileTypes: true });

			// Important: filter to valid calendar months (1–12) so the UI never gets
			// impossible values that would be rejected by route param validation.
			return entries
				.filter((entry) => entry.isDirectory() && /^\d{1,2}$/.test(entry.name))
				.map((entry) => entry.name.trim())
				.filter((raw) => {
					const n = parseInt(raw, 10);
					return Number.isInteger(n) && n >= 1 && n <= 12;
				})
				.map((raw) => raw.padStart(2, "0"))
				.sort(sortNumericStrings);
		}
	);
}

// -----------------------------------------------------------------------------
// 4. Days (01–31)
// -----------------------------------------------------------------------------

export async function listDays(branch, year, month) {
	return withTtlCache(
		buildCacheKey("days", branch, year, month),
		TTL_DAYS_MS,
		async () => {
			const dir = fullPath(branch, year, month);
			const entries = await fs.readdir(dir, { withFileTypes: true });

			// Important: filter to valid calendar days (1–31) to align with UI param validation.
			return entries
				.filter((entry) => entry.isDirectory() && /^\d{1,2}$/.test(entry.name))
				.map((entry) => entry.name.trim())
				.filter((raw) => {
					const n = parseInt(raw, 10);
					return Number.isInteger(n) && n >= 1 && n <= 31;
				})
				.map((raw) => raw.padStart(2, "0"))
				.sort(sortNumericStrings);
		}
	);
}

// -----------------------------------------------------------------------------
// 5. Files (PDFs) for a given day
// -----------------------------------------------------------------------------

export async function listFiles(branch, year, month, day) {
	return withTtlCache(
		buildCacheKey("files", branch, year, month, day),
		TTL_FILES_MS,
		async () => {
			const dir = fullPath(branch, year, month, day);
			const entries = await fs.readdir(dir);

			return entries
				.filter((name) => name.toLowerCase().endsWith(".pdf"))
				.sort((a, b) => a.localeCompare(b, "en"))
				.map((name) => ({
					name,
					relativePath: `${branch}/${year}/${month}/${day}/${name}`,
				}));
		}
	);
}
