 /**
 * Created by Gifer on 13.02.2017.
 */

 var console  = require('gifer-console');
 var Service  = require('./service.js');
 var CMD      = require('../client/modules/cmd/cmd').CMD;
 var web_client_connect = require('./web_client_connect');

 /*var oneDay = 1000*60*60*24;
 var CRLF   = "\r\n";
 var CR     = "\r";
 var LF     = "\n";*/

 var ServToClientCMD = {};
 var ClientToServCMD = require('./clientCMD').ClientToServCMD;
 var tokenStore      = require('./tokenStore');
 var DbWork          = require('./dbwork');
 // mcconnect тягнемо ОБ'ЄКТОМ модуля і кличемо в рантаймі (потрібен dropTokenAndRestart):
 // require між цими двома файлами циклічний, тож на момент виконання цього тіла експорти
 // mcconnect ще порожні, і деструктуризація зафіксувала б undefined назавжди
 var MCConnect       = require('./mcconnect');

 const TOKEN_LIFE_DAYS  = 30;
 const TOKEN_REUSABLE   = 1;
 // Тип ЗАСТОСУНКУ для токена, а не тип клієнтської програми — це різні словники.
 // Перевірено живцем на сервері 2026.6.1: для reconnect-токена підходить ТІЛЬКИ "chat".
 // Платформний тип із cs_hello ('win32next') сервер відхиляє помилкою 216
 // («неверный тип приложения для токена») уже на вході, хоча токен із ним створюється.
 // Літерал, а не CMD.ClientType.WEB_CHAT: збіг значень випадковий, це різні набори, і зв'язати
 // їх константою означало б повторити ту саму плутанину словників.
 // Те саме значення мусить поїхати в Client команди cs_login_by_token — див. sc_accept_connection.
 const TOKEN_WHERE      = "chat";

 var penaltyList  = [];
 var LoginMethods = {};
 var firstLogin   = false;
 // var serverHWID   = "";

 LoginMethods[CMD.Login]    = CMD.cs_login;
 LoginMethods[CMD.Register] = CMD.cs_register_new_user;
 LoginMethods[CMD.cs_login_by_token] = CMD.cs_login_by_token;

 //====================== SEVER TO CLIENT ===============================

/*    moved to mcconnect
 ServToClientCMD[CMD.sc_hello]                = function(_data){
     var socket = this;

     process.nextTick(function(){
         socket.ServerInfo = _data;

         socket.UserInfo.HWID     = socket.ServerInfo.HWID;
         socket.UserInfo.ServerID = socket.ServerInfo.ID;

         serverHWID           = socket.ServerInfo.HWID;
         // serverHWID           = socket.ServerInfo.ID;

         MCServer.PortNode    = socket.ServerInfo.PortNode;
         MCServer.HTTPS       = socket.ServerInfo.HTTPS;
         MCServer.PortFTP     = socket.ServerInfo.PortFTP;
         MCServer.CompanyName = socket.ServerInfo.Registered;
         MCServer.ServerName  = socket.ServerInfo.ServName;
         MCServer.ServerID    = socket.UserInfo.ServerID;

         console.info("ServerID: " + socket.UserInfo.ServerID);
         console.log('Hello:\n' + JSON.stringify(socket.ServerInfo), console.logLevel.L_Full);

         socket.SendDataToServer(ClientToServCMD[CMD.cs_hello](socket));
     });
 };
*/

 ServToClientCMD[CMD.sc_accept_connection]    = function(){
     var socket = this;

     process.nextTick(function(){
         console.log('UserInfo:\n' + Service.redactSecrets(socket.UserInfo), console.logLevel.L_Full);

         if (LoginMethods.hasOwnProperty(socket.UserInfo.authType)){
             let loginInfo = socket.UserInfo;

             // Client у cs_login_by_token мусить дорівнювати Where, з яким токен замовлявся.
             // Підміна ЛИШЕ для цієї команди й через копію: сам UserInfo.ClientType лишається
             // платформним, бо його читають cs_login (гілка websupport) і isAdminSid
             if (loginInfo.authType === CMD.cs_login_by_token){
                 loginInfo = Service.extend({}, socket.UserInfo, { ClientType : TOKEN_WHERE });
             }

             socket.SendDataToServer(ClientToServCMD[LoginMethods[loginInfo.authType]](loginInfo));
         }
     });
 };

 ServToClientCMD[CMD.sc_login]                = function(_data){
     var socket = this;
     var out    = {};

     console.important("User UIN: " + _data.UIN + " Successfully Logged In! sID: " + socket.UserInfo.sessionID, console.logLevel.L_Full);
     console.log("sc_login: " + JSON.stringify(_data), console.logLevel.L_Full);

     out[CMD.sc_login] = _data;
     out[CMD.sc_login].HWID     = socket.ServerInfo.HWID;
     out[CMD.sc_login].PortNode = socket.ServerInfo.PortNode;
     out[CMD.sc_login].HTTPS    = socket.ServerInfo.HTTPS;
     out[CMD.sc_login].PortFTP  = socket.ServerInfo.PortFTP;
     out[CMD.sc_login].CompanyName = MCServer.CompanyName;
     out[CMD.sc_login].ServerName  = MCServer.ServerName;
     out[CMD.sc_login].useAltAddr  = MCServer.useAltAddr;
     out[CMD.sc_login].LicenseFree = MCServer.LicenseFree;
     out[CMD.sc_login].ServID      = socket.ServerInfo.ID;
     out[CMD.sc_login].CID         = socket.ServerInfo.CID;
     out[CMD.sc_login].CurrentDT   = socket.ServerInfo.CurrentDT; // "CurrentDT" from 8000 sc_hello  : "26.11.2012.09.18.31.785",    // UTC текущие дата и время на сервере в формате dd.mm.yyyy.hh.nn.ss.zzz

     MCServer.SID        = _data.sid;

     socket.UserInfo.UIN = _data.UIN;
     socket.UserInfo.LoggedIn = true;

     if (firstLogin){
         firstLogin = false;

         Service.Delete_File(MCPathes.ProfilNode + 'first.login');
     }

     this.UsersInBuffer.AddDataToInBuf(out);

     // Строго ПІСЛЯ пуша в буфер: саме цей рядок віддає renderer успішний вхід, а ланцюг нижче —
     // три звернення до SQLite плюс крипта. На вміст out він не впливає.
     processReconnectToken(socket, _data);
 };

 // Порядок жорсткий: чистка чужих → реюз-перевірка по ПАРІ → замовлення.
 // Перевірка наявності до чистки зробила б вхід іншого користувача на той самий сервер схожим на
 // «токен уже є»: видача пропускається, чужий запис потім усе одно зноситься — і клієнт лишається
 // без токена й без пароля.
 function processReconnectToken(socket, _data){
     let servId = socket.ServerInfo.ID; // серверна ідентичність, НЕ mcservers.rowid із getMyServerID()
     let uin    = _data.UIN;

     // Синхронно, до будь-якого await, і лише коли пара справді інша: dropOther іде на КОЖНОМУ
     // sc_login, зокрема на звичайному reconnect тією ж парою, а беззастережний cancel() там убив
     // би власну чинну операцію — і пустив би друге замовлення, лишивши перший токен сиротою
     tokenStore.cancelIfOther(servId, uin);

     return DbWork.do.reconnectToken.dropOther(servId, uin)
         .then(function () {
             return DbWork.do.reconnectToken.get();
         })
         .then(function (rows) {
             // Живий токен є, а пароль міг пережити обрив: крок обнулення після round-trip
             // спрацьовує тільки на свіжій видачі, а reuse-first нових токенів не замовляє —
             // без цього добивання такий пароль лишився б у БД назавжди
             if (findPair(rows, servId, uin)) return clearStoredPassword();

             if (socket.UserInfo.RM !== true) return;      // галка «запам'ятати» має лишатись чесною

             if (tokenStore.isBusy()) return;              // замовлення вже в польоті

             // Постійний збій видачі не має перетворюватись на замовлення при кожному вході
             if (tokenStore.isGivenUp()) return;

             // З'єднання могло закритись, поки йшли два запити до SQLite. Без цієї звірки застарілий
             // ланцюг створив би операцію на мертвому сокеті, а вона на 30 секунд (до таймауту)
             // блокувала б замовлення новому з'єднанню
             // Обрив зв'язку не рахуємо невдачею видачі: на нестабільному каналі саме повтор і
             // потрібен, а лічильник здався б рівно там, де він шкодить
             if (socket.isClosed){
                 console.warn("(token) connection closed before order, chain aborted");

                 return;
             }

             orderToken(socket, servId, uin);
         })
         .catch(function (e) {
             tokenStore.noteFailure("sc_login token chain: " + (e && e.message ? e.message : e));
         });
 }

 // Безумовним методом: guard на записі спрацьовує саме на «токен є», що і є передумовою цього виклику
 function clearStoredPassword(){
     return DbWork.do.system.loadPrvPwd()
         .then(function (rows) {
             let info = (rows || [])[0];

             if (!info || (!info.pwd && !info.srvpwd)) return;

             return DbWork.do.system.clearPwd()
                 .then(function () {
                     console.log("(token) leftover stored password cleared", console.logLevel.INFO);
                 });
         });
 }

 function findPair(rows, servId, uin){
     return (rows || []).filter(function (row) {
         return row.serv_id === servId && String(row.uin) === String(uin);
     })[0];
 }

 function orderToken(socket, servId, uin){
     let expired = tokenStore.expiresFrom(socket.ServerInfo.CurrentDT, TOKEN_LIFE_DAYS);

     // Expired мусить бути готовим рядком: порожній дав би дефолт білдера 01.01.3000,
     // тобто вічний і невідкличний токен
     if (!expired){
         tokenStore.noteFailure("bad server CurrentDT, order skipped: " + socket.ServerInfo.CurrentDT);

         return;
     }

     let where = TOKEN_WHERE;
     let op    = tokenStore.begin(socket, servId, uin, expired);

     console.log("(token) order #" + op.id + " cs_create_token: Where=" + where + ", Link='', Type=" + TOKEN_REUSABLE +
                 ", Expired=" + expired + ", BindIP=false, BindCID=false", console.logLevel.INFO);

     // Білдер позиційний — cs_create_token(Where, Link, Type, Expired, BindIP, BindCID).
     // Порожній Link пропустити не можна: при зсуві Type стане parseInt("26.12...")=26,
     // а Expired отримає false — токен перестане бути TOKEN_REUSABLE без жодного симптому на клієнті
     // BindCID=false: прив'язка до CID не переживає перезапуск клієнта — токен ставав непридатним
     // рівно там, де він і потрібен
     socket.SendDataToServer(ClientToServCMD[CMD.cs_create_token](where, "", TOKEN_REUSABLE, expired, false, false));
 }

 // Where вирішує, чий пакет; писати чи ні — вирішує стан операції
 ServToClientCMD[CMD.sc_token_created]        = function (_data) {
     var socket = this;

     // Без дискримінатора реєстрація обробника з'їдає ВСІ sc_token_created, а renderer матчить свої
     // відповіді спільною FIFO-чергою: зламались би FTP, форум, kanban, адмінка, термінал і support —
     // причому з підміною чужих токенів, а не з явною помилкою.
     // Перетину немає: renderer шле forum / kanban / admin / websupport / ftp_* / win32, але не chat
     if (!_data || _data.Where !== TOKEN_WHERE){
         socket.ServerToClientMultiCMD(CMD.sc_token_created, _data);

         return;
     }

     let op = tokenStore.accept(socket, socket.ServerInfo.ID, socket.UserInfo.UIN);

     if (!op){
         console.warn("(token) sc_token_created is ours, but no matching pending operation - dropped");

         return;
     }

     console.log("(token) sc_token_created accepted for order #" + op.id, console.logLevel.INFO);

     storeToken(op, _data.Token);
 };

 // expired пишемо З ОПЕРАЦІЇ: у відповіді сервера його немає взагалі
 function storeToken(op, token){
     let enc;

     try {
         enc = tokenStore.encrypt(token);
     } catch (e){
         tokenStore.noteFailure("encrypt: " + (e && e.message ? e.message : e));

         tokenStore.release(op);

         return;
     }

     // ok / fail / cancelled — рахуємо результат ОДИН раз наприкінці, інакше збій delete усередині
     // гілки провалу дав би два записи в лічильник за одну невдачу
     let outcome = 'ok';

     return DbWork.do.reconnectToken.set(op.servId, op.uin, enc.data, op.expired, enc.enc)
         .then(function () {
             return DbWork.do.reconnectToken.get();
         })
         .then(function (rows) {
             let row = findPair(rows, op.servId, op.uin);
             let ok  = false;

             try {
                 ok = !!row && tokenStore.decrypt(row.token, row.enc) === token;
             } catch (e){
                 ok = false;
             }

             // Провал round-trip: прибрати СВІЙ рядок і лишити все як було — пароль на місці.
             // Точково, а не dropAll(): поки ми писали, міг увійти інший акаунт і покласти свій
             if (!ok){
                 outcome = 'fail';

                 console.warn("(token) round-trip check failed, row dropped");

                 return DbWork.do.reconnectToken.drop(op.servId, op.uin);
             }

             // Logout, що трапився посеред запису, лишив би токен у БД — він ліг би туди
             // мілісекундою пізніше за чистку. Це не невдача видачі — користувач сам вийшов
             if (tokenStore.isCancelled(op)){
                 outcome = 'cancelled';

                 console.log("(token) order #" + op.id + " cancelled during write - final delete", console.logLevel.INFO);

                 return DbWork.do.reconnectToken.drop(op.servId, op.uin);
             }

             console.log("(token) reconnect token stored, expired " + op.expired, console.logLevel.INFO);

             // Пароль стираємо СТРОГО після збігу round-trip: проміжний стан «токен є + пароль є»
             // безпечний, зворотний — ні. Безумовним методом: guard блокує запис саме тоді, коли
             // токен уже є, тобто рівно в цей момент
             return DbWork.do.system.clearPwd()
                 .then(function () {
                     console.log("(token) stored password cleared", console.logLevel.INFO);
                 });
         })
         .catch(function (e) {
             outcome = 'fail';

             console.error("(token) store failed: " + (e && e.message ? e.message : e));
         })
         .then(function () {
             if (outcome === 'fail'){
                 tokenStore.noteFailure("store chain");
             } else
             if (outcome === 'ok'){
                 tokenStore.noteSuccess();
             }

             tokenStore.release(op);
         });
 }

 ServToClientCMD[CMD.sc_halt]                 = function (){
     var socket = this;

     process.nextTick(function(){
         socket.SendDataToServer();

         socket.CloseConnection();

         console.important('Server send HALT. Terminate web-client session.');
     });
 };

 ServToClientCMD[CMD.sc_error]                = function(_data){
     var socket = this;

     // Мертвий токен. Гілка вмикається лише коли поточний вхід був токен-логіном: ті самі коди на
     // парольному вході належать паролю, і їх треба показати користувачу як завжди
     if ((_data.ErrNum == 215 || _data.ErrNum == 216) && socket.UserInfo.authType === CMD.cs_login_by_token){
         let started = MCConnect.dropTokenAndRestart("server rejected token, error " + _data.ErrNum);

         // false = рестарт уже йде (друга помилка на ту саму спробу) — ковтаємо
         if (started === false) return;

         // Помилку в renderer НЕ віддаємо: клієнт перезапуститься сам.
         // Не вдалось прибрати токен — деградуємо до звичайної модалки, інакше рестарт зациклиться
         started.catch(function () {
             passErrorToRenderer(socket, _data);
         });

         return;
     }

     passErrorToRenderer(socket, _data);
 };

 function passErrorToRenderer(socket, _data){
     process.nextTick(function(){
         var out           = {};
         out[CMD.sc_error] = _data;
         out[CMD.sc_error].HWID     = socket.ServerInfo.HWID;
         out[CMD.sc_error].PortNode = socket.ServerInfo.PortNode;
         out[CMD.sc_error].HTTPS    = socket.ServerInfo.HTTPS;
         out[CMD.sc_error].PortFTP  = socket.ServerInfo.PortFTP;

         socket.UsersInBuffer.AddDataToInBuf(out);

         if (_data.ErrNum == 14){
             penaltyList.push(socket.UserInfo.login);
         }

         console.warn("MCServer ERROR: [" + _data.ErrNum + "] " + Service.myReplace(global.MCServer.ErrorText[_data.ErrNum], [].concat(_data.Params)));
     });
 }

 ServToClientCMD[CMD.sc_web_user_foto_file]   = function(_data){
     if (_data.FileName != ""){
         var uin = this.UserInfo.UIN;
         var res = require('./router').AddRandomPath({
             source  : _data.FileName,
             fileName: '/' + Service.ExtractFileName(_data.FileName),
             UIN     : uin
         });

         var out = {};
         out[CMD.sc_web_user_foto_file] = {
             "UIN"      : uin,
             "FileName" : res[1]
         };

         this.UsersInBuffer.AddDataToInBuf(out);
     }
 };

 ServToClientCMD[CMD.sc_get_all_rights]       = function(_data){
     var out = {};

     out[CMD.sc_get_all_rights] = _data;

     this.RightSet = ' ' + _data.RightsSet;

     console.log('Received cmd: ' + CMD.sc_get_all_rights, console.logLevel.L_Extended);

     this.UsersInBuffer.AddDataToInBuf(out);
 };

 ServToClientCMD[CMD.sc_ping]                 = function (){
     var socket = this;

     if (socket.UserInfo.LoggedIn){
         console.log('Send PONG to server!', console.logLevel.L_Full);
         
         socket.SendDataToServer(ClientToServCMD[CMD.cs_ping]());
     }
 };

 /*ServToClientCMD[CMD.sc_get_all_rights]       = function(_data){
  console.log('Received cmd sc_get_all_rights: ');
  console.log(_data);

  var out      = {};
  out[CMD.sc_get_all_rights] = _data;

  this.UsersInBuffer.AddDataToInBuf(out);
  };*/

 /**
  * @return {boolean}
  */
 function getPenaltyUser(user){
     var res = false;
     var idx = Service.inArrayNoStrict(user, penaltyList);

     if (idx !== -1){
         res = true;

         penaltyList.splice(idx, 1);
     }

     return res;
 }

 function setPenaltyUser(user){
     var idx = Service.inArrayNoStrict(user, penaltyList);

     if (idx == -1){
         penaltyList.push(user);
     }
 }

 exports.ServToClientCMD = ServToClientCMD;
 exports.LoginMethods = LoginMethods;
 exports.firstLogin           = function(){ return firstLogin };
 exports.httpStarted          = function(){
     Service.LoadJSONFile(MCPathes.ProfilNode + 'first.login', function(data){
         firstLogin = !!data;
     }, 'utf8');

     return web_client_connect;
 };
 exports.getPenaltyUser       = getPenaltyUser;
 // exports.getServerHWID        = function () {
 //     return serverHWID;
 // };
