/*
  Сервис интеграции ПП Парус 8 с WEB API
  Модуль ядра: запись протокола работы в файл
*/

//----------------------
// Подключение библиотек
//----------------------

const fs = require("fs"); //Файловая система
const path = require("path"); //Пути файловой системы
const { validateObject, getNowString, makeErrorText } = require("./utils"); //Вспомогательные функции
const { ServerError } = require("./server_errors"); //Типовая ошибка
const {
    SCONSOLE_LOG_COLOR_PATTERN_ERR,
    SCONSOLE_LOG_COLOR_PATTERN_INF,
    SERR_COMMON,
    SLOG_PREFIX_INF,
    SLOG_PREFIX_ERR,
    SLOG_PREFIX_PROTOCOL_ERR
} = require("./constants"); //Общие константы
const prmsLogFileWriterSchema = require("../models/prms_log_file_writer"); //Схемы валидации параметров функций модуля

//--------------------------
// Глобальные идентификаторы
//--------------------------

//Таймаут ожидания опустошения очереди записи в файл при закрытии (мс)
const NLOG_FILE_CLOSE_TIMEOUT = 10000;

//Интервал повторного сообщения об ошибке записи в файл (мс)
const NLOG_FILE_ERROR_REPORT_INTERVAL = 10000;

//Интервал повторного открытия файла лога после ошибки (мс)
const NLOG_FILE_REOPEN_INTERVAL = 10000;

//Максимум строк в очереди записи (защита памяти при высокой нагрузке / медленном диске)
const NLOG_FILE_MAX_PENDING = 2000;

//------------
// Тело модуля
//------------

//Дополнение числа ведущим нулём до двух знаков
const pad2 = nVal => String(nVal).padStart(2, "0");

//Формирование имени файла лога: log_yyyymmdd_hh24miss.log
const buildLogFileName = () => {
    const dNow = new Date();
    const sDate = `${dNow.getFullYear()}${pad2(dNow.getMonth() + 1)}${pad2(dNow.getDate())}`;
    const sTime = `${pad2(dNow.getHours())}${pad2(dNow.getMinutes())}${pad2(dNow.getSeconds())}`;
    return `log_${sDate}_${sTime}.log`;
};

//Проверка, что каталог существует и доступен для записи
const checkLogDirWritable = sPath => {
    try {
        //Каталог должен существовать
        if (!fs.existsSync(sPath) || !fs.statSync(sPath).isDirectory()) return false;
        //Права на запись
        fs.accessSync(sPath, fs.constants.W_OK);
        //Пробная запись
        const sProbe = path.join(sPath, `.exs_log_write_probe_${process.pid}`);
        fs.writeFileSync(sProbe, "");
        fs.unlinkSync(sProbe);
        return true;
    } catch {
        return false;
    }
};

//Признак, что поток записи в файл непригоден
const isLogStreamBroken = stream => !stream || stream.destroyed || !stream.writable;

//Гарантия перевода строки в конце текста
const ensureLogLineEnd = sLine => (sLine.endsWith("\n") ? sLine : `${sLine}\n`);

//Безопасное уничтожение потока записи (removeAllListeners + destroy)
const destroyLogStream = stream => {
    if (!stream) return null;
    try {
        stream.removeAllListeners("error");
        if (!stream.destroyed) stream.destroy();
        return null;
    } catch (e) {
        return e;
    }
};

//Класс записи протокола в файл
class LogFileWriter {
    //Конструктор класса
    constructor() {
        this.logStream = null;
        this.sLogFile = null;
        this.bFileLog = false;
        this.bClosing = false;
        this.writeChain = Promise.resolve();
        this.nQueued = 0;
        this.nLastFileWriteErrorAt = 0;
        this.nLastLogFileReopenAt = 0;
        this.sPendingLogLine = null;
    }
    //Признак активного протоколирования в файл
    isEnabled() {
        return this.bFileLog === true;
    }
    //Сброс состояния писателя (без уничтожения потока)
    clearState() {
        this.bFileLog = false;
        this.bClosing = false;
        this.logStream = null;
        this.sLogFile = null;
        this.writeChain = Promise.resolve();
        this.nQueued = 0;
        this.nLastFileWriteErrorAt = 0;
        this.nLastLogFileReopenAt = 0;
        this.sPendingLogLine = null;
    }
    //Создание потока записи в файл
    createWriteStream(sFilePath) {
        const stream = fs.createWriteStream(sFilePath, { flags: "a", encoding: "utf8" });
        stream.on("error", e => this.handleWriteError(e));
        return stream;
    }
    //Уничтожение текущего потока записи без сброса пути файла
    destroyStream() {
        destroyLogStream(this.logStream);
        this.logStream = null;
    }
    //Уведомление в консоль о протоколировании в файл (без записи в файл)
    notifyOpenedConsole() {
        if (!this.isEnabled() || !this.sLogFile) return;
        const sNow = getNowString();
        console.log(SCONSOLE_LOG_COLOR_PATTERN_INF, `${sNow} ${SLOG_PREFIX_INF}: `, `Протоколирование в файл: ${this.sLogFile}`);
    }
    //Уведомление в консоль о результате закрытия файла лога (без записи в файл)
    notifyClosedConsole(sLogFile, e) {
        const sNow = getNowString();
        if (e) {
            console.log(
                SCONSOLE_LOG_COLOR_PATTERN_ERR,
                `${sNow} ${SLOG_PREFIX_ERR}: `,
                `Ошибка закрытия файла лога${sLogFile ? ` (${sLogFile})` : ""}: ${makeErrorText(e)}`
            );
            return;
        }
        if (!sLogFile) return;
        console.log(SCONSOLE_LOG_COLOR_PATTERN_INF, `${sNow} ${SLOG_PREFIX_INF}: `, `Файл лога закрыт: ${sLogFile}`);
    }
    //Сообщение об ошибке записи в файл с ограничением частоты
    reportWriteError(e) {
        const nNow = Date.now();
        //Ограничиваем спам повторных сообщений
        if (nNow - this.nLastFileWriteErrorAt < NLOG_FILE_ERROR_REPORT_INTERVAL) return;
        this.nLastFileWriteErrorAt = nNow;
        const sNow = getNowString();
        console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${sNow} ${SLOG_PREFIX_PROTOCOL_ERR} В ФАЙЛ: `, makeErrorText(e));
    }
    //Обработка ошибки записи в файл лога
    handleWriteError(e, sFailedLine) {
        //Если протоколирование в файл не активно - выходим
        if (!this.bFileLog && !this.sLogFile) return;
        //Сообщим в консоль (с ограничением спама)
        this.reportWriteError(e);
        //Запомним неудачную строку для однократного повтора после переоткрытия
        if (sFailedLine) this.sPendingLogLine = sFailedLine;
        //При закрытии не пытаемся восстановить поток - close/release завершат освобождение
        if (this.bClosing) return;
        //Повтор только после переоткрытия непригодного потока (без цикла на "живом" потоке)
        if (isLogStreamBroken(this.logStream)) {
            if (this.reopen()) this.retryPendingLine();
        }
    }
    //Открытие файла лога
    open(sLogPath) {
        //Если путь не задан - протоколирование в файл не ведётся
        if (!sLogPath || !String(sLogPath).trim()) return;
        //Нормализуем путь
        const sPath = path.resolve(String(sLogPath).trim());
        //Проверяем параметры
        let sCheckResult = validateObject({ sLogPath: sPath }, prmsLogFileWriterSchema.open, "Параметры функции открытия файла лога");
        if (sCheckResult) throw new ServerError(SERR_COMMON, sCheckResult);
        //Каталог должен существовать и быть доступен для записи
        if (!checkLogDirWritable(sPath))
            throw new ServerError(SERR_COMMON, `Каталог размещения логов не существует или недоступен для записи: ${sPath}`);
        //Если уже открыт - сначала освободим без сообщения в console
        if (this.logStream || this.bFileLog) this.release(undefined, true);
        //Формируем полный путь файла сессии
        const sFullPath = path.join(sPath, buildLogFileName());
        //Открываем поток записи
        this.logStream = this.createWriteStream(sFullPath);
        this.sLogFile = sFullPath;
        this.bFileLog = true;
    }
    //Повторное открытие текущего файла лога
    reopen() {
        //Нет пути файла или идёт закрытие - не переоткрываем
        if (!this.sLogFile || this.bClosing) return false;
        //Ограничиваем частоту переоткрытия
        const nNow = Date.now();
        if (this.nLastLogFileReopenAt > 0 && nNow - this.nLastLogFileReopenAt < NLOG_FILE_REOPEN_INTERVAL) return false;
        this.nLastLogFileReopenAt = nNow;
        try {
            //Освободим прежний поток, если он ещё есть
            this.destroyStream();
            //Откроем тот же файл на дозапись
            this.logStream = this.createWriteStream(this.sLogFile);
            this.bFileLog = true;
            return true;
        } catch (e) {
            this.logStream = null;
            this.reportWriteError(e);
            return false;
        }
    }
    //Освобождение ресурсов файла лога
    release(eNotify, bSilentNotify) {
        const stream = this.logStream;
        const sLogFile = this.sLogFile;
        this.clearState();
        const eClose = destroyLogStream(stream);
        //Результат закрытия - только в console
        if (!bSilentNotify && sLogFile) this.notifyClosedConsole(sLogFile, eNotify !== undefined ? eNotify : eClose);
    }
    //Запись одной строки в файл
    writeLine(sData) {
        return new Promise((resolve, reject) => {
            const stream = this.logStream;
            //Уже поставленные в очередь строки дописываем даже при bClosing (close ждёт writeChain)
            if (isLogStreamBroken(stream) || !this.bFileLog) {
                resolve();
                return;
            }
            //Пишем строку целиком
            stream.write(sData, err => {
                if (err) reject(err);
                else resolve();
            });
        });
    }
    //Постановка записи в сериализованную очередь (с ограничением глубины)
    scheduleWrite(sData) {
        //Защита от разрастания очереди при отставании диска
        if (this.nQueued >= NLOG_FILE_MAX_PENDING) {
            this.reportWriteError(new Error(`Переполнена очередь записи в файл лога (лимит ${NLOG_FILE_MAX_PENDING}), строка пропущена`));
            return;
        }
        this.nQueued++;
        this.writeChain = this.writeChain
            .then(() => this.writeLine(sData))
            .catch(e => this.handleWriteError(e, sData))
            .finally(() => {
                if (this.nQueued > 0) this.nQueued--;
            });
    }
    //Повторная запись строки, не попавшей в файл из-за ошибки
    retryPendingLine() {
        //Нечего повторять или нельзя писать
        if (!this.sPendingLogLine || this.bClosing || isLogStreamBroken(this.logStream)) return;
        const sData = this.sPendingLogLine;
        this.sPendingLogLine = null;
        this.scheduleWrite(sData);
    }
    //Постановка строки в очередь записи в файл (без ожидания записи вызывающей стороной)
    enqueue(sLine) {
        //Проверяем параметры
        if (typeof sLine !== "string") return;
        //Если файл не активен или идёт закрытие - ничего не делаем
        if (!this.bFileLog || this.bClosing) return;
        //Если поток потерян - попробуем переоткрыть
        if (isLogStreamBroken(this.logStream)) {
            if (!this.reopen()) return;
        }
        //Однократный повтор отложенной строки (если была ошибка записи)
        this.retryPendingLine();
        if (isLogStreamBroken(this.logStream)) return;
        //Гарантируем перевод строки и ставим в очередь
        this.scheduleWrite(ensureLogLineEnd(sLine));
    }
    //Синхронная дозапись строки в файл (авария / process.on("exit"))
    appendSync(sLine) {
        if (!this.sLogFile || typeof sLine !== "string") return;
        try {
            if (this.logStream) {
                this.destroyStream();
                this.bFileLog = false;
            }
            fs.appendFileSync(this.sLogFile, ensureLogLineEnd(sLine), "utf8");
        } catch (e) {
            this.reportWriteError(e);
        }
    }
    //Корректное закрытие файла лога
    async close() {
        //Если файла нет - нечего закрывать
        if (!this.logStream && !this.bFileLog && !this.sLogFile) return;
        //Фиксируем режим закрытия (новые строки и reopen не принимаем)
        this.bClosing = true;
        this.sPendingLogLine = null;
        const sLogFile = this.sLogFile;
        let nDrainTimer = null;
        try {
            //Дождёмся опустошения очереди записи (с ограничением по времени)
            const bDrained = await Promise.race([
                this.writeChain.then(() => true).catch(() => true),
                new Promise(resolve => {
                    nDrainTimer = setTimeout(() => resolve(false), NLOG_FILE_CLOSE_TIMEOUT);
                })
            ]);
            if (nDrainTimer) {
                clearTimeout(nDrainTimer);
                nDrainTimer = null;
            }
            //Очередь не успела опустеть - принудительно освободим файл
            if (!bDrained) {
                this.reportWriteError(
                    new Error(`Очередь записи в файл лога не опустела за ${NLOG_FILE_CLOSE_TIMEOUT} мс, файл будет закрыт принудительно`)
                );
                this.release();
                return;
            }
            //Закроем поток штатно
            await new Promise((resolve, reject) => {
                if (!this.logStream || this.logStream.destroyed) {
                    resolve();
                    return;
                }
                this.logStream.end(err => {
                    if (err) reject(err);
                    else resolve();
                });
            });
        } catch (e) {
            //Только сообщение - без reopen при закрытии
            this.reportWriteError(e);
            this.release(e);
            return;
        } finally {
            if (nDrainTimer) clearTimeout(nDrainTimer);
        }
        //Освобождаем состояние без повторного destroy уже закрытого потока
        if (this.logStream) {
            try {
                this.logStream.removeAllListeners("error");
            } catch {}
        }
        this.clearState();
        //Результат закрытия - только в console
        this.notifyClosedConsole(sLogFile, null);
    }
    //Аварийное синхронное освобождение файла лога
    closeSync() {
        if (!this.logStream && !this.bFileLog && !this.sLogFile) return;
        this.bClosing = true;
        this.release();
    }
}

//-----------------
// Интерфейс модуля
//-----------------

exports.LogFileWriter = LogFileWriter;
