import type { RelativePath } from "../persistence/database";
import type { Logger } from "./logger";

export interface SyncCreateDetails {
	type: SyncType.CREATE;
	relativePath: RelativePath;
}

export interface SyncUpdateDetails {
	type: SyncType.UPDATE;
	relativePath: RelativePath;
}

export interface SyncMovedDetails {
	type: SyncType.MOVE;
	relativePath: RelativePath;
	movedFrom: RelativePath;
}

export interface SyncDeleteDetails {
	type: SyncType.DELETE;
	relativePath: RelativePath;
}

export interface SyncSkippedDetails {
	type: SyncType.SKIPPED;
	relativePath: RelativePath;
}

export type SyncDetails =
	| SyncCreateDetails
	| SyncUpdateDetails
	| SyncDeleteDetails
	| SyncMovedDetails
	| SyncSkippedDetails;

export interface CommonHistoryEntry {
	status: SyncStatus;
	message: string;
	details: SyncDetails;
	author?: string;
}

export enum SyncType {
	CREATE = "CREATE",
	UPDATE = "UPDATE",
	DELETE = "DELETE",
	MOVE = "MOVE",
	SKIPPED = "SKIPPED"
}

export enum SyncStatus {
	SUCCESS = "SUCCESS",
	ERROR = "ERROR",
	SKIPPED = "SKIPPED"
}

export type HistoryEntry = CommonHistoryEntry & { timestamp: Date };

export interface HistoryStats {
	success: number;
	error: number;
}

export class SyncHistory {
	private static readonly MAX_ENTRIES = 5000;
	private static readonly TIMEOUT_FOR_MERGING_ENTRIES_IN_SECONDS = 60;

	private _entries: HistoryEntry[] = [];

	private readonly syncHistoryUpdateListeners: ((
		status: HistoryStats
	) => unknown)[] = [];

	private status: HistoryStats = {
		success: 0,
		error: 0
	};

	public constructor(private readonly logger: Logger) {}

	public get entries(): readonly HistoryEntry[] {
		return this._entries;
	}

	/**
	 * Insert the entry at the beginning of the history list. If the entry
	 * already in the list, it will get moved to the beginning and updated.
	 *
	 * If the entry list is too long, the oldest entry will be removed.
	 */
	public addHistoryEntry(entry: CommonHistoryEntry): void {
		const historyEntry = {
			...entry,
			timestamp: new Date()
		};

		const candidate = this.findSimilarRecentUpdateEntry(historyEntry);
		if (candidate !== undefined) {
			this._entries = this._entries.filter((e) => e !== candidate);
		}

		// Insert the entry at the beginning
		this._entries.unshift(historyEntry);

		if (this._entries.length > SyncHistory.MAX_ENTRIES) {
			this._entries.pop();
		}

		this.updateSuccessCount(historyEntry);
	}

	public addSyncHistoryUpdateListener(
		listener: (stats: HistoryStats) => unknown
	): void {
		this.syncHistoryUpdateListeners.push(listener);
		listener({ ...this.status });
	}

	public reset(): void {
		this._entries.length = 0;
		this.status = {
			success: 0,
			error: 0
		};
		this.syncHistoryUpdateListeners.forEach((listener) => {
			listener(this.status);
		});
	}

	private findSimilarRecentUpdateEntry(
		entry: HistoryEntry
	): HistoryEntry | undefined {
		if (entry.details.type !== SyncType.UPDATE) {
			return;
		}

		const candidate = this._entries.find(
			(e) =>
				e.details.type === SyncType.UPDATE &&
				e.details.relativePath === entry.details.relativePath
		);
		if (
			candidate !== undefined &&
			(this._entries[0] === candidate ||
				candidate.timestamp.getTime() +
					SyncHistory.TIMEOUT_FOR_MERGING_ENTRIES_IN_SECONDS * 1000 >
					entry.timestamp.getTime())
		) {
			return candidate;
		}
	}

	private updateSuccessCount(entry: HistoryEntry): void {
		const message = `${entry.details.relativePath} - ${entry.message} (${entry.details.type.toLocaleLowerCase()})`;
		switch (entry.status) {
			case SyncStatus.SUCCESS:
				this.status.success++;
				this.logger.info(`History entry: ${message}`);
				break;
			case SyncStatus.ERROR:
				this.status.error++;
				this.logger.error(`Cannot sync file: ${message}`);
				break;
			case SyncStatus.SKIPPED:
				this.logger.error(`Skipping file: ${message}`);
				break;
		}

		this.syncHistoryUpdateListeners.forEach((listener) => {
			listener(this.status);
		});
	}
}
