import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import pg from "pg";
import config from "../built/config/index.js";

const { Client } = pg;
const marker = "karaoke-service-rights-safe-test-data";
const createdAt = new Date();

function genId(date = new Date()) {
	const time2000 = 946684800000;
	const timestamp = Math.max(date.getTime() - time2000, 0).toString(36).padStart(8, "0");
	const random = crypto.randomBytes(8).toString("base64url").replace(/[^a-z0-9]/gi, "").toLowerCase().slice(0, 8).padEnd(8, "0");
	return `${timestamp}${random}`;
}

function md5(buffer) {
	return crypto.createHash("md5").update(buffer).digest("hex");
}

function saveInternalFile(name, buffer) {
	const accessKey = crypto.randomUUID();
	fs.mkdirSync(config.mediaDir, { recursive: true });
	fs.writeFileSync(path.join(config.mediaDir, accessKey), buffer);
	return {
		accessKey,
		url: `${config.url}/files/${accessKey}`,
		name,
		size: buffer.byteLength,
		md5: md5(buffer),
	};
}

function createWav() {
	const sampleRate = 44100;
	const seconds = 16;
	const samples = sampleRate * seconds;
	const data = Buffer.alloc(samples * 2);
	const notes = [261.63, 293.66, 329.63, 392.0, 349.23, 329.63, 293.66, 261.63];

	for (let i = 0; i < samples; i++) {
		const t = i / sampleRate;
		const beat = Math.floor(t / 2) % notes.length;
		const root = notes[beat] / 2;
		const value =
			Math.sin(2 * Math.PI * root * t) * 0.22 +
			Math.sin(2 * Math.PI * root * 1.5 * t) * 0.12 +
			Math.sin(2 * Math.PI * root * 2 * t) * 0.08;
		data.writeInt16LE(Math.round(Math.max(-1, Math.min(1, value)) * 0x7fff), i * 2);
	}

	const header = Buffer.alloc(44);
	header.write("RIFF", 0);
	header.writeUInt32LE(36 + data.length, 4);
	header.write("WAVE", 8);
	header.write("fmt ", 12);
	header.writeUInt32LE(16, 16);
	header.writeUInt16LE(1, 20);
	header.writeUInt16LE(1, 22);
	header.writeUInt32LE(sampleRate, 24);
	header.writeUInt32LE(sampleRate * 2, 28);
	header.writeUInt16LE(2, 32);
	header.writeUInt16LE(16, 34);
	header.write("data", 36);
	header.writeUInt32LE(data.length, 40);
	return Buffer.concat([header, data]);
}

function varLen(value) {
	let buffer = value & 0x7f;
	while ((value >>= 7) > 0) {
		buffer <<= 8;
		buffer |= (value & 0x7f) | 0x80;
	}
	const bytes = [];
	for (;;) {
		bytes.push(buffer & 0xff);
		if (buffer & 0x80) buffer >>= 8;
		else break;
	}
	return Buffer.from(bytes);
}

function createMidi() {
	const ticksPerQuarter = 480;
	const events = [];
	const push = (...bytes) => events.push(Buffer.from(bytes));
	const pushDelta = (delta) => events.push(varLen(delta));

	pushDelta(0);
	push(0xff, 0x51, 0x03, 0x07, 0xa1, 0x20); // 120 BPM
	pushDelta(0);
	push(0xc0, 0x00);

	const melody = [60, 62, 64, 67, 65, 64, 62, 60];
	for (const note of melody) {
		pushDelta(0);
		push(0x90, note, 0x64);
		pushDelta(ticksPerQuarter * 2);
		push(0x80, note, 0x40);
	}

	pushDelta(0);
	push(0xff, 0x2f, 0x00);

	const track = Buffer.concat(events);
	const header = Buffer.alloc(14);
	header.write("MThd", 0);
	header.writeUInt32BE(6, 4);
	header.writeUInt16BE(0, 8);
	header.writeUInt16BE(1, 10);
	header.writeUInt16BE(ticksPerQuarter, 12);

	const trackHeader = Buffer.alloc(8);
	trackHeader.write("MTrk", 0);
	trackHeader.writeUInt32BE(track.length, 4);
	return Buffer.concat([header, trackHeader, track]);
}

function textBuffer(text) {
	return Buffer.from(text.replace(/\n/g, "\r\n"), "utf8");
}

async function main() {
	const client = new Client({
		host: config.db.host,
		port: config.db.port,
		user: config.db.user,
		password: config.db.pass,
		database: config.db.db,
		...config.db.extra,
	});

	await client.connect();
	try {
		const admin = await client.query(
			`SELECT "id", "username", "host" FROM "user" WHERE "usernameLower" = 'admin' AND "host" IS NULL LIMIT 1`,
		);
		if (admin.rowCount === 0) {
			throw new Error("Local @admin user was not found. Create @admin first, then rerun this script.");
		}
		const adminUser = admin.rows[0];

		const existing = await client.query(
			`SELECT "id", "fileIds" FROM "note" WHERE "userId" = $1 AND "text" ILIKE $2 LIMIT 1`,
			[adminUser.id, `%${marker}%`],
		);

		const assets = [
			{
				name: "karaoke-test-accompaniment.wav",
				type: "audio/wav",
				comment: "KaraokeService test accompaniment. Original generated tone progression.",
				buffer: createWav(),
			},
			{
				name: "karaoke-test-lyrics.lrc",
				type: "text/plain",
				comment: "KaraokeService timed lyrics.",
				buffer: textBuffer(`[00:00.000]Original karaoke test
[00:02.000]Sing the first clear tone
[00:04.000]Move to the next note
[00:06.000]Hold the bright third
[00:08.000]Reach the open fifth
[00:10.000]Return with steady timing
[00:12.000]Finish on the home note
[00:14.000]This data is rights-safe`),
			},
			{
				name: "karaoke-test-pitch.mid",
				type: "audio/midi",
				comment: "KaraokeService MIDI pitch and tempo data.",
				buffer: createMidi(),
			},
			{
				name: "karaoke-test-score.json",
				type: "application/json",
				comment: "KaraokeService scoring metadata.",
				buffer: textBuffer(JSON.stringify({
					version: 1,
					scoring: "midi-pitch",
					pitchFile: "karaoke-test-pitch.mid",
					toleranceCents: 50,
					partialToleranceCents: 100,
					extraSingingPenalty: true,
				}, null, 2)),
			},
			{
				name: "karaoke-test-song-info.txt",
				type: "text/plain",
				comment: "KaraokeService song metadata.",
				buffer: textBuffer(`Title: Original Karaoke Test
Artist: FrozenFriendsYume test data
License: Public-domain equivalent test fixture generated for this repository
BPM: 120
Key: C major
Notes: All melody, lyrics, accompaniment, and metadata were generated locally for testing.`),
			},
		];

		const files = assets.map((asset) => ({
			id: genId(createdAt),
			createdAt,
			userId: adminUser.id,
			userHost: null,
			...saveInternalFile(asset.name, asset.buffer),
			type: asset.type,
			comment: asset.comment,
		}));

		await client.query("BEGIN");
		for (const file of files) {
			await client.query(
				`INSERT INTO "drive_file" (
					"id", "createdAt", "userId", "userHost", "md5", "name", "type", "size", "comment",
					"blurhash", "properties", "storedInternal", "url", "thumbnailUrl", "webpublicUrl",
					"webpublicType", "accessKey", "thumbnailAccessKey", "webpublicAccessKey", "uri", "src",
					"folderId", "isSensitive", "allowDownload", "isLink", "requestHeaders", "requestIp"
				) VALUES (
					$1, $2, $3, $4, $5, $6, $7, $8, $9,
					NULL, '{}', TRUE, $10, NULL, NULL,
					NULL, $11, NULL, NULL, NULL, NULL,
					NULL, FALSE, FALSE, FALSE, NULL, NULL
				)`,
				[
					file.id,
					file.createdAt,
					file.userId,
					file.userHost,
					file.md5,
					file.name,
					file.type,
					file.size,
					file.comment,
					file.url,
					file.accessKey,
				],
			);
		}

		const noteText = `Original Karaoke Test
Rights-safe generated karaoke test data.
Marker: ${marker}
#KaraokeService`;

		if (existing.rowCount > 0) {
			const noteId = existing.rows[0].id;
			const oldFileIds = existing.rows[0].fileIds ?? [];
			const oldFiles = oldFileIds.length > 0
				? await client.query(`SELECT "id", "accessKey" FROM "drive_file" WHERE "id" = ANY($1)`, [oldFileIds])
				: { rows: [] };

			await client.query(
				`UPDATE "note" SET "text" = $2, "fileIds" = $3, "attachedFileTypes" = $4, "updatedAt" = $5 WHERE "id" = $1`,
				[noteId, noteText, files.map((file) => file.id), files.map((file) => file.type), new Date()],
			);
			if (oldFileIds.length > 0) {
				await client.query(`DELETE FROM "drive_file" WHERE "id" = ANY($1)`, [oldFileIds]);
				for (const file of oldFiles.rows) {
					if (!file.accessKey) continue;
					fs.rmSync(path.join(config.mediaDir, file.accessKey), { force: true });
				}
			}
			await client.query("COMMIT");
			console.log(`Recreated KaraokeService test media for existing @admin note: ${noteId}`);
			return;
		}

		const noteId = genId(createdAt);
		await client.query(
			`INSERT INTO "note" (
				"id", "createdAt", "replyId", "renoteId", "threadId", "text", "name", "cw",
				"userId", "groupId", "localOnly", "renoteCount", "repliesCount", "viewCount",
				"reactions", "visibility", "uri", "url", "score", "fileIds", "attachedFileTypes",
				"visibleUserIds", "mentions", "mentionedRemoteUsers", "emojis", "tags", "hasPoll",
				"channelId", "quoteAuthorization", "canQuote", "userHost", "replyUserId",
				"replyUserHost", "renoteUserId", "renoteUserHost", "updatedAt"
			) VALUES (
				$1, $2, NULL, NULL, NULL, $3, NULL, NULL,
				$4, NULL, FALSE, 0, 0, 0,
				'{}', 'public', NULL, NULL, 0, $5, $6,
				'{}', '{}', '[]', '{}', $7, FALSE,
				NULL, NULL, TRUE, NULL, NULL,
				NULL, NULL, NULL, NULL
			)`,
			[
				noteId,
				createdAt,
				noteText,
				adminUser.id,
				files.map((file) => file.id),
				files.map((file) => file.type),
				["karaokeservice"],
			],
		);

		await client.query(`UPDATE "user" SET "notesCount" = "notesCount" + 1 WHERE "id" = $1`, [adminUser.id]);
		await client.query("COMMIT");
		console.log(`Created KaraokeService test note as @admin: ${noteId}`);
	} catch (err) {
		await client.query("ROLLBACK").catch(() => undefined);
		throw err;
	} finally {
		await client.end();
	}
}

main().catch((err) => {
	console.error(err);
	process.exit(1);
});
