import { gzipSync } from 'zlib';
import { v4 as uuidv4 } from 'uuid';
import { LogSystem } from '../../core/log';
import { LoginSystem } from '../login';
import { CallProcRetry } from './CallProcRetry';
import { Chunk } from './Chunk';
import { Sleep } from './Sleep';
import { PlayerWrapper } from '../../core/player/player-registry';

export class LargeDataServer {
    static async CheckReady(player: PlayerWrapper): Promise<boolean> {
        let ready = await CallProcRetry(player, 'LargeData:ready');
        if (!ready) {
            for (let attempts = 0; attempts < 5; attempts++) {
                await Sleep(250);
                const res = await CallProcRetry(player, 'LargeData:ready');
                if (res) { ready = true; break; }
            }
        }
        return !!ready;
    }

    static async Send(
        player: PlayerWrapper,
        event: string,
        data: string,
        finishArg?: any,
        unreliable = false,
        callback?: (success: boolean) => void,
    ): Promise<boolean> {
        let success = true;
        const requestId = uuidv4();

        try {
            const ready = unreliable ? true : await this.CheckReady(player);
            if (!ready && !unreliable) throw 'Large data client is not ready.';

            const compressed = gzipSync(data).toString('base64');
            const safeData = Chunk(compressed, 40_000);

            if (unreliable) {
                player.call('LargeData:unreliable:init', [requestId, event, safeData.length, true]);
            } else {
                const proc = await CallProcRetry(player, 'LargeData:init', [requestId, event, safeData.length, true]);
                if (!proc) throw `[${requestId}] Failed to initialise large data with handle "${event}"`;
            }

            let sentChunks = 0;
            const start = Date.now();

            for (const [idx, transfer] of safeData.entries()) {
                if (unreliable) {
                    player.call('LargeData:unreliable:send', [requestId, event, transfer, idx]);
                } else {
                    const result = await CallProcRetry(player, 'LargeData:send', [requestId, event, transfer]);
                    if (!result) throw `[${requestId}] Failed to transfer chunk ${sentChunks + 1}/${safeData.length}`;
                }
                sentChunks++;
                if (unreliable) await Sleep(50);
            }

            const playerName = LoginSystem.IsLoggedIn(player)
                ? (player as PlayerMpLoggedIn).account.name
                : player.name;

            LogSystem.Log(
                `LargeDataServer${unreliable ? ':unreliable' : ''}:${event}`,
                `[${requestId}] Sent ${sentChunks}/${safeData.length} chunks to ${playerName} in ${Date.now() - start}ms`,
            );

            if (!unreliable) {
                const validation = await CallProcRetry(player, 'LargeData:validate', [requestId, event]);
                if (!validation) throw `[${requestId}] Failed to validate data size`;
            }
        } catch (e) {
            success = false;
            LogSystem.Error('LargeDataServer', event, JSON.stringify(e), e, player);
            if (!unreliable) {
                return this.Send(player, event, data, finishArg, true, callback);
            }
        } finally {
            if (unreliable || success) {
                player.call('LargeData:finish', [requestId, event, finishArg]);
            }
        }

        if (callback) callback(success);
        return unreliable ? true : success;
    }
}
