const sqlite3 = require("sqlite3");
const faker = require("@faker-js/faker").faker;
const { random, sample, sampleSize, sum } = require("lodash");
const { DateTime } = require("luxon");
const { open } = require("sqlite");
const { v4: uuidv4 } = require("uuid");

const dbPath = "./db/sqlite.db";

// Configuration for test data generation
let startDate = DateTime.fromISO("2024-10-08");
let endDate = DateTime.fromISO("2024-11-08");
let numberOfPrinters = 5;

// Use weekday names for better readability and ease of setting trends
let avgPrintTimesPerDay = {
	Monday: 4,
	Tuesday: 2,
	Wednesday: 5,
	Thursday: 2,
	Friday: 3,
	Saturday: 0,
	Sunday: 0,
}; // Average number of prints for each weekday

let avgPrintDurationPerDay = {
	Monday: 240, // Total average duration in minutes for Monday
	Tuesday: 30,
	Wednesday: 45,
	Thursday: 40,
	Friday: 120,
	Saturday: 0,
	Sunday: 0,
}; // Average total duration of prints for each weekday

let printerUsage = {
	"Drucker 1": 0.5,
	"Drucker 2": 0.7,
	"Drucker 3": 0.6,
	"Drucker 4": 0.3,
	"Drucker 5": 0.4,
}; // Usage percentages for each printer

// **New Configurations for Error Rates**
let generalErrorRate = 0.05; // 5% chance any print job may fail
let printerErrorRates = {
	"Drucker 1": 0.02, // 2% error rate for Printer 1
	"Drucker 2": 0.03,
	"Drucker 3": 0.01,
	"Drucker 4": 0.05,
	"Drucker 5": 0.04,
}; // Error rates for each printer

const holidays = []; // Example holidays
const existingJobs = [];

const initDB = async () => {
	console.log("Initializing database connection...");
	return open({
		filename: dbPath,
		driver: sqlite3.Database,
	});
};

const createUser = (isPowerUser = false) => {
	const name = [faker.person.firstName(), faker.person.lastName()];

	const user = {
		id: uuidv4(),
		github_id: faker.number.int(),
		username: `${name[0].slice(0, 2)}${name[1].slice(0, 6)}`.toUpperCase(),
		displayName: `${name[0]} ${name[1]}`.toUpperCase(),
		email: `${name[0]}.${name[1]}@example.com`,
		role: sample(["user", "admin"]),
		isPowerUser,
	};
	console.log("Created user:", user);
	return user;
};

const createPrinter = (index) => {
	const printer = {
		id: uuidv4(),
		name: `Drucker ${index}`,
		description: faker.lorem.sentence(),
		status: random(0, 2),
	};
	console.log("Created printer:", printer);
	return printer;
};

const isPrinterAvailable = (printer, startAt, duration) => {
	const endAt = startAt + duration * 60 * 1000; // Convert minutes to milliseconds
	return !existingJobs.some((job) => {
		const jobStart = job.startAt;
		const jobEnd = job.startAt + job.durationInMinutes * 60 * 1000;
		return (
			printer.id === job.printerId &&
			((startAt >= jobStart && startAt < jobEnd) ||
				(endAt > jobStart && endAt <= jobEnd) ||
				(startAt <= jobStart && endAt >= jobEnd))
		);
	});
};

const createPrintJob = (users, printers, startAt, duration) => {
	const user = sample(users);
	let printer;

	// Weighted selection based on printer usage
	const printerNames = Object.keys(printerUsage);
	const weightedPrinters = printers.filter((p) => printerNames.includes(p.name));

	// Create a weighted array of printers based on usage percentages
	const printerWeights = weightedPrinters.map((p) => ({
		printer: p,
		weight: printerUsage[p.name],
	}));

	const totalWeight = sum(printerWeights.map((pw) => pw.weight));
	const randomWeight = Math.random() * totalWeight;
	let accumulatedWeight = 0;
	for (const pw of printerWeights) {
		accumulatedWeight += pw.weight;
		if (randomWeight <= accumulatedWeight) {
			printer = pw.printer;
			break;
		}
	}

	if (!printer) {
		printer = sample(printers);
	}

	if (!isPrinterAvailable(printer, startAt, duration)) {
		console.log("Printer not available, skipping job creation.");
		return null;
	}

	// **Determine if the job should be aborted based on error rates**
	let aborted = false;
	let abortReason = null;

	// Calculate the combined error rate
	const printerErrorRate = printerErrorRates[printer.name] || 0;
	const combinedErrorRate = 1 - (1 - generalErrorRate) * (1 - printerErrorRate);

	if (Math.random() < combinedErrorRate) {
		aborted = true;
		const errorMessages = [
			"Unbekannt",
			"Keine Ahnung",
			"Falsch gebucht",
			"Filament gelöst",
			"Druckabbruch",
			"Düsenverstopfung",
			"Schichthaftung fehlgeschlagen",
			"Materialmangel",
			"Dateifehler",
			"Temperaturproblem",
			"Mechanischer Fehler",
			"Softwarefehler",
			"Kalibrierungsfehler",
			"Überhitzung",
		];
		abortReason = sample(errorMessages); // Generate a random abort reason
	}

	const printJob = {
		id: uuidv4(),
		printerId: printer.id,
		userId: user.id,
		startAt,
		durationInMinutes: duration,
		comments: faker.lorem.sentence(),
		aborted,
		abortReason,
	};
	console.log("Created print job:", printJob);
	return printJob;
};

const generatePrintJobsForDay = async (users, printers, dayDate, totalJobsForDay, totalDurationForDay, db, dryRun) => {
	console.log(`Generating print jobs for ${dayDate.toISODate()}...`);

	// Generate random durations that sum up approximately to totalDurationForDay
	const durations = [];
	let remainingDuration = totalDurationForDay;
	for (let i = 0; i < totalJobsForDay; i++) {
		const avgJobDuration = remainingDuration / (totalJobsForDay - i);
		const jobDuration = Math.max(
			Math.round(random(avgJobDuration * 0.8, avgJobDuration * 1.2)),
			5, // Minimum duration of 5 minutes
		);
		durations.push(jobDuration);
		remainingDuration -= jobDuration;
	}

	// Shuffle durations to randomize job lengths
	const shuffledDurations = sampleSize(durations, durations.length);

	for (let i = 0; i < totalJobsForDay; i++) {
		const duration = shuffledDurations[i];

		// Random start time between 8 AM and 6 PM, adjusted to avoid overlapping durations
		const possibleStartHours = Array.from({ length: 10 }, (_, idx) => idx + 8); // 8 AM to 6 PM
		let startAt;
		let attempts = 0;
		do {
			const hour = sample(possibleStartHours);
			const minute = random(0, 59);
			startAt = dayDate.set({ hour, minute, second: 0, millisecond: 0 }).toMillis();
			attempts++;
			if (attempts > 10) {
				console.log("Unable to find available time slot, skipping job.");
				break;
			}
		} while (!isPrinterAvailable(sample(printers), startAt, duration));

		if (attempts > 10) continue;

		const printJob = createPrintJob(users, printers, startAt, duration);
		if (printJob) {
			if (!dryRun) {
				await db.run(
					`INSERT INTO printJob (id, printerId, userId, startAt, durationInMinutes, comments, aborted, abortReason)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
					[
						printJob.id,
						printJob.printerId,
						printJob.userId,
						printJob.startAt,
						printJob.durationInMinutes,
						printJob.comments,
						printJob.aborted ? 1 : 0,
						printJob.abortReason,
					],
				);
			}
			existingJobs.push(printJob);
			console.log("Inserted print job into database:", printJob.id);
		}
	}
};

const generateTestData = async (dryRun = false) => {
	console.log("Starting test data generation...");
	const db = await initDB();

	// Generate users and printers
	const users = [
		...Array.from({ length: 7 }, () => createUser(false)),
		...Array.from({ length: 3 }, () => createUser(true)),
	];
	const printers = Array.from({ length: numberOfPrinters }, (_, index) => createPrinter(index + 1));

	if (!dryRun) {
		// Insert users into the database
		for (const user of users) {
			await db.run(
				`INSERT INTO user (id, github_id, name, displayName, email, role)
         VALUES (?, ?, ?, ?, ?, ?)`,
				[user.id, user.github_id, user.username, user.displayName, user.email, user.role],
			);
			console.log("Inserted user into database:", user.id);
		}

		// Insert printers into the database
		for (const printer of printers) {
			await db.run(
				`INSERT INTO printer (id, name, description, status)
         VALUES (?, ?, ?, ?)`,
				[printer.id, printer.name, printer.description, printer.status],
			);
			console.log("Inserted printer into database:", printer.id);
		}
	}

	// Generate print jobs for each day within the specified date range
	let currentDay = startDate;
	while (currentDay <= endDate) {
		const weekdayName = currentDay.toFormat("EEEE"); // Get weekday name (e.g., 'Monday')
		if (holidays.includes(currentDay.toISODate()) || avgPrintTimesPerDay[weekdayName] === 0) {
			console.log(`Skipping holiday or no jobs scheduled: ${currentDay.toISODate()}`);
			currentDay = currentDay.plus({ days: 1 });
			continue;
		}

		const totalJobsForDay = avgPrintTimesPerDay[weekdayName];
		const totalDurationForDay = avgPrintDurationPerDay[weekdayName];

		await generatePrintJobsForDay(users, printers, currentDay, totalJobsForDay, totalDurationForDay, db, dryRun);
		currentDay = currentDay.plus({ days: 1 });
	}

	if (!dryRun) {
		await db.close();
		console.log("Database connection closed. Test data generation complete.");
	} else {
		console.log("Dry run complete. No data was written to the database.");
	}
};

const setConfigurations = (config) => {
	if (config.startDate) startDate = DateTime.fromISO(config.startDate);
	if (config.endDate) endDate = DateTime.fromISO(config.endDate);
	if (config.numberOfPrinters) numberOfPrinters = config.numberOfPrinters;
	if (config.avgPrintTimesPerDay) avgPrintTimesPerDay = config.avgPrintTimesPerDay;
	if (config.avgPrintDurationPerDay) avgPrintDurationPerDay = config.avgPrintDurationPerDay;
	if (config.printerUsage) printerUsage = config.printerUsage;
	if (config.generalErrorRate !== undefined) generalErrorRate = config.generalErrorRate;
	if (config.printerErrorRates) printerErrorRates = config.printerErrorRates;
};

// Example usage
setConfigurations({
	startDate: "2024-10-08",
	endDate: "2024-11-08",
	numberOfPrinters: 6,
	avgPrintTimesPerDay: {
		Monday: 4, // High usage
		Tuesday: 2, // Low usage
		Wednesday: 3, // Low usage
		Thursday: 2, // Low usage
		Friday: 8, // High usage
		Saturday: 0,
		Sunday: 0,
	},
	avgPrintDurationPerDay: {
		Monday: 300, // High total duration
		Tuesday: 60, // Low total duration
		Wednesday: 90,
		Thursday: 60,
		Friday: 240,
		Saturday: 0,
		Sunday: 0,
	},
	printerUsage: {
		"Drucker 1": 2.3,
		"Drucker 2": 1.7,
		"Drucker 3": 0.1,
		"Drucker 4": 1.5,
		"Drucker 5": 2.4,
		"Drucker 6": 0.3,
		"Drucker 7": 0.9,
		"Drucker 8": 0.1,
	},
	generalErrorRate: 0.05, // 5% general error rate
	printerErrorRates: {
		"Drucker 1": 0.02,
		"Drucker 2": 0.03,
		"Drucker 3": 0.1,
		"Drucker 4": 0.05,
		"Drucker 5": 0.04,
		"Drucker 6": 0.02,
		"Drucker 7": 0.01,
		"PrinteDrucker 8": 0.03,
	},
});

generateTestData(process.argv.includes("--dry-run"))
	.then(() => {
		console.log("Test data generation script finished.");
	})
	.catch((err) => {
		console.error("Error generating test data:", err);
	});