// node.js ow3 JavaScript worker interface
// Copyright (C) Omnis Software Ltd 2018

/* Changes
Date			Edit			      Fault				Description
06-Aug-24 caa_js_mactree  ST/EC/1898  Secure folder issues on macOS.
25-Apr-24	jmg1372		      ST/AD/287		JS Worker support for ES Modules.
16-Apr-24	unmarked	      						CAA - restructured a few things
15-Apr-23	jmg1259		      ST/JS/3247	JS Worker now handles default error messages in the component, rather than sending with the response.
14-Apr-23 jmg1257         ST/JS/3246  Use encrypted HTTP/2 connection for communications between JS Worker and Node.
06-Apr-23	jmg1254		      ST/JS/3245	Only listen for connections from localhost.
28-Mar-19	rmm10046	      ST/EC/1544	Added better reporting of node.js errors.
11-Mar-19	rmm10018	      ST/JS/2001	vListOrRow parameter of JavaScript worker $callmethod method is now optional.
11-Mar-19	rmm10016	      ST/JS/1996	JavaScript Worker node.js process did not exit if Omnis crashed.
21-Nov-18	rmm_jsw		      						OW3 component - JavaScript Worker.
*/

// Start jmg1372
import http2 from "node:http2";
import fs from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from 'url';
import omnis_calls from "../omnis_calls.js"
import omnis_errors from "../errors.js"
import loadModuleHelper from "./loadModuleHelper.mjs"

// Store omnis_calls and omnis_errors in the global context, so modules can access them without having to import them:
globalThis.omnis_calls = omnis_calls;
globalThis.omnis_errors = omnis_errors;


const __filename = fileURLToPath(import.meta.url);
const thisdir = dirname(__filename);
const jsworkerDir = dirname(thisdir);
globalThis.loadModule = loadModuleHelper([jsworkerDir]); // Global function that searches jsworker dir & all folders on NODE_PATH to load a CommonJS or ES Module.
// End jmg1372

// Start rmm10046: If argv[4] is present, write the stack traceback to the file with path argv[4]
process.on("uncaughtException", (err) => {
  const errorLogPath = process.argv[4];

  if (errorLogPath) {
    try {
      fs.writeFileSync(errorLogPath, err.stack, "utf8");
      console.error(
        `uncaughtException: the stack was written to ${errorLogPath}`
      );
    } catch (writeError) {
      console.error(
        "uncaughtException: failed to write the stack to file:",
        writeError
      );
      console.error("Error:", err.stack);
    }
  } else {
    console.error("Error:", err.stack);
  }

  process.exit(-1);
});

// End rmm10046

const omnis_modules = (await loadModule("omnis_modules")).default; // jmg1372

// Start rmm10016: Interval timer to monitor the Omnis process that started this worker - check twice a second
const omnisPID = Number(process.argv[3]);

if (omnisPID) {
  setInterval(() => {
    try {
      if (!process.kill(omnisPID, 0)) {
        console.log(
          `Omnis process (PID ${omnisPID}) is no longer running. Terminating NodeJS process gracefully.`
        );
        process.kill(process.pid);
      }
    } catch (error) {
      if (error.code === "ESRCH") {
        console.log(
          `Omnis process (PID ${omnisPID}) could not be found. Terminating NodeJS process gracefully.`
        );
        process.kill(process.pid);
      }

      if (error.code !== "EPERM") {
        console.error(
          `Error checking Omnis process (PID ${omnisPID}) status:`,
          error
        );
        process.exit(1);
      }
    }
  }, 500);
} else {
  console.error("No valid PID provided. Exiting.");
  process.exit(1);
}
// End rmm10016

// caa_js_mactree start
let secureFolder = undefined;
if (process.platform === 'darwin')
  secureFolder = process.argv[5];

const server = http2.createSecureServer({
  key: fs.readFileSync(
    process.platform === 'darwin' ?
      join(secureFolder, "localhost-privkey.pem") :
      join(dirname(process.execPath), "secure", "localhost-privkey.pem")
  ),
  cert: fs.readFileSync(
    process.platform === 'darwin' ?
      join(secureFolder, "localhost-cert.pem") :
      join(dirname(process.execPath), "secure", "localhost-cert.pem")
  ),
});
// caa_js_mactree end

server.on("stream", (stream, headers) => {
  // stream is a Duplex (noth readable & writeable)
  let chunks = [];

  stream.on("error", (err) => {
    console.error(err);
  });
  stream.on("data", (chunk) => {
    chunks.push(chunk);
  });

  stream.on("end", async () => {
    if (headers[":method"] != "POST")
      return omnis_calls.sendError(stream, 463, ""); // jmg1259: Only POST messages are supported

    // Split module and method from URL
    let urlOK = false;
    let module, method;

    const match = /^\/call\/([^\/]+)\/([^\/]+)$/.exec(headers[":path"]);
    if (match) {
      [, module, method] = match;
      urlOK = true;
    }

    if (!urlOK) return omnis_calls.sendError(stream, 462, ""); // jmg1259: Improperly formed URL

    // Call the module's method
    let isCallSuccessful = false;
    let errText;
    let errCode;

    try {
      const reqBody = Buffer.concat(chunks).toString();
      isCallSuccessful = await omnis_modules.call(
        module,
        method,
        reqBody.length ? JSON.parse(reqBody) : null,
        stream
      );
    } catch (error) {
      errText = error instanceof Error ? error.message : error.toString();
      errCode = error instanceof Error ? error.errorCode : undefined;
    }

    if (!isCallSuccessful)
      return omnis_calls.sendError(stream, errCode || 400, errText);
  });
});

server.on("listening", () => {
  // Write port number to predefined file, Omnis is waiting to retrieve it
  const serverDetails = { port: server.address().port };
  try {
    fs.writeFileSync(process.argv[2], JSON.stringify(serverDetails), "utf8");
  } catch (e) {
    process.exit(-1);
  }
});

server.on("error", (err) => console.error(err));

server.listen(0, "127.0.0.1"); // jmg1254
