/*eslint no-undef:0*/
/*eslint-env browser*/

"use strict";
var apx = apx || {};

/**************************************************************************
 * apx.js manage data to interact with an apxtri instance from a webpage *
 * component can be add with name-wco
 *************************************************************************
 * This code is not minify and target to be understood by a maximum of 
 * curious people to audit and give any feedback to support@ndda.fr 
 * 
 * To audit it in a browser like chrome:
 *  - open web developpement (F12)
 *  - Menu Sources: apx.js , see below for more information
 *  -             : apxid.js, this is the authentification module that works as a microservice  
 *  
 * 
 * Main principle:
 * - Usage of localStorage to store any usefull data and save it between session
 * - All data and template can be download from an apxtri
 *     get /api/apxtri/wwws/updatelocaldb{ano}/{tribe}/{xapp}/{pagename}/{version} 
 *     ano = empty if authenticated anonymous if not
 *     tribe = the tribe where tha xapp is strore
 *     xapp = the folder name in /tribe/xapp/
 *     pagename = the name of the page without _xx (language)
 *     version = 0 or the version currently store (version is to manage cach)
 * 
 * ------------------
 * ? State management
 * ------------------
 * html page must have apxtri with at least headers key
 * 
    <script>
      const apxtri={
       version:0, // 0 mean it is a first visit if you force to 0 then it will reload all app data
       pagename:"pagename", // without _lg.html ex:pageadmin
       pageauth:"apxid" local page with authentification without _lg,
       headers:{
        xalias:"anonymous",
        xhash:"anonymous",
        xtribe:"tribeId",
        xapp:"app name",
        xlang:"en",
        xdays:0,
        xuuid:"0",
        xtrkversion:1}
       }
       wco:{}
       //specific init data for this webpage 
    } ;
    </script>   
 * ++++++++   
 * ? apx.ready(callback) await DOM is ready equivalent of jquery Document.ready()
 * +++++++++  
 * ? apx.readyafterupdate(callback) allow to add function callback that will be load after apx.ready to add a function to execute  apx.readyafterupdate(functname) without () after functname
 * +++++++++ 
 * ? apx.listendatawco(newpropertie) allow to store data in apx.data.wco and to listen any change to update HTML DOM content <tag data-wco="propertie"></tag> if apx.data.wco.propertie value change then it change every where data-wco=propertie exist innerHTML, textContent, class, ...value
 * to add a new propertie to listen just add it apx.data.wco.propertie={innerHTML:"<p>test</p>"} and run apx.listendatawco(propertie)
 * +++++++++
 * ? apx.notification(selector, data) insert in tag selector an apx return call into data{status:xxx,ref:"Ref",msg:"key",data:{}}, it use apx.data.ref[data.ref].msg as template and render it with data 
 * +++++++++
 * ? apx.save() save apx.data as {xapp} value in localStorage to be able for the same domain to share data
 * +++++++++
 * ? apx.update() :
 * Run at least once after loading a pagename, what it does:   
 * - url/page.html?k1=v1&k2=v2#h1=v4&h2=v5 => store in apx.pagecontext = { search: {k1:v1,k2:v2}, hash: {h1:v4,h2:v5} };
 * - updatelocaldb from api to localstorage name xapp (options / ref / schema / tpl /tpldata) store in {tribe}/wwws/{xapp}.json with key {pagename}
 * - run apx.listendatawco() to allow any apx.data.wco variable to update DOM
 * - run any function that were store in apx.afterupdate.
 * - run apx.lazyload() to load image in background (this is to decrease time loading for search engine)
 * ++++++++++
 * */

apx.ready = (callback) => {
  if (!callback) {
    alert(
      "You have an unknown callback apx.ready(callback), you need to order your code callback = ()=>{} then apx.ready(callback), boring but js rules ;-)"
    );
  }
  if (document.readyState != "loading") callback();
  // modern browsers
  else if (document.addEventListener)
    document.addEventListener("DOMContentLoaded", callback);
  // IE <= 8
  else
    document.attachEvent("onreadystatechange", function () {
      if (document.readyState == "complete") callback();
    });
};

apx.readyafterupdate = (callback) => {
  if (!apx.afterupdate) apx.afterupdate = [];
  apx.afterupdate.push(callback);
};

apx.lazyload = () => {
  document.querySelectorAll("img[data-lazysrc]").forEach((e) => {
    //  in src/page.html:  src contain original img and data-lazysrc='true'
    //  in dist/page.html src is removed img src is in data-lazysrc=newimage.webp or svg
    let src = e.getAttribute("src")
      ? e.getAttribute("src")
      : e.getAttribute("data-lazysrc");
    if (e.getAttribute("data-trksrckey")) {
      src = `/trk/${src}?alias=${apx.data.headers.xalias}&uuid=${
        apx.data.headers.xuuid
      }&srckey=${e.getAttribute("data-trksrckey")}&version=${
        apx.data.headers.xtrkversion
      }&consentcookie=${localStorage.getItem("consentcookie")}&lg=${
        apx.data.headers.xlang
      }`;
    }
    e.setAttribute("src", src);
    console.log("lazyload track:", src);
    e.removeAttribute("data-lazysrc");
  });
  document.querySelectorAll("[data-lazybgsrc]").forEach((e) => {
    e.style.backgroundImage = `url(${e.getAttribute("src")})`;
    e.removeAttribute("data-lazybgsrc");
  });
  document.querySelectorAll("a[data-trksrckey]").forEach((e) => {
    let urldestin = e.getAttribute("href");
    if (
      urldestin.substring(0, 4) != "http" &&
      urldestin.substring(0, 1) != "/"
    ) {
      urldestin = "/" + urldestin;
    }
    const hreftrack = `/trk/redirect?alias=${apx.data.headers.xalias}&uuid=${
      apx.data.headers.xuuid
    }&srckey=${e.getAttribute("data-trksrckey")}&version=${
      apx.data.headers.xtrkversion
    }&consentcookie=${localStorage.getItem("consentcookie")}&lg=${
      apx.data.headers.xlang
    }&url=${urldestin}`;
    console.log("href track:", hreftrack);
    e.setAttribute("href", hreftrack);
    e.removeAttribute("data-trksrckey");
  });
};
apx.notification = (selector, data, clearbefore) => {
  /**
   * @selector a text to use querySelector() in document
   * @data apxtri return from any request {status:200,ref:"",msg:"key", data }
   *   if {status,multimsg:[{ref,msg,data}]} then a multi feedback are in
   * @clearbefore boolean if true then it remove the previous message
   * @return update the dom selctor with the relevant render message in the relevant language
   */
  console.log("notification of ", data);
  const el = document.querySelector(selector);
  if (!el) {
    console.log(
      `WARNING !!! check apx.notification selector:${selector} does not exist in this page`
    );
    return false;
  }
  if (clearbefore) el.innerHTML = "";
  const multimsg = data.multimsg ? data.multimsg : [data];
  multimsg.forEach((info) => {
    if (!apx.data.ref[info.ref]) {
      console.log(
        `check apx.data.ref, ${info.ref} does not exist in this page, add it `
      );
      return false;
    } else if (!apx.data.ref[info.ref][info.msg]) {
      console.log(
        `check apx.data.ref.${info.ref} does not contain ${info.msg} update /schema/lg or /model/lg`
      );
      return false;
    }
    el.innerHTML +=
      " " + Mustache.render(apx.data.ref[info.ref][info.msg], info.data);
    if (data.status == 200) {
      el.classList.remove("text-red");
      el.classList.add("text-green");
    } else {
      el.classList.add("text-red");
      el.classList.remove("text-green");
    }
  });
};
apx.listendatawco = (newpropertie) => {
  // listen any change in apx.wco.newpropertie and update interface with this new value
  //  < data-wco="propertie of apx.wco"> is updated with content text, html any attribute in this new value
  // to init run apx.listendatawco() this is done by apx.update()
  // to dynamicaly add a new propertie to listen just run apx.listendatawco(propertietoadd);
  // Then manage your data by modifying apx.wco and it will be update anywhere it use in the webpage
  //example:
  //  <img data-wco="logodetoto" src="urltoimage" "alt">
  //  apx.wco.logodetoto={src:"newurltoimage",alt:"newalt"} then it will change every where data-wco=logodetoto
  //
  //  <p data-wco="claim">Blabla</p>
  //  apx.wco.claim={html:"newblabla"}
  console.log("From apx.data.wco:", apx.data.wco);
  if (!apx.wco) apx.wco = {};
  console.log(
    "wco dynamic into the webpage",
    apx.data.wco,
    "no propertie to add:",
    !newpropertie
  );
  if (!apx.data.wco || Object.keys(apx.data.wco).length == 0) return false;
  newpropertie = !newpropertie ? Object.keys(apx.data.wco) : [newpropertie];
  console.log("listen apx.data.wco properties:", newpropertie);
  newpropertie.forEach((p) => {
    const actionprop = (val, elt) => {
      if (val.innerHTML) elt.innerHTML = val.innerHTML;
      if (val.textContent) elt.textContent = val.textContent;
      for (const h in ["innerHTML", "textContent"]) {
        if (val[h]) elt[h] = val[h];
      }
      for (const a in ["src", "alt", "placeholder", "class", "href"]) {
        if (val[a]) elt.setAttribute(a, val[a]);
      }
    };
    const elements = document.querySelectorAll(`[data-wco='${p}']`);
    elements.forEach((e) => actionprop(apx.data.wco[p], e));
    //console.log(p, Object.hasOwnProperty(apx.wco));
    if (!apx.data.wco.hasOwnProperty(p)) {
      let _val = apx.data.wco[p]; // Stocke la valeur initiale
      Object.defineProperty(apx.data.wco, p, {
        get: () => _val,
        set: (newv) => {
          _val = newv; // Met à jour la valeur interne
          const elements = document.querySelectorAll(`[data-wco='${p}']`); // Re-sélectionne les éléments au cas où le DOM aurait changé
          elements.forEach((e) => actionprop(newv, e));
        },
      });
      if (_val !== undefined) {
        apx.data.wco[p] = _val;
      }
    }
  });
};
apx.wcoobserver = () => {
  /**
   * wco web component observer if apxtri.wcoobserver==true,
   * Observe existing or creation of any element in DOM with <div wco-name="wconame" id="uniqueid" wco-YYY="aa"></div>
   * if create or if any wco-YYY value change it runs apx[wconame].loadwco(id,ctx) where ctx={YYY:aa}
   * Example:
   * <div id="monComponent" wco-name="monComposant" wco-screen="dashboard" wco-theme="dark" wco-ref="123"></div>
   * if innerHTML this OR if any wco-YYYY change =>
   *      run apx.monComposant.loadwco('monComponent',
   *                                   {'wco-name': 'monComposant',
   *                                    'wco-screen': 'dashboard',
   *                                    'wco-theme': 'dark',
   *                                    'wco-ref': '123'});
   * Used with wco component manage in apx to communicate between autonomous component to reload content if contexte change.
   * Typical example of a wco menu that will load another wco content if not exist and will change a wco-screen to change the content of the wco content
   */
  console.log("listen wcoobserver");
  const processElement = (element) => {
    if (element.nodeType === 1 && element.tagName === "DIV") {
      if (element.id && element.hasAttribute("wco-name")) {
        const ctx = {};
        for (const attr of element.attributes) {
          if (attr.name.startsWith("wco-")) {
            ctx[attr.name.slice(4)] = attr.value;
          }
        }

        const wcoName = element.getAttribute("wco-name");
        if (apx[wcoName] && typeof apx[wcoName].loadwco === "function") {
          apx[wcoName].loadwco(element.id, ctx);
        } else {
          console.log(`ERROR: apx.${wcoName}.loadwco() not found`);
        }
      }
    }
    // Process children recursively
    if (element.children) {
      Array.from(element.children).forEach(processElement);
    }
  };

  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === "childList") {
        mutation.addedNodes.forEach((node) => {
          processElement(node);
        });
      }

      if (
        mutation.type === "attributes" &&
        mutation.attributeName.startsWith("wco-")
      ) {
        const element = mutation.target;
        if (element.id && element.hasAttribute("wco-name")) {
          const ctx = {};
          for (const attr of element.attributes) {
            if (attr.name.startsWith("wco-")) {
              ctx[attr.name.slice(4)] = attr.value;
            }
          }

          const wcoName = element.getAttribute("wco-name");
          if (apx[wcoName] && typeof apx[wcoName].loadwco === "function") {
            apx[wcoName].loadwco(element.id, ctx);
          }
        }
      }
    });
  });

  observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true, // <-- active la détection de changements d'attributs
  });

  // Pour les éléments déjà présents au chargement
  document.addEventListener("DOMContentLoaded", () => {
    document.querySelectorAll("div[wco-name]").forEach((element) => {
      if (element.id) {
        const ctx = {};
        for (const attr of element.attributes) {
          if (attr.name.startsWith("wco-")) {
            ctx[attr.name.slice(4)] = attr.value;
          }
        }
        const wcoName = element.getAttribute("wco-name");
        console.log(`load observer of wco for ${wcoName} in id=${element.id}`);
        if (apx[wcoName] && typeof apx[wcoName].loadwco === "function") {
          apx[wcoName].loadwco(element.id, ctx);
        }
      }
    });
  });
  //load existing wco-name in the html page to initiate the wco process
  // it read and write the wco-name that will trig the observer as a change
  document.querySelectorAll("div[wco-name]").forEach((e) => {
    const wconame = e.getAttribute("wco-name");
    console.log("load wco-name:", wconame);
    e.setAttribute("wco-name", wconame);
  });
};
apx.managecontext = () => {
  // Set authenticate parameter if in pagecontext and redirect to the requested url
  console.log(
    apx.pagecontext.hash.xdays,
    apx.pagecontext.hash.xprofils,
    apx.pagecontext.hash.xtribe,
    dayjs(apx.pagecontext.hash.xdays),
    dayjs(apx.pagecontext.hash.xdays).diff(dayjs(), "hours") < 25,
    apx.pagecontext.hash.xhash
  );
  if (
    apx.pagecontext.hash.xhash &&
    apx.pagecontext.hash.xdays &&
    apx.pagecontext.hash.xprofils &&
    apx.pagecontext.hash.xtribe &&
    dayjs(apx.pagecontext.hash.xdays) &&
    dayjs(apx.pagecontext.hash.xdays).diff(dayjs(), "hours") < 25
  ) {
    //Means this page is called from an external auth app
    let headervalid = true;
    headerkey.forEach((h) => {
      if (apx.pagecontext.hash[h]) {
        apx.data.headers[h] =
          h === "xprofils"
            ? apx.pagecontext.hash[h].split(",")
            : apx.pagecontext.hash[h];
      } else {
        headervalid = false;
      }
    });
    console.log(headervalid, apx.data.headers);
    if (headervalid) {
      apx.save();
      if (apx.pagecontext.hash.url) {
        window.location.href = apx.pagecontext.hash.url;
      }
    } else {
      console.log("Your try to access a page failled with ", apx.pagecontext);
    }
  }
};
// State management
apx.save = () => {
  localStorage.setItem(apx.data.headers.xapp, JSON.stringify(apx.data));
};
apx.update = async () => {
  if (!apxtri || !apxtri.headers || !apxtri.headers.xapp) {
    console.log(
      'Please add to the html page header, this line  const apxtri = { headers: { xtrkversion: 1, xtribe: "smatchit", xapp: "pwa", xlang: "fr", xalias: "anonymous", xhash: "anonymous", xdays: 0} ,pagename:"apxid"} '
    );
    return;
  }
  const headerkey = ["xalias", "xhash", "xdays", "xprofils", "xtribe", "xlang"];
  if (document.querySelector("html").getAttribute("lang")) {
    apxtri.headers.xlang = document.querySelector("html").getAttribute("lang");
  }
  apxtri.headers.xlang = apxtri.headers?.xlang || "en";
  //Get if exist in localstorage a key apxtri.headers.xapp
  if (apxtri.headers.xapp && localStorage.getItem(apxtri.headers.xapp)) {
    apx.data = JSON.parse(localStorage.getItem(apxtri.headers.xapp));
    //update with current pagename and eventualy pageauth
    apx.data.pagename = apxtri.pagename;
    if (apxtri.pageauth) apx.data.pageauth = apxtri.pageauth;
    const isauth = await axios.get('/api/apxtri/pagans/isauth', { headers: apx.data.headers,  withCredentials:true,validateStatus: (status) => true});
    if (
      (isauth.status!==200 && apx.data.headers.alias!=="anonymous") ||
      apx.data.headers.xtribe != apxtri.headers.xtribe ||
      apx.data.headers.xlang != apxtri.headers.xlang ||
      apx.data.headers.xtrkversion != apxtri.headers.xtrkversion
    ) {
      // if no more authenticated or change of tribe...
      localStorage.removeItem(apxtri.headers.xapp);
      delete apx.data;
    }
  }
  //reinit if apx.data does not exist
  apx.data = apx?.data || apxtri;
  // get context from url search ?q=x&s=z or hash #q=x&s=z
  // search comme from a reloading page instead of hash is just a passive link
  apx.pagecontext = { search: {}, hash: {} };
  ["hash", "search"].forEach((type) => {
    const paramString = window.location[type];
    if (paramString) {
      paramString
        .slice(1)
        .split("&")
        .forEach((kv) => {
          const [key, value] = kv.split("=");
          apx.pagecontext.hash[key] = value;
        });
    }
  });
  console.log("apx.pagecontext:", apx.pagecontext);

  // check is authenticated
  if (
    apx.data.headers.xalias === "anonymous" ||
    (apx.pagecontext.hash.xdays &&
      dayjs(apx.pagecontext.hash.xdays).diff(dayjs(), "hours") < 25)
  ) {
    // reset authentification data
    apx.data.headers.xalias = "anonymous";
    apx.data.headers.xdays = 0;
    apx.data.headers.xprofils = ["anonymous"];
    delete apx.data.headers.xhash;
  }
  //apx.managecontext(); (todo when all work target is to use context to redirect if key word used in contexte url, ....)
  if (
    apx.data.headers.xalias === "anonymous" &&
    (apx.data.auth || !apx.data.allowedprofils.includes("anonymous"))
  ) {
    // Means user is not authenticat and has a rememberMe previously selected (auth) OR this page is not accessible to an anonymous.
    // Redirect to apxid page if auth is ok then redirect to this page
    //                           notauth then expected to have alias privatekey
    //document.location.href = `/apxtriadmin/${apx.data.pageauth}_${apx.data.headers.xlang}.html#url=${apx.data.
    document.location.href = `/apxtriadmin/apxid_${apx.data.headers.xlang}.html#url=${apx.data.pagename}_${apx.data.headers.xlang}.html`;
  }

  // check accessright to the page
  if (
    apx.data.allowedprofils &&
    !apx.data.allowedprofils.includes("anonymous") &&
    apx.data.pagename !== apx.data.pageauth
  ) {
    const profilintersect = apx.data.allowedprofils.filter((x) =>
      apx.data.headers.xprofils.includes(x)
    );
    console.log("profils authorized:", profilintersect);
    if (profilintersect.length == 0) {
      alert(apx.data.ref.Middlewares.notallowtoaccess);
      document.location.href = `/apxtriadmin/apxid_${apx.data.headers.xlang}.html`
      return false;
    }
  }
  console.log("authorized to access");
  /* à voir si utile redirect to authentification page pageauth with a redirection if authentify to the pagename (check if /src/ then add it)
  window.location.href = `${apxtri.pageauth}_${
    apxtri.headers.xlang
  }.html?url=${window.location.href.includes("/src/") ? "/src/" : ""}${
    apxtri.pagename
  }_${apxtri.headers.xlang}.html`;
  */
  /// axios setting //
  axios.defaults.withCredentials = true; // force to send cookie http only for authentification
  axios.defaults.headers.common=apx.data.headers;
  ////////////////////

  ////////////////////////////////////////////
  apx.data.version = 0; //this force an update to be removed in production
  ///////////////////////////////////////////
  const ano = apx.data.headers.xalias == "anonymous" ? "anonymous" : "";
  const initdb = `/api/apxtri/wwws/updatelocaldb${ano}/${apx.data.headers.xtribe}/${apx.data.headers.xapp}/${apx.data.pagename}/${apx.data.version}`;
  let initset = {};
  try {
    initset = await axios.get(initdb, {
      timeout: 2000
    });
  } catch (err) {
    console.log(err);
    initset = { data: { msg: "unavailableAPI" } };
  }
  console.log("recupe inidb for ", initdb, initset);
  if (initset.data.msg == "forbidenaccess") {
    alert(apx.data.ref.Middlewares.notallowtoaccess);
    return false;
  }
  if (initset.data.msg == "unavailableAPI") {
    console.log("Your api endpoint is down check your hosted server");
    //try again in 30 seconds
    setTimeout(apx.update, 30000);
  }
  if (initset.data.msg == "data_model_update") {
    Object.keys(initset.data.data).forEach((k) => {
      if (k !== "headers") {
        apx.data[k] = initset.data.data[k];
      }
    });
    console.log("local update done");
    apx.save();
  }

  apx.listendatawco(); // listen any data-wco tag and update it when apxdatawco propertie change
  if (apxtri.wcoobserver) apx.wcoobserver();
  if (apx.afterupdate) apx.afterupdate.forEach((cb) => cb()); //run all function store in apx.afterupdate in order
  apx.lazyload(); //reload image or any media that takes time to load to improve engine search
  apx.save(); //store in local the modification
};
apx.ready(apx.update); //2nd param optional=> true mean does not wait same if apx.lock is set

apx.indexedDB = apx.indexedDB || {};

apx.indexedDB.set = async (db, storeName, value) => {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(db, 1);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;

      if (!db.objectStoreNames.contains("threads")) {
        db.createObjectStore("threads", { keyPath: "uuid" });
      }

      if (!db.objectStoreNames.contains("messages")) {
        db.createObjectStore("messages", { keyPath: "privatriid" });
      }
    };

    request.onsuccess = (event) => {
      const db = event.target.result;

      if (!db.objectStoreNames.contains(storeName)) {
        return resolve();
      }

      const transaction = db.transaction(storeName, "readwrite");
      const store = transaction.objectStore(storeName);

      const putRequest = store.put(value);
      putRequest.onsuccess = () => resolve();
      putRequest.onerror = (error) => reject(error);
    };

    request.onerror = (error) => reject(error);
  });
};

apx.indexedDB.get = async (db, storeName, key) => {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(db, 1);

    request.onsuccess = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains(storeName)) {
        return resolve(null);
      }
      const transaction = db.transaction(storeName, "readonly");
      const store = transaction.objectStore(storeName);

      const getRequest = store.get(key);

      getRequest.onsuccess = () => {
        resolve(getRequest.result || null);
      };

      getRequest.onerror = () => resolve(null);
    };

    request.onerror = (error) => reject(error);
  });
};

apx.indexedDB.del = async (db, storeName, key) => {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(db, 1);

    request.onsuccess = (event) => {
      const db = event.target.result;

      if (!db.objectStoreNames.contains(storeName)) {
        return resolve();
      }

      const transaction = db.transaction(storeName, "readwrite");
      const store = transaction.objectStore(storeName);

      const deleteRequest = store.delete(key);
      deleteRequest.onsuccess = () => resolve();
      deleteRequest.onerror = (error) => reject(error);
    };

    request.onerror = (error) => reject(error);
  });
};
