import { strict as assert } from 'node:assert';
import type { FolderData, ItemId } from './types';
import Serializable, { BaseSchema } from './Serializable';

export type ShareRecord = {
	email: string;
	readOnly: boolean;
};

interface InitializationOptions extends FolderData {
	childIds: readonly ItemId[];
	sharedWith: readonly ShareRecord[];
	isShared: boolean;
	// Email of the Joplin Server account that controls the item
	ownedByEmail: string;
}

const validateId = (id: string) => {
	return !!id.match(/^[a-zA-Z0-9]{32}$/);
};

const schema = {
	isFolder: 'boolean',
	parentId: 'id',
	id: 'id',
	title: 'string',
	ownedByEmail: 'string',
	childIds: 'id[]',
	isShared: 'boolean',
	sharedWith: [
		{ email: 'string', readOnly: 'boolean' },
		'...',
	],
} satisfies BaseSchema;

const normalizeFolderTitle = (title: string) => {
	// Joplin removes / and \ from the beginning of folder titles.
	// For consistency, this must be done in the fuzzer, too.
	const illegalStartCharRegex = /^[/\\]+/;
	return title.replace(illegalStartCharRegex, '');
};

export default class FolderRecord extends Serializable<typeof schema> implements FolderData {
	public readonly parentId: ItemId;
	public readonly id: ItemId;
	public readonly title: string;
	public readonly ownedByEmail: string;
	public readonly childIds: readonly ItemId[];

	private readonly isShared_: boolean;
	private readonly sharedWith_: readonly ShareRecord[];

	public constructor(options: InitializationOptions) {
		super(schema);

		this.parentId = options.parentId;
		this.id = options.id;
		this.title = normalizeFolderTitle(options.title);
		this.childIds = options.childIds;
		this.isShared_ = options.isShared ?? options.sharedWith.length > 0;
		this.ownedByEmail = options.ownedByEmail;
		this.sharedWith_ = options.sharedWith;

		if (this.parentId !== '' && !validateId(this.parentId)) {
			throw new Error(`Invalid parent ID: ${this.parentId}`);
		}

		if (!validateId(this.id)) {
			throw new Error(`Invalid ID: ${this.id}`);
		}
	}

	public static fromSerialized(serialized: unknown) {
		const data = this.deserialize(schema, serialized);
		return new FolderRecord(data);
	}

	public serialize() {
		return {
			isFolder: true,
			parentId: this.parentId,
			id: this.id,
			title: this.title,
			ownedByEmail: this.ownedByEmail,
			childIds: [...this.childIds],
			sharedWith: [...this.sharedWith_],
			isShared: this.isShared_,
		};
	}

	public get shareRecipients() {
		return this.sharedWith_.map(sharee => sharee.email);
	}

	private get metadata_(): InitializationOptions {
		return {
			parentId: this.parentId,
			id: this.id,
			title: this.title,
			isShared: this.isShared_,
			ownedByEmail: this.ownedByEmail,
			childIds: [...this.childIds],
			sharedWith: [...this.sharedWith_],
		};
	}

	public get isRootSharedItem() {
		return this.isShared_ && this.parentId === '';
	}

	public isSharedWith(email: string) {
		assert.equal(this.parentId, '', 'only supported for toplevel folders');
		return this.sharedWith_.some(record => record.email === email);
	}

	public isReadOnlySharedWith(email: string) {
		assert.equal(this.parentId, '', 'only supported for toplevel folders');
		return this.sharedWith_.some(record => record.email === email && record.readOnly);
	}

	public withTitle(title: string) {
		return new FolderRecord({
			...this.metadata_,
			title,
		});
	}

	public withParent(parentId: ItemId) {
		return new FolderRecord({
			...this.metadata_,
			parentId,
		});
	}

	public withId(id: ItemId) {
		return new FolderRecord({
			...this.metadata_,
			id,
		});
	}

	public withChildren(childIds: readonly ItemId[]) {
		return new FolderRecord({
			...this.metadata_,
			childIds: [...childIds],
		});
	}

	public withChildAdded(childId: ItemId) {
		if (this.childIds.includes(childId)) {
			return this;
		}

		return this.withChildren([...this.childIds, childId]);
	}

	public withChildRemoved(childId: ItemId) {
		return this.withChildren(
			this.childIds.filter(id => id !== childId),
		);
	}

	private withShareState_(isShared: boolean, shareState: readonly ShareRecord[]) {
		return new FolderRecord({
			...this.metadata_,
			isShared,
			sharedWith: [...shareState],
		});
	}

	public withShareOwner(email: string) {
		if (email === this.ownedByEmail) return this;

		const sharedWith = this.metadata_.sharedWith.filter(recipient => recipient.email !== email);
		return new FolderRecord({
			...this.metadata_,
			sharedWith: sharedWith,
			isShared: sharedWith.length > 0,
			ownedByEmail: email,
		});
	}

	public withShared(recipientEmail: string, readOnly: boolean) {
		if (this.isSharedWith(recipientEmail) && this.isReadOnlySharedWith(recipientEmail) === readOnly) {
			return this;
		}

		if (this.parentId !== '') {
			throw new Error('Cannot share non-top-level folder');
		}

		return this.withShareState_(true, [
			...this.sharedWith_.filter(record => record.email !== recipientEmail),
			{ email: recipientEmail, readOnly },
		]);
	}

	public withRemovedFromShare(recipientEmail: string) {
		if (!this.isSharedWith(recipientEmail)) {
			return this;
		}

		return this.withShareState_(
			// Even if no users are present in the share, Joplin still considers the folder to be shared
			this.isShared_,
			this.sharedWith_.filter(record => record.email !== recipientEmail),
		);
	}

	public withUnshared() {
		if (!this.isShared_) {
			return this;
		}

		return this.withShareState_(false, []);
	}
}
