import { SERVER, SERVER_OPTIONS } from '@andyburke/serverus/server';
import { convert_to_words } from '@andyburke/lurid/word_bytes';
import { API_CLIENT } from '../utils/api.ts';
import { Cookie, getSetCookies } from '@std/http/cookie';
import { generateTotp } from '../utils/totp.ts';
import { USER } from '../models/user.ts';
import { SESSION } from '../models/session.ts';

const TLDs: string[] = [
	'com',
	'org',
	'net',
	'edu',
	'gov',
	'nexus',
	'shop',
	'unreasonablylongtldname'
];

const random_byte_buffer: Uint8Array = new Uint8Array(3);
export function random_email_address(): string {
	crypto.getRandomValues(random_byte_buffer);
	const name = convert_to_words(random_byte_buffer).join('-');

	crypto.getRandomValues(random_byte_buffer);
	const domain = convert_to_words(random_byte_buffer).join('-');

	const tld = TLDs[Math.floor(Math.random() * TLDs.length)];
	return `${name}@${domain}.${tld}`;
}

export function random_username(): string {
	crypto.getRandomValues(random_byte_buffer);
	return convert_to_words(random_byte_buffer).join('-');
}

const BASE_PORT: number = 50_000;
const MAX_PORT_OFFSET: number = 10_000;
let current_port_offset = 0;
function get_next_free_port(): number {
	let free_port: number | undefined = undefined;
	let attempts = 0;
	do {
		const port_to_try: number = BASE_PORT + (current_port_offset++ % MAX_PORT_OFFSET);

		try {
			Deno.listen({ port: port_to_try }).close();
			free_port = port_to_try;
		} catch (error) {
			if (!(error instanceof Deno.errors.AddrInUse)) {
				throw error;
			}
		}

		++attempts;

		if (attempts % MAX_PORT_OFFSET === 0) {
			console.warn(`Tried all ports at least once while trying to locate a free one, something wrong?`);
		}
	} while (!free_port);

	return free_port;
}

/**
 * Interface defining the configuration for an ephemeral server
 * @property {string} hostname - hostname bound to, default: 'localhost'
 * @property {number} port - port bound to, default: next free port
 * @property {SERVER} server - server instance
 */
export interface EPHEMERAL_SERVER {
	hostname: string;
	port: number;
	server: SERVER;
}

/**
 * Gets an ephemeral Serverus SERVER on an unused port.
 *
 * @param options Optional SERVER_OPTIONS
 * @returns A LISTEN_SERVER_SETUP object with information and a reference to the server
 */
export async function get_ephemeral_listen_server(options?: SERVER_OPTIONS): Promise<EPHEMERAL_SERVER> {
	const server_options = {
		...{
			hostname: 'localhost',
			port: get_next_free_port()
		},
		...(options ?? {})
	};

	const server = new SERVER(server_options);

	const ephemeral_server: EPHEMERAL_SERVER = {
		hostname: server_options.hostname,
		port: server_options.port,
		server: await server.start()
	};

	return ephemeral_server;
}

export type TEST_USER_INFO = {
	user: USER;
	session: SESSION;
	headers: Headers;
};

export async function get_new_user(client: API_CLIENT, user_info?: Record<string, any>, inviting_user_info?: TEST_USER_INFO): Promise<TEST_USER_INFO> {
	const new_user_request_json: Record<string, any> = {
		username: random_username(),
		...user_info
	};

	if (
		!((typeof new_user_request_json.password === 'string' && new_user_request_json.password.length) ||
			(typeof new_user_request_json.password_hash === 'string' && new_user_request_json.password_hash.length))
	) {
		new_user_request_json.password = `${Math.round(Math.random() * 10)} - ${random_username()} ! ${random_username()}`;
	}

	if (inviting_user_info) {
		const invite_code = await client.fetch(`/users/${inviting_user_info.user?.id}/invites`, {
			method: 'POST',
			headers: {
				'x-session_id': inviting_user_info.session.id,
				'x-totp': await generateTotp(inviting_user_info.session.secret)
			},
			json: {
				code: random_username()
			}
		});

		new_user_request_json.invite_code = invite_code.code;
	}

	await client.fetch('/users', {
		method: 'POST',
		json: new_user_request_json
	});

	const cookies: Cookie[] = [];
	const auth_response: any = await client.fetch('/auth', {
		method: 'POST',
		json: new_user_request_json,
		done: (response) => {
			cookies.push(...getSetCookies(response.headers));
		}
	});

	const test_user_info: TEST_USER_INFO = {
		user: auth_response.user,
		session: auth_response.session,
		headers: new Headers()
	};

	cookies.push({
		name: 'totp',
		value: await generateTotp(test_user_info.session?.secret),
		maxAge: 30,
		expires: Date.now() + 30_000,
		path: '/'
	});

	for (const cookie of cookies) {
		test_user_info.headers.append(`x-${cookie.name}`, cookie.value);
	}
	test_user_info.headers.append('cookie', cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; '));

	return test_user_info;
}

export async function set_user_permissions(client: API_CLIENT, user: any, session: any, permissions: string[]): Promise<any> {
	return await client.fetch(`/users/${user.id}`, {
		method: 'PUT',
		headers: {
			'x-session_id': session.id,
			'x-totp': await generateTotp(session.secret),
			'x-test-override': 'true'
		},
		json: {
			permissions
		}
	});
}

export async function delete_user(client: API_CLIENT, user_info: any): Promise<undefined> {
	await client.fetch(`/users/${user_info.user.id}`, {
		method: 'DELETE',
		headers: {
			'x-session_id': user_info.session.id,
			'x-totp': await generateTotp(user_info.session.secret)
		}
	});
}
