import { Tree } from "@weborigami/async-tree";
import http from "node:http";
import { createServer } from "node:net";
import process from "node:process";
import assertTreeIsDefined from "../common/assertTreeIsDefined.js";
import { isTransformApplied, transformObject } from "../common/utilities.js";
import { requestListener } from "../server/server.js";
import debug from "./debug.js";
import ExplorableSiteTransform from "./ExplorableSiteTransform.js";
import watch from "./watch.js";

const defaultPort = 5000;

/**
 * Start a local web server for the indicated tree.
 *
 * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
 * @typedef {import("@weborigami/async-tree").Treelike} Treelike
 *
 * @param {Treelike} treelike
 * @param {number} [port]
 * @this {AsyncTree|null}
 */
export default async function serve(treelike, port) {
  assertTreeIsDefined(this, "dev:serve");
  let tree;
  if (treelike) {
    tree = Tree.from(treelike, { parent: this });
    if (!isTransformApplied(ExplorableSiteTransform, tree)) {
      tree = transformObject(ExplorableSiteTransform, tree);
    }
  } else {
    // By default, watch the default tree and add default pages.
    const withDefaults = await debug.call(this);
    tree = await watch.call(this, withDefaults);
  }

  if (port === undefined) {
    if (process.env.PORT) {
      // Use the port specified in the environment.
      port = parseInt(process.env.PORT);
    } else {
      // Find an open port.
      port = await findOpenPort(defaultPort);
    }
  }

  // @ts-ignore
  http.createServer(requestListener(tree)).listen(port, undefined, () => {
    console.log(
      `Server running at http://localhost:${port}. Press Ctrl+C to stop.`
    );
  });
}

// Return the first open port number on or after the given port number.
// From https://gist.github.com/mikeal/1840641?permalink_comment_id=2896667#gistcomment-2896667
function findOpenPort(port) {
  const server = createServer();
  return new Promise((resolve, reject) =>
    server
      .on("error", (/** @type {any} */ error) =>
        error.code === "EADDRINUSE" ? server.listen(++port) : reject(error)
      )
      .on("listening", () => server.close(() => resolve(port)))
      .listen(port)
  );
}
