import { GlobalConfig } from '@n8n/config';
import { Service } from '@n8n/di';
import { DataTableSizeStatus, DataTablesSizeData } from 'n8n-workflow';

import { Telemetry } from '@/telemetry';

import { DataTableValidationError } from './errors/data-table-validation.error';
import { toMb } from './utils/size-utils';

@Service()
export class DataTableSizeValidator {
	private lastCheck: Date | undefined;
	private cachedSizeData: DataTablesSizeData | undefined;
	private pendingCheck: Promise<DataTablesSizeData> | null = null;

	constructor(
		private readonly globalConfig: GlobalConfig,
		private readonly telemetry: Telemetry,
	) {}

	private shouldRefresh(now: Date): boolean {
		if (
			!this.lastCheck ||
			!this.cachedSizeData ||
			now.getTime() - this.lastCheck.getTime() >= this.globalConfig.dataTable.sizeCheckCacheDuration
		) {
			return true;
		}

		return false;
	}

	async getCachedSizeData(
		fetchSizeDataFn: () => Promise<DataTablesSizeData>,
		now = new Date(),
	): Promise<DataTablesSizeData> {
		// If there's a pending check, wait for it to complete
		if (this.pendingCheck) {
			this.cachedSizeData = await this.pendingCheck;
		} else {
			// Check if we need to refresh the size data
			if (this.shouldRefresh(now)) {
				this.pendingCheck = fetchSizeDataFn();
				try {
					this.cachedSizeData = await this.pendingCheck;
					this.lastCheck = now;
				} finally {
					this.pendingCheck = null;
				}
			}
		}

		return this.cachedSizeData!;
	}

	async validateSize(
		fetchSizeFn: () => Promise<DataTablesSizeData>,
		now = new Date(),
	): Promise<void> {
		const size = await this.getCachedSizeData(fetchSizeFn, now);
		if (size.totalBytes >= this.globalConfig.dataTable.maxSize) {
			this.telemetry.track('User hit data table storage limit', {
				total_bytes: size.totalBytes,
				max_bytes: this.globalConfig.dataTable.maxSize,
			});

			throw new DataTableValidationError(
				`Data table size limit exceeded: ${toMb(size.totalBytes)}MB used, limit is ${toMb(this.globalConfig.dataTable.maxSize)}MB`,
			);
		}
	}

	sizeToState(sizeBytes: number): DataTableSizeStatus {
		const warningThreshold =
			this.globalConfig.dataTable.warningThreshold ??
			Math.floor(0.8 * this.globalConfig.dataTable.maxSize);

		if (sizeBytes >= this.globalConfig.dataTable.maxSize) {
			return 'error';
		} else if (sizeBytes >= warningThreshold) {
			return 'warn';
		}
		return 'ok';
	}

	async getSizeStatus(fetchSizeFn: () => Promise<DataTablesSizeData>, now = new Date()) {
		const size = await this.getCachedSizeData(fetchSizeFn, now);
		return this.sizeToState(size.totalBytes);
	}

	reset() {
		this.lastCheck = undefined;
		this.cachedSizeData = undefined;
		this.pendingCheck = null;
	}
}
