/**
 * Created by Gifer on 26.07.2017.
 */
"use strict";

const console         = require('gifer-console');
const CMD             = require('../mcconnect').CMD;
const ExtractFileName = require('../service').ExtractFileName;
const randomHash      = require('../service').RandomHash;
const format_size     = require('../service').format_size;
const isArray         = require('../service').isArray;
const myReplace       = require('../service').myReplace;
const crypto          = require('crypto');
const fs              = require('fs');
const os              = require('os');
const { fetch, Agent, FormData } = require('undici');

const __tlsAgent = new Agent({ connect: { rejectUnauthorized: false } });

// true = single streamed multipart POST; false = chunked (fallback). Перемикається налаштуванням клієнта.
let __streamMode = true;

let __abortUpload = null;
let __dialogs     = null;
let __mainWindow  = null;
let __uploadsList = [];
var __inProgress  = false;
let __checkedFile = null;

let __handshakeTimer = null;

// Renderer після ec_file_check_exist зобов'язаний відповісти ce_file_check_exist → Upload().
// Якщо відповіді нема (F5 renderer посеред процесу, зламаний стан, загублений колбек) —
// __inProgress висить назавжди і всі наступні аплоади мовчки стають у чергу до перезапуску.
// Ланка повністю автоматична (жодних діалогів), 60 с вистачає на повільний roundtrip.
const HANDSHAKE_TIMEOUT = 60 * 1000;

function hasFilesToUpload() {
    return __uploadsList && __uploadsList.length > 0;
}

function mcError(id, items, term){
    let res = global._ErrorsText[id.toString()];

    return (items) ? myReplace(res, items, term) : res;
}

function onError(errNumb, param){
    if (__mainWindow){
        __mainWindow.webContents.send('ec_CMD', [
            CMD.ec_file_upload_error,
            {
                code: errNumb,
                msg : mcError(errNumb, param)
            }
        ]);
    }
}

// ===== Handshake watchdog =====

function clearHandshakeTimer() {
    if (__handshakeTimer) {
        clearTimeout(__handshakeTimer);
        __handshakeTimer = null;
    }
}

function armHandshakeTimer(filePath) {
    clearHandshakeTimer();

    __handshakeTimer = setTimeout(function () {
        __handshakeTimer = null;

        console.error('[UPLOADING] check-exist handshake timeout (' + HANDSHAKE_TIMEOUT + 'ms), reset state: ' + filePath);

        __checkedFile = null;
        __inProgress  = false;
        __uploadsList = [];

        onError(CMD.errElectron.eFileUpload, ['handshake timeout']);
    }, HANDSHAKE_TIMEOUT);
}

function doUpload([fileHash, filePath, stat, okOptions]) {
    const CHUNK_SIZE    = 10 * 1024 * 1024; // 10 MB
    const MAX_RETRIES   = 7;             // цикл backoff (1→30 с) переживає перепідняття лінка після збою драйвера (5-10+ с)
    const MAX_RESTARTS  = 3;             // повних рестартів з chunk 0, коли сервер втратив стан
    const CHUNK_TIMEOUT = 2 * 60 * 1000; // стеля на висячий chunk: мертвий сокет виявляється за 2 хв (мін. швидкість ~87 KB/s на 10MB chunk)

    let fileName     = ExtractFileName(filePath);
    let fileSize     = stat.size;
    let totalChunks  = Math.max(1, Math.ceil(fileSize / CHUNK_SIZE)); // ≥1: файл 0 байт теж один (порожній) chunk
    let currentChunk = 0;
    let totalSent    = 0;
    let onePercent   = fileSize / 100;
    let retryCount   = 0;
    let restartCount = 0;
    let aborted      = false;
    let abortCtrl    = null;

    let myServer = 'http' + (MCServer.HTTPS ? 's' : '') + '://' + MCServer.ipNode
        + (MCServer.PortNode ? ':' + MCServer.PortNode : '');

    let baseUrl = myServer + '/uploading/?hash=' + fileHash;

    function sendProgressInfo() {
        let currentPercent = (totalSent / onePercent).toFixed();

        __mainWindow.webContents.send('ec_CMD', [
            CMD.ec_file_upload_progress,
            {
                size    : format_size(fileSize),
                text    : format_size(totalSent),
                percent : currentPercent,
                name    : fileName
            }
        ]);
    }

    function sendCancel() {
        fetch(baseUrl + '&cancel=1', {
            method    : 'POST',
            dispatcher: __tlsAgent
        }).catch(function(){});
    }

    function onFail(msg) {
        __abortUpload = null;
        abortCtrl     = null;

        sendCancel();

        onError(CMD.errElectron.eFileUpload, [msg]);

        __inProgress  = false;
        __uploadsList = [];
    }

    // Експоненційний backoff: 1, 2, 4, 8, 16, 30, 30 с — дає лінку час перепіднятись
    function retryDelay() {
        return Math.min(1000 * Math.pow(2, retryCount - 1), 30 * 1000);
    }

    function readChunk(start, end) {
        return new Promise(function (resolve, reject) {
            if (end <= start) { resolve(Buffer.alloc(0)); return; } // порожній chunk (файл 0 байт)

            let stream = fs.createReadStream(filePath, { start: start, end: end - 1 });
            let parts  = [];

            stream.on('data', function (c) { parts.push(c); });
            stream.on('end',  function () { resolve(Buffer.concat(parts)); });
            stream.on('error', reject);
        });
    }

    async function sendChunk() {
        if (aborted) return;

        let start = currentChunk * CHUNK_SIZE;
        let end   = Math.min(start + CHUNK_SIZE, fileSize);
        let url   = baseUrl + '&chunk=' + currentChunk + '&chunks=' + totalChunks;

        let buf;

        try {
            buf = await readChunk(start, end);
        } catch (e) {
            onFail(e.message);
            return;
        }

        if (aborted) return;

        let fd = new FormData();

        fd.append('custom_file', new Blob([buf]), fileName);

        abortCtrl = new AbortController();

        // Таймаут на завислий chunk: undici fetch не має власного timeout на віддачу тіла.
        let timeoutId = setTimeout(function () {
            try { abortCtrl.abort(); } catch (e) {}
        }, CHUNK_TIMEOUT);

        let res;

        try {
            res = await fetch(url, {
                method    : 'POST',
                body      : fd,
                signal    : abortCtrl.signal,
                dispatcher: __tlsAgent
            });
        } catch (err) {
            clearTimeout(timeoutId);

            if (aborted) return;

            console.error('[UPLOADING] CHUNK ' + currentChunk + '/' + totalChunks + ' ERROR: ' + err.message + ', code: ' + (err.code || 'none'));

            retryCount++;

            if (retryCount <= MAX_RETRIES) {
                console.log('[UPLOADING] Retrying chunk ' + currentChunk + ', attempt ' + retryCount);

                setTimeout(sendChunk, retryDelay());
            } else {
                onFail(err.message);
            }
            return;
        }

        clearTimeout(timeoutId);

        if (aborted) return;

        if (res.status >= 400) {
            let body = '';
            try { body = await res.text(); } catch(e){}

            console.error('[UPLOADING] CHUNK ' + currentChunk + ' server error: ' + res.status + ', body: ' + body.substring(0, 200));

            // Сервер втратив стан аплоаду (рестарт NODEJS, фатальна помилка) — ретраїти
            // той самий chunk марно, протокол вимагає почати заново з chunk 0
            if (body.indexOf('start from chunk 0') >= 0) {
                restartCount++;

                if (restartCount <= MAX_RESTARTS) {
                    console.log('[UPLOADING] Server lost upload state, restart from chunk 0, attempt ' + restartCount);

                    currentChunk = 0;
                    totalSent    = 0;
                    retryCount   = 0;

                    sendProgressInfo();

                    setTimeout(sendChunk, 1000);
                } else {
                    onFail('Server error: ' + res.status);
                }
                return;
            }

            retryCount++;

            if (retryCount <= MAX_RETRIES) {
                console.log('[UPLOADING] Retrying chunk ' + currentChunk + ', attempt ' + retryCount);

                setTimeout(sendChunk, retryDelay());
            } else {
                onFail('Server error: ' + res.status);
            }
            return;
        }

        let answer = null;

        try { answer = JSON.parse(await res.text()); } catch(e){}

        retryCount = 0;

        // Ресинхронізація по received: якщо відповідь на попередній chunk загубилась,
        // сервер відповідає на дубль реальним прогресом — довіряємо його лічильнику
        if (answer && typeof answer.received === 'number') {
            currentChunk = answer.received;
        } else {
            currentChunk++;
        }

        totalSent = Math.min(currentChunk * CHUNK_SIZE, fileSize);

        sendProgressInfo();

        if (currentChunk < totalChunks) {
            sendChunk();
        } else {
            console.log('[UPLOADING] COMPLETE: ' + fileName + ', total: ' + totalSent + '/' + fileSize);

            okOptions.Hash     = fileHash;
            okOptions.FileName = filePath;
            okOptions.lastModifiedDate = new Date(stat.mtime).getTime();
            okOptions.fileSize = fileSize;

            __mainWindow.webContents.send('ec_CMD', [
                CMD.ec_file_upload_complete,
                okOptions
            ]);

            __abortUpload = null;
            abortCtrl     = null;
            __inProgress  = false;

            if (hasFilesToUpload()) {
                setTimeout(function () {
                    check(__uploadsList, okOptions);
                }, 100);
            }
        }
    }

    __mainWindow.webContents.send('ec_CMD', [
        CMD.ec_file_upload_start
    ]);

    __abortUpload = function () {
        aborted = true;

        if (abortCtrl) {
            try { abortCtrl.abort(); } catch(e){}
            abortCtrl = null;
        }

        sendCancel();

        __mainWindow.webContents.send('ec_CMD', [
            CMD.ec_file_upload_aborted
        ]);

        __inProgress  = false;
        __uploadsList = [];
        __abortUpload = null;
    };

    console.log('[UPLOADING] START (chunked): ' + fileName + ', size: ' + fileSize + ' (' + format_size(fileSize) + '), chunks: ' + totalChunks + ', url: ' + baseUrl);

    sendProgressInfo();
    sendChunk();
}

// function checkExistFinish(data) {
//     let [hash, filePath, stat, okOptions] = data;
//
//     okOptions.FileName = filePath;
//     okOptions.Hash     = hash;
//     okOptions.fileSize = stat.size;
//     okOptions.lastModifiedDate = (new Date(stat.mtime)).getTime();
//
//     __mainWindow.webContents.send('ec_CMD', [
//         CMD.ec_file_check_exist,
//         {
//             info: okOptions
//             // id  : id
//         }
//     ]);
// }

function swap16(val) {
    return ((val & 0xFF) << 8)
        | ((val >> 8) & 0xFF);
}

function check(filesList, okOptions) {
    if (filesList && filesList.length){
        let filePath = "";

        if (isArray(filesList)){
            filePath = filesList.shift();
        } else {
            filePath  = filesList + "";
            filesList = [];
        }

        if (__inProgress) {
            console.log('[UPLOADING] check() while in progress, queued: ' + filePath);

            __uploadsList = __uploadsList.concat([filePath]).concat(filesList);
        } else {
            // Закриваємо вікно гонки: до завершення SHA1 другий старт має ставати в чергу,
            // а не перезаписувати __abortUpload/__checkedFile як новий процес (#6).
            __inProgress = true;

            fs.open(filePath, 'r', function (err, fd) {
            function sendProgressInfo() {
                currentPercent = onePercent ? (totalLength / onePercent).toFixed() : 100;

                __mainWindow.webContents.send('ec_CMD', [
                    CMD.ec_file_upload_prepare_progress,
                    {
                        percent : currentPercent
                    }
                ]);
            }

            // console.log("check: " + filePath);

            let progressTimer  = null;
            let currentPercent = 0;
            let totalLength    = 0;
            let stat;
            let fileSize       = 0;
            let onePercent     = 0;

            __mainWindow.webContents.send('ec_CMD', [
                CMD.ec_file_upload_prepare_start
            ]);

            if (err) {
                console.error("[Check before upload]: " + err.message);

                onError(CMD.errElectron.eFileUpload, [err.message]);

                __inProgress  = false;
                __uploadsList = [];
            } else {
                // stat після перевірки err і в try: зниклий файл/нема прав → onError, не uncaught (#4).
                try {
                    stat       = fs.statSync(filePath);
                    fileSize   = stat.size;
                    onePercent = fileSize / 100;
                } catch (e) {
                    console.error("[Check before upload]: " + e.message);

                    onError(CMD.errElectron.eFileUpload, [e.message]);

                    fs.close(fd, function(){});

                    __inProgress  = false;
                    __uploadsList = [];

                    return;
                }

                delete okOptions.resolution; // не переносити resolution з попереднього файла (#5)

                let file = fs.createReadStream(filePath, { fd: fd });
                let sha1 = crypto.createHash('sha1');

                __abortUpload = function () {
                    __mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_file_upload_aborted
                    ]);

                    file.close();
                };

                sendProgressInfo();

                file.on('data', (chunk) => {
                    if (totalLength === 0 && chunk.length > 32){
                        // let isPNG  = new Buffer([chunk[1], chunk[2], chunk[3], chunk[12], chunk[13], chunk[14], chunk[15]]);
                        let isPNG  = Buffer.from([chunk[1], chunk[2], chunk[3], chunk[12], chunk[13], chunk[14], chunk[15]]);
                        let Width  = 0;
                        let Height = 0;

                        if (isPNG.toString() === "PNGIHDR"){
                            Width = chunk[16];
                            Width = (Width << 8) + chunk[17];
                            Width = (Width << 8) + chunk[18];
                            Width = (Width << 8) + chunk[19];

                            Height = chunk[20];
                            Height = (Height << 8) + chunk[21];
                            Height = (Height << 8) + chunk[22];
                            Height = (Height << 8) + chunk[23];

                            okOptions.resolution = Width + "x" + Height;
                        }
                    }

                    totalLength += chunk.length;

                    sha1.update(chunk);

                    if (!progressTimer){
                        progressTimer = setTimeout(function () {
                            sendProgressInfo();

                            progressTimer = null;
                        }, 100);
                    }
                });

                file.on('end', () => {
                    __abortUpload = null;

                    file.close();

                    // setTimeout(() => {
                    //     if (filesList && filesList.length){
                    //         check(filesList, okOptions);
                    //     }
                    // }, 10);
                });

                file.on('error', (err) => {
                    onError(CMD.errElectron.eFileUpload, [err.message]);

                    console.error('[PREPARE UPLOAD]: ' + err.message);

                    file.close();

                    __inProgress  = false;
                    __uploadsList = [];
                });

                file.on('close', () => {
                    if (!__abortUpload){
                        sendProgressInfo();

                        let hash = sha1.digest('hex');
                        // let id   = hash + "-" + filePath;

                        __checkedFile = [hash, filePath, stat, okOptions];
                        __uploadsList = filesList;

                        // if (!__inProgress) {
                        //     checkExistFinish(__checkedFile);
                        // }

                        __inProgress = true;

                        okOptions.FileName = filePath;
                        okOptions.Hash     = hash;
                        okOptions.fileSize = stat.size;
                        okOptions.lastModifiedDate = (new Date(stat.mtime)).getTime();

                        __mainWindow.webContents.send('ec_CMD', [
                            CMD.ec_file_check_exist,
                            {
                                info: okOptions
                                // id  : id
                            }
                        ]);

                        armHandshakeTimer(filePath);
                    } else {
                        __inProgress  = false;
                        __uploadsList = [];
                    }

                    __abortUpload = null;
                    sha1 = null;
                    file = null;
                });
            }
            });
        }
    }
}

// Стрім на вбудованому Node http/https (vanilla HTTP POST + content-length + pipe),
// як це робить нативний win32-клієнт. Не undici і не Chromium-net — рідний http стек
// з коректним backpressure (ClientRequest.write() повертає false, pipe гальмує fileStream).
function streamUploadHttps([fileHash, filePath, stat, okOptions]) {
    __checkedFile = null;

    const http  = require('http');
    const https = require('https');

    let useHttps = !!MCServer.HTTPS;
    let lib      = useHttps ? https : http;

    let fileName     = ExtractFileName(filePath);
    let fileSize     = stat.size;
    let onePercent   = fileSize / 100;
    let totalSent    = 0;
    let aborted      = false;
    let bodySent     = false;
    let finished     = false;
    let lastProgress = 0;

    let host = MCServer.ipNode;
    let port = MCServer.PortNode || (useHttps ? 443 : 80);
    let path = '/uploading/?hash=' + fileHash;

    let boundary = '----mcUpload' + Math.random().toString(16).slice(2) + Date.now().toString(16);
    let enc      = new TextEncoder();

    let headerBytes = Buffer.from(enc.encode(
        '--' + boundary + '\r\n' +
        'Content-Disposition: form-data; name="custom_file"; filename="' + fileName.replace(/"/g, '\\"') + '"\r\n' +
        'Content-Type: application/octet-stream\r\n\r\n'
    ));
    let footerBytes = Buffer.from(enc.encode('\r\n--' + boundary + '--\r\n'));
    let totalLen    = headerBytes.length + fileSize + footerBytes.length;

    let fileStream = fs.createReadStream(filePath, fileSize > 0 ? { end: fileSize - 1 } : {});

    let request  = null;
    let watchdog = null;
    const STALL_TIMEOUT    = 60 * 1000;
    const RESPONSE_TIMEOUT = 120 * 1000;

    function sendProgressInfo() {
        let currentPercent = onePercent ? (totalSent / onePercent).toFixed() : 100;

        __mainWindow.webContents.send('ec_CMD', [
            CMD.ec_file_upload_progress,
            {
                size    : format_size(fileSize),
                text    : format_size(totalSent),
                percent : currentPercent,
                name    : fileName
            }
        ]);
    }

    function sendCancel() {
        try {
            let r = lib.request({
                method: 'POST', host: host, port: port,
                path  : '/uploading/?hash=' + fileHash + '&cancel=1',
                rejectUnauthorized: false
            });
            r.on('error', function(){});
            r.end();
        } catch (e){}
    }

    function clearWatchdog() {
        if (watchdog) { clearTimeout(watchdog); watchdog = null; }
    }

    function abortTransport() {
        try { fileStream.destroy(); } catch(e){}
        try { if (request) request.destroy(); } catch(e){}
    }

    function onFail(msg) {
        aborted       = true;
        __abortUpload = null;

        clearWatchdog();

        if (!bodySent) sendCancel();

        onError(CMD.errElectron.eFileUpload, [msg]);

        __inProgress  = false;
        __uploadsList = [];
    }

    function finishSuccess() {
        if (finished) return;
        finished = true;

        clearWatchdog();
        sendProgressInfo();

        console.log('[UPLOADING] COMPLETE (https): ' + fileName + ', total: ' + totalSent + '/' + fileSize);

        okOptions.Hash             = fileHash;
        okOptions.FileName         = filePath;
        okOptions.lastModifiedDate = new Date(stat.mtime).getTime();
        okOptions.fileSize         = fileSize;

        __mainWindow.webContents.send('ec_CMD', [
            CMD.ec_file_upload_complete,
            okOptions
        ]);

        __abortUpload = null;
        __inProgress  = false;

        if (hasFilesToUpload()) {
            setTimeout(function () {
                check(__uploadsList, okOptions);
            }, 100);
        }
    }

    function armWatchdog() {
        clearWatchdog();

        watchdog = setTimeout(function () {
            watchdog = null;
            if (aborted) return;

            console.error('[UPLOADING] HTTPS stalled ' + STALL_TIMEOUT + 'ms, aborting: ' + fileName);

            abortTransport();
            onFail('Upload stalled (timeout)');
        }, STALL_TIMEOUT);
    }

    function armResponseWatchdog() {
        clearWatchdog();

        watchdog = setTimeout(function () {
            watchdog = null;
            if (aborted || finished) return;

            console.error('[UPLOADING] HTTPS response timeout ' + RESPONSE_TIMEOUT + 'ms, aborting: ' + fileName);

            abortTransport();
            onFail('Server response timeout');
        }, RESPONSE_TIMEOUT);
    }

    __abortUpload = function () {
        aborted = true;

        clearWatchdog();
        abortTransport();
        sendCancel();

        __mainWindow.webContents.send('ec_CMD', [
            CMD.ec_file_upload_aborted
        ]);

        __inProgress  = false;
        __uploadsList = [];
        __abortUpload = null;
    };

    request = lib.request({
        method : 'POST',
        host   : host,
        port   : port,
        path   : path,
        rejectUnauthorized: false,
        headers: {
            'content-type'  : 'multipart/form-data; boundary=' + boundary,
            'content-length': String(totalLen)
        }
    }, function (response) {
        if (aborted || finished) return;

        // Відповідь прибула → рішення дефінітивне, знімаємо watchdog (#1).
        clearWatchdog();

        let status = response.statusCode;

        response.on('data', function(){}); // дренуємо тіло відповіді
        response.on('error', function (e) {
            if (aborted || finished) return;
            onFail('Response error: ' + (e.message || e));
        });
        response.on('end', function () {
            if (aborted || finished) return;

            if (status >= 400) {
                console.error('[UPLOADING] HTTPS server error: ' + status);
                onFail('Server error: ' + status);
                return;
            }

            finishSuccess();
        });
    });

    request.on('error', function (err) {
        if (aborted || finished) return;

        console.error('[UPLOADING] HTTPS ERROR: ' + (err && err.message)
            + ', code: ' + ((err && err.code) || 'none')
            + ', sent: ' + totalSent + '/' + fileSize + ', bodySent: ' + bodySent);

        onFail((err && err.message) || 'https request error');
    });

    __mainWindow.webContents.send('ec_CMD', [
        CMD.ec_file_upload_start
    ]);

    sendProgressInfo();
    armWatchdog();

    console.log('[UPLOADING] START (https): ' + fileName + ', size: ' + fileSize + ' (' + format_size(fileSize) + '), url: '
        + (useHttps ? 'https' : 'http') + '://' + host + ':' + port + path);

    // header першим (синхронно в буфер запиту), далі стрім файла з нативним backpressure pipe.
    request.write(headerBytes);

    fileStream.on('data', function (chunk) {
        if (aborted) return;

        totalSent += chunk.length;

        let now = Date.now();
        if (now - lastProgress > 100) {
            lastProgress = now;
            sendProgressInfo();
            armWatchdog();
        }
    });

    fileStream.on('error', function (err) {
        if (aborted || finished) return;

        abortTransport();
        onFail('File read error: ' + (err.message || err));
    });

    fileStream.on('end', function () {
        if (aborted) return;

        bodySent = true;

        request.end(footerBytes); // footer + завершення тіла

        armResponseWatchdog();
    });

    // Node ClientRequest має коректний backpressure → pipe сам гальмує fileStream.
    fileStream.pipe(request, { end: false });
}

function init(dialog, mainWindow) {
    __dialogs    = dialog;
    __mainWindow = mainWindow;
}

module.exports = function () {
    init.apply(null, arguments);

    return {
        init,
        check,
        setMode: function (mode) { __streamMode = (mode !== "block"); },
        Upload: function (exist, okOptions) {
            clearHandshakeTimer();

            // if (__uploadsList.hasOwnProperty(id)){
            //     doUpload.apply(null, __uploadsList[id]);
            //
            //     delete __uploadsList[id];
            // }
            if (!exist && __checkedFile){
                if (__streamMode) {
                    streamUploadHttps(__checkedFile);
                } else {
                    doUpload(__checkedFile);
                }
            } else
            if (exist && __uploadsList.length){
                __inProgress  = false;

                check(__uploadsList, okOptions);
            } else {
                __inProgress  = false;
            }
        },
        abortUpload: function () {
            if (__abortUpload){
                __abortUpload();
            }
        },
        // openDialogUpload
    }
};
