import {
	getSequelize,
	type ChannelSubscriptionModel,
	type UserKeyStoreModel,
	type UserTableModel
} from './database';
import { Op } from 'sequelize';
import crypto from 'crypto';
import { error } from '@sveltejs/kit';

export class User {
	data: UserTableModel;

	constructor(data: UserTableModel) {
		this.data = data;
	}

	public get id() {
		return this.data.id;
	}

	public get publicPasswordSalts() {
		return {
			decryptionKeySalt: this.data.decryptionKeySalt,
			passwordSalt: this.data.passwordSalt
		};
	}

	private get userWhere() {
		return {
			where: {
				[Op.or]: [{ id: this.data.id }, { username: this.data.username }]
			}
		};
	}

	async delete() {
		await getSequelize().UserTable.destroy(this.userWhere);
	}

	async subscriptionRssUpdated(id: string) {
		await getSequelize().ChannelSubscriptionTable.update(
			{ lastRSSFetch: new Date() },
			{
				where: {
					id: id,
					UserId: this.id
				}
			}
		);
	}

	async addSubscription(subscription: Omit<ChannelSubscriptionModel, 'userId'>) {
		if (await this.amSubscribed(subscription.id)) return;

		await getSequelize().ChannelSubscriptionTable.create({
			...subscription,
			UserId: this.id
		});
	}

	async removeSubscription(id: string) {
		await getSequelize().ChannelSubscriptionTable.destroy({
			where: {
				id
			}
		});
	}

	async amSubscribed(id: string): Promise<boolean> {
		return (
			(await getSequelize().ChannelSubscriptionTable.count({
				where: {
					id
				}
			})) > 0
		);
	}

	async subscriptions(): Promise<ChannelSubscriptionModel[]> {
		const subscriptions = await getSequelize().ChannelSubscriptionTable.findAll({
			where: {
				userId: this.data.id
			}
		});

		if (!subscriptions) return [];

		return subscriptions as unknown as ChannelSubscriptionModel[];
	}

	async getKeyValue(key: string): Promise<UserKeyStoreModel> {
		const keyStore = await getSequelize().UserKeyValueTable.findOne({
			where: {
				UserId: this.id,
				key
			}
		});

		if (!keyStore) {
			throw error(404);
		}

		const keyStoreModel = keyStore as unknown as UserKeyStoreModel;

		return {
			key: keyStoreModel.key,
			valueCipher: keyStoreModel.valueCipher,
			valueNonce: keyStoreModel.valueNonce
		};
	}

	async addOrUpdateKeyValue(key: string, valueCipher: string, valueNonce: string) {
		const keyStore = await getSequelize().UserKeyValueTable.findOne({
			where: {
				UserId: this.id,
				key
			}
		});

		if (keyStore) {
			await keyStore.update({
				valueCipher,
				valueNonce
			});
		} else {
			await getSequelize().UserKeyValueTable.create({
				key,
				valueCipher,
				valueNonce,
				UserId: this.id
			});
		}
	}

	async deleteKeyValue(key: string) {
		await getSequelize().UserKeyValueTable.destroy({
			where: {
				UserId: this.id,
				key
			}
		});
	}
}

export type CreateUser = {
	username: string;
	password: {
		hash: string;
		salt: string;
	};
	decryptionKeySalt: string;
	masterKey: {
		cipher: string;
		nonce: string;
	};
};

export async function createUser(user: CreateUser): Promise<User> {
	const id = crypto.randomUUID().toString();

	const createdUser = {
		id,
		username: user.username,
		passwordHash: user.password.hash,
		passwordSalt: user.password.salt,
		created: new Date(),
		decryptionKeySalt: user.decryptionKeySalt,
		masterKeyCipher: user.masterKey.cipher,
		masterKeyNonce: user.masterKey.nonce
	};

	let userCreated: boolean;
	try {
		await getSequelize().UserTable.create(createdUser);
		userCreated = true;
	} catch {
		userCreated = false;
	}

	if (!userCreated) {
		throw error(400);
	}

	return new User(createdUser as UserTableModel);
}

export async function getUser(identifier: string): Promise<User> {
	const user = await getSequelize().UserTable.findOne({
		where: {
			[Op.or]: [{ id: identifier }, { username: identifier }]
		}
	});

	if (!user) {
		throw error(404);
	}

	return new User(user as UserTableModel);
}

export async function authenticateUser(username: string, passwordHash: string): Promise<User> {
	const user = await getSequelize().UserTable.findOne({
		where: {
			username
		}
	});

	if (!user) {
		throw error(404);
	}

	const userModel = user as UserTableModel;

	const textEncoder = new TextEncoder();

	// Password is hashed in the browser, so if db is leaked it doesn't matter.
	// Timing safe equal used to stop timing attacks when comparing strings.
	if (
		crypto.timingSafeEqual(
			textEncoder.encode(passwordHash),
			textEncoder.encode(userModel.passwordHash)
		)
	) {
		return new User(userModel);
	}

	throw error(404);
}
