"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var dev_command_exports = {};
__export(dev_command_exports, {
  DevCommand: () => DevCommand
});
module.exports = __toCommonJS(dev_command_exports);
var import_axios = __toESM(require("axios"));
var import_chalk = __toESM(require("chalk"));
var import_lodash = require("lodash");
var pathlib = __toESM(require("path"));
var uuid = __toESM(require("uuid"));
var apiUtils = __toESM(require("../api"));
var errors = __toESM(require("../errors"));
var tables = __toESM(require("../tables"));
var utils = __toESM(require("../utils"));
var import_worker = require("../worker");
var import_build_command = require("./build-command");
var import_project_command = require("./project-command");
const DEFAULT_BOT_PORT = 8075;
const DEFAULT_INTEGRATION_PORT = 8076;
const TUNNEL_HELLO_INTERVAL = 5e3;
const FILEWATCHER_DEBOUNCE_MS = 500;
class DevCommand extends import_project_command.ProjectCommand {
  _initialDef = void 0;
  _cacheDevRequestBody;
  _buildContext;
  constructor(...args) {
    super(...args);
    this._buildContext = new utils.esbuild.BuildCodeContext();
  }
  async run() {
    this.logger.warn("This command is experimental and subject to breaking changes without notice.");
    const api = await this.ensureLoginAndCreateClient(this.argv);
    const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS();
    if (projectType === "interface") {
      throw new errors.BotpressCLIError("This feature is not available for interfaces.");
    }
    const projectDef = await resolveProjectDefinition();
    this._initialDef = projectDef;
    let env = {
      ...process.env,
      BP_API_URL: api.url,
      BP_TOKEN: api.token
    };
    let defaultPort = DEFAULT_BOT_PORT;
    if (this._initialDef.type === "integration") {
      defaultPort = DEFAULT_INTEGRATION_PORT;
      const secretEnvVariables = await this.promptSecrets(this._initialDef.definition, this.argv, { formatEnv: true });
      const nonNullSecretEnvVariables = utils.records.filterValues(secretEnvVariables, utils.guards.is.notNull);
      env = { ...env, ...nonNullSecretEnvVariables };
    }
    const port = this.argv.port ?? defaultPort;
    const urlParseResult = utils.url.parse(this.argv.tunnelUrl);
    if (urlParseResult.status === "error") {
      throw new errors.BotpressCLIError(`Invalid tunnel URL: ${urlParseResult.error}`);
    }
    const cachedTunnelId = await this.projectCache.get("tunnelId");
    let tunnelId;
    if (this.argv.tunnelId) {
      tunnelId = this.argv.tunnelId;
    } else if (cachedTunnelId) {
      tunnelId = cachedTunnelId;
    } else {
      tunnelId = uuid.v4();
    }
    if (cachedTunnelId !== tunnelId) {
      await this.projectCache.set("tunnelId", tunnelId);
    }
    const { url: parsedTunnelUrl } = urlParseResult;
    const isSecured = parsedTunnelUrl.protocol === "https" || parsedTunnelUrl.protocol === "wss";
    const wsTunnelUrl = utils.url.format({ ...parsedTunnelUrl, protocol: isSecured ? "wss" : "ws" });
    const httpTunnelUrl = utils.url.format({
      ...parsedTunnelUrl,
      protocol: isSecured ? "https" : "http",
      path: `/${tunnelId}`
    });
    let worker = void 0;
    const supervisor = new utils.tunnel.TunnelSupervisor(wsTunnelUrl, tunnelId, this.logger);
    supervisor.events.on("connected", ({ tunnel }) => {
      const timer = setInterval(() => {
        if (tunnel.closed) {
          return handleClose();
        }
        tunnel.hello();
      }, TUNNEL_HELLO_INTERVAL);
      const handleClose = () => clearInterval(timer);
      tunnel.events.on("close", handleClose);
      tunnel.events.on("request", (req) => {
        if (!worker) {
          this.logger.debug("Worker not ready yet, ignoring request");
          tunnel.send({ requestId: req.id, status: 503, body: "Worker not ready yet" });
          return;
        }
        void this._forwardTunnelRequest(`http://localhost:${port}`, req).then((res) => {
          tunnel.send(res);
        }).catch((thrown) => {
          const err = errors.BotpressCLIError.wrap(thrown, "An error occurred while handling request");
          this.logger.error(err.message);
          tunnel.send({
            requestId: req.id,
            status: 500,
            body: err.message
          });
        });
      });
    });
    supervisor.events.on("manuallyClosed", () => {
      this.logger.debug("Tunnel manually closed");
    });
    await supervisor.start();
    await this._runBuild();
    await this._deploy(api, httpTunnelUrl);
    worker = await this._spawnWorker(env, port);
    try {
      const watcher = await utils.filewatcher.FileWatcher.watch(
        this.argv.workDir,
        async (events) => {
          if (!worker) {
            this.logger.debug("Worker not ready yet, ignoring file change event");
            return;
          }
          const typescriptEvents = events.filter((e) => !e.path.startsWith(this.projectPaths.abs.outDir)).filter((e) => pathlib.extname(e.path) === ".ts");
          const distEvents = events.filter((e) => e.path.startsWith(this.projectPaths.abs.distDir));
          if (typescriptEvents.length > 0) {
            this.logger.log("Changes detected, rebuilding");
            await this._restart(api, worker, httpTunnelUrl);
          } else if (distEvents.length > 0) {
            this.logger.log("Changes detected in output directory, reloading worker");
            await worker.reload();
          }
        },
        {
          debounceMs: FILEWATCHER_DEBOUNCE_MS
        }
      );
      await Promise.race([worker.wait(), watcher.wait(), supervisor.wait()]);
      if (worker.running) {
        await worker.kill();
      }
      await watcher.close();
      supervisor.close();
    } catch (thrown) {
      throw errors.BotpressCLIError.wrap(thrown, "An error occurred while running the dev server");
    } finally {
      if (worker.running) {
        await worker.kill();
      }
    }
  }
  _restart = async (api, worker, tunnelUrl) => {
    try {
      await this._runBuild();
    } catch (thrown) {
      const error = errors.BotpressCLIError.wrap(thrown, "Build failed");
      this.logger.error(error.message);
      return;
    }
    await this._deploy(api, tunnelUrl);
    await worker.reload();
  };
  _deploy = async (api, tunnelUrl) => {
    const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS();
    if (projectType === "interface") {
      throw new errors.BotpressCLIError("This feature is not available for interfaces.");
    }
    if (projectType === "integration") {
      const projectDef = await resolveProjectDefinition();
      this._checkSecrets(projectDef.definition);
      return await this._deployDevIntegration(api, tunnelUrl, projectDef.definition);
    }
    if (projectType === "bot") {
      const projectDef = await resolveProjectDefinition();
      return await this._deployDevBot(api, tunnelUrl, projectDef.definition);
    }
    throw new errors.UnsupportedProjectType();
  };
  _checkSecrets(integrationDef) {
    if (this._initialDef?.type !== "integration") {
      return;
    }
    const initialSecrets = this._initialDef?.definition.secrets ?? {};
    const currentSecrets = integrationDef.secrets ?? {};
    const newSecrets = Object.keys(currentSecrets).filter((s) => !initialSecrets[s]);
    if (newSecrets.length > 0) {
      throw new errors.BotpressCLIError("Secrets were added while the server was running. A restart is required.");
    }
  }
  _spawnWorker = async (env, port) => {
    const outfile = this.projectPaths.abs.outFileCJS;
    const importPath = utils.path.toUnix(outfile);
    const code = `require('${importPath}').default.start(${port})`;
    const worker = await import_worker.Worker.spawn(
      {
        type: "code",
        code,
        env
      },
      this.logger
    ).catch((thrown) => {
      throw errors.BotpressCLIError.wrap(thrown, "Could not start dev worker");
    });
    return worker;
  };
  _runBuild() {
    return new import_build_command.BuildCommand(this.api, this.prompt, this.logger, this.argv).setProjectContext(this.projectContext).run(this._buildContext);
  }
  async _deployDevIntegration(api, externalUrl, integrationDef) {
    const devId = await this.projectCache.get("devId");
    let integration = void 0;
    if (devId) {
      const resp = await api.client.getIntegration({ id: devId }).catch(async (thrown) => {
        const err = errors.BotpressCLIError.wrap(thrown, `Could not find existing dev integration with id "${devId}"`);
        this.logger.warn(err.message);
        return { integration: void 0 };
      });
      if (resp.integration?.dev) {
        integration = resp.integration;
      } else {
        await this.projectCache.rm("devId");
      }
    }
    const line = this.logger.line();
    line.started(`Deploying dev integration ${import_chalk.default.bold(integrationDef.name)}...`);
    const createIntegrationBody = {
      ...await this.prepareCreateIntegrationBody(integrationDef),
      ...await this.prepareIntegrationDependencies(integrationDef, api),
      url: externalUrl
    };
    if (integration) {
      const updateIntegrationBody = apiUtils.prepareUpdateIntegrationBody(
        { ...createIntegrationBody, id: integration.id },
        integration
      );
      const resp = await api.client.updateIntegration(updateIntegrationBody).catch((thrown) => {
        throw errors.BotpressCLIError.wrap(thrown, `Could not update dev integration "${integrationDef.name}"`);
      });
      integration = resp.integration;
    } else {
      const resp = await api.client.createIntegration({ ...createIntegrationBody, dev: true }).catch((thrown) => {
        throw errors.BotpressCLIError.wrap(thrown, `Could not deploy dev integration "${integrationDef.name}"`);
      });
      integration = resp.integration;
    }
    line.success(`Dev Integration deployed with id "${integration.id}" at "${externalUrl}"`);
    line.commit();
    await this.projectCache.set("devId", integration.id);
  }
  async _deployDevBot(api, externalUrl, botDef) {
    const devId = await this.projectCache.get("devId");
    let bot = void 0;
    if (devId) {
      const resp = await api.client.getBot({ id: devId }).catch(async (thrown) => {
        const err = errors.BotpressCLIError.wrap(thrown, `Could not find existing dev bot with id "${devId}"`);
        this.logger.warn(err.message);
        return { bot: void 0 };
      });
      if (resp.bot?.dev) {
        bot = resp.bot;
      } else {
        await this.projectCache.rm("devId");
      }
    }
    if (!bot) {
      const createLine = this.logger.line();
      createLine.started("Creating dev bot...");
      const resp = await api.client.createBot({
        dev: true,
        url: externalUrl
      }).catch((thrown) => {
        throw errors.BotpressCLIError.wrap(thrown, "Could not deploy dev bot");
      });
      bot = resp.bot;
      createLine.log("Dev Bot created");
      createLine.commit();
      await this.projectCache.set("devId", bot.id);
    }
    const updateBotBody = apiUtils.prepareUpdateBotBody(
      {
        ...await apiUtils.prepareCreateBotBody(botDef),
        ...await this.prepareBotDependencies(botDef, api),
        id: bot.id,
        url: externalUrl
      },
      bot
    );
    if (!await this._didDefinitionChange(updateBotBody)) {
      this.logger.log("Skipping deployment step. No changes found in bot.definition.ts");
      return;
    }
    const updateLine = this.logger.line();
    updateLine.started("Deploying dev bot...");
    const { bot: updatedBot } = await api.client.updateBot(updateBotBody).catch((thrown) => {
      throw errors.BotpressCLIError.wrap(thrown, "Could not deploy dev bot");
    });
    this.validateIntegrationRegistration(updatedBot, (failedIntegrations) => {
      throw new errors.BotpressCLIError(
        `Some integrations failed to register:
${Object.entries(failedIntegrations).map(([key, int]) => `\u2022 ${key}: ${int.statusReason}`).join("\n")}`
      );
    });
    updateLine.success(`Dev Bot deployed with id "${updatedBot.id}" at "${externalUrl}"`);
    updateLine.commit();
    const tablesPublisher = new tables.TablesPublisher({ api, logger: this.logger, prompt: this.prompt });
    await tablesPublisher.deployTables({ botId: updatedBot.id, botDefinition: botDef });
    this.displayWebhookUrls(updatedBot);
  }
  async _didDefinitionChange(body) {
    const didChange = !(0, import_lodash.isEqual)(body, this._cacheDevRequestBody);
    this._cacheDevRequestBody = { ...body };
    return didChange;
  }
  _forwardTunnelRequest = async (baseUrl, request) => {
    const axiosConfig = {
      method: request.method,
      url: this._formatLocalUrl(baseUrl, request),
      headers: request.headers,
      data: request.body,
      responseType: "text",
      validateStatus: () => true
    };
    this.logger.debug(`Forwarding request to ${axiosConfig.url}`);
    const response = await (0, import_axios.default)(axiosConfig);
    this.logger.debug("Sending back response up the tunnel");
    return {
      requestId: request.id,
      status: response.status,
      headers: this._getHeaders(response.headers),
      body: response.data
    };
  };
  _formatLocalUrl = (baseUrl, req) => {
    if (req.query) {
      return `${baseUrl}${req.path}?${req.query}`;
    }
    return `${baseUrl}${req.path}`;
  };
  _getHeaders = (res) => {
    const headers = {};
    for (const key in res) {
      if (typeof res[key] === "string" || typeof res[key] === "number") {
        headers[key] = String(res[key]);
      }
    }
    return headers;
  };
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  DevCommand
});
//# sourceMappingURL=dev-command.js.map
