import Logger from '@joplin/utils/Logger';
import ActionTracker from '../model/ActionTracker';
import Client from './Client';
import { FuzzContext } from '../types';
import { join } from 'path';
import { mkdir } from 'fs-extra';
import { readdir } from 'fs/promises';

type ClientFilter = (client: Client)=> boolean;

const logger = Logger.create('ClientPool');

export default class ClientPool {
	public static async create(context: FuzzContext) {
		return new ClientPool(context);
	}

	public static async fromSnapshot(snapshotDirectory: string, actionTracker: ActionTracker, context: FuzzContext) {
		const pool = await ClientPool.create(context);

		const matchingDirectories = (await readdir(snapshotDirectory))
			.filter(child => child.startsWith('client-'))
			.map(child => join(snapshotDirectory, child))
			.sort();

		const accounts = new Map();
		for (const clientDirectory of matchingDirectories) {
			const client = await Client.fromSnapshotDirectory(clientDirectory, actionTracker, context, accounts);
			pool.clients_.push(client);
			pool.listenForClientClose_(client);
		}

		return pool;
	}

	private clients_: Client[] = [];

	private constructor(
		private readonly context_: FuzzContext,
	) {
	}

	private listenForClientClose_(client: Client) {
		client.onClose(() => {
			this.clients_ = this.clients_.filter(other => other !== client);
		});
	}

	public async saveSnapshot(outputDirectory: string) {
		let i = 0;
		for (const client of this.clients) {
			const outputChildDirectory = join(outputDirectory, `client-${i++}`);
			await mkdir(outputChildDirectory);
			await client.saveSnapshot(outputChildDirectory);
		}
	}

	public async newClient(model: ActionTracker) {
		const client = await Client.create(model, this.context_);

		this.clients_.push(client);
		this.listenForClientClose_(client);
	}

	public async createRandomInitialItemsAndSync() {
		for (const client of this.clients) {
			logger.info('Creating items for ', client.email);
			const actionCount = this.context_.randomFrom([0, 10, 100]);
			await client.createOrUpdateMany(actionCount, { createProbability: 1, updateProbability: 1, deleteProbability: 1 });

			await client.sync();
		}
	}

	public clientsByEmail(email: string) {
		return this.clients.filter(client => client.email === email);
	}

	public clientById(id: number) {
		const client = this.clients[id];
		if (!client) throw new Error(`Not found: ${id}`);
		return client;
	}

	public getClientId(client: Client): number {
		const index = this.clients.indexOf(client);
		if (index === -1) throw new Error(`Not found: ${client}`);
		return index;
	}

	public randomClient(filter: ClientFilter = ()=>true) {
		const clients = this.clients_.filter(filter);
		return clients[
			this.context_.randInt(0, clients.length)
		];
	}

	public async newWithSameAccount(sourceClient: Client) {
		const client = await sourceClient.createClientOnSameAccount();
		this.listenForClientClose_(client);
		this.clients_ = [...this.clients_, client];
		return client;
	}

	public othersWithSameAccount(client: Client) {
		return this.clients_.filter(other => other !== client && other.hasSameAccount(client));
	}

	public async checkState() {
		for (const client of this.clients_) {
			await client.checkState();
		}
	}

	public async syncAll() {
		// Sync all clients at roughly the same time. Some sync bugs are only apparent
		// when multiple clients are syncing simultaneously.
		await Promise.all(this.clients_.map(c => c.sync()));

		// Note: For more deterministic behavior, sync clients individually instead:
		//   for (const client of this.clients_) { await client.sync(); }
	}

	public get clients() {
		return [...this.clients_];
	}

	public helpText() {
		return this.clients_.map(client => client.getHelpText()).join('\n\n');
	}

	public async close() {
		for (const client of this.clients) {
			try {
				await client.close();
			} catch (error) {
				logger.warn('Failed to close client', error);
			}
		}
	}
}

