"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 __name = (target, value) => __defProp(target, "name", { value, configurable: true });
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 ESPHomeSocket_exports = {};
__export(ESPHomeSocket_exports, {
  createESPHomeFactory: () => createESPHomeFactory
});
module.exports = __toCommonJS(ESPHomeSocket_exports);
var import_core = require("@zwave-js/core");
var import_shared = require("@zwave-js/shared");
var import_node_net = __toESM(require("node:net"), 1);
var import_ConnectionMessages = require("../esphome/ConnectionMessages.js");
var import_DeviceInfoMessages = require("../esphome/DeviceInfoMessages.js");
var import_HelloMessages = require("../esphome/HelloMessages.js");
var import_ZWaveProxyMessages = require("../esphome/ZWaveProxyMessages.js");
var import_NoiseProtocol = require("../esphome/noise/NoiseProtocol.js");
var import_ESPHomeMessageParser = require("../esphome/parsers/ESPHomeMessageParser.js");
var import_NoiseDecryptTransform = require("../esphome/parsers/NoiseDecryptTransform.js");
var import_NoiseFrameParser = require("../esphome/parsers/NoiseFrameParser.js");
function createESPHomeFactory(options) {
  return async function() {
    const socket = new import_node_net.default.Socket();
    const host = options.host;
    const port = options.port ?? 6053;
    const timeout = 5e3;
    const useEncryption = !!options.encryptionKey;
    let psk;
    if (useEncryption) {
      psk = import_shared.Bytes.from(options.encryptionKey, "base64");
      if (psk.length !== 32) {
        throw new import_core.ZWaveError(`Invalid encryption key length: expected 32 bytes, got ${psk.length}`, import_core.ZWaveErrorCodes.Driver_InvalidOptions);
      }
    }
    let deviceInfo;
    let sourceController;
    let isOpen = false;
    let sendCipher;
    function removeListeners() {
      socket.removeAllListeners("close");
      socket.removeAllListeners("error");
      socket.removeAllListeners("connect");
      socket.removeAllListeners("timeout");
      socket.removeAllListeners("data");
    }
    __name(removeListeners, "removeListeners");
    function sendNoiseFrame(payload) {
      const frame = (0, import_NoiseProtocol.encodeNoiseFrame)(payload);
      return new Promise((resolve, reject) => {
        socket.write(frame, (err) => {
          if (err)
            reject(err);
          else
            resolve();
        });
      });
    }
    __name(sendNoiseFrame, "sendNoiseFrame");
    async function sendMessage(message) {
      if (useEncryption) {
        if (!sendCipher) {
          throw new import_core.ZWaveError("Cannot send encrypted message: handshake not complete", import_core.ZWaveErrorCodes.Driver_NotReady);
        }
        message.serialize();
        const messageData = message.serializeForNoise();
        const encrypted = await sendCipher.encryptWithAd(new import_shared.Bytes(0), messageData);
        await sendNoiseFrame(encrypted);
      } else {
        const frame = message.serialize();
        return new Promise((resolve, reject) => {
          socket.write(frame, (err) => {
            if (err)
              reject(err);
            else
              resolve();
          });
        });
      }
    }
    __name(sendMessage, "sendMessage");
    async function performNoiseHandshake(frameReader) {
      const readNoiseFrame = /* @__PURE__ */ __name((timeoutMs) => readWithTimeout(frameReader, timeoutMs), "readNoiseFrame");
      await sendNoiseFrame(new import_shared.Bytes(0));
      const serverHelloPayload = await readNoiseFrame(timeout);
      const serverHello = (0, import_NoiseProtocol.parseServerHello)(serverHelloPayload);
      if (serverHello.protocolVersion !== 1) {
        throw new import_core.ZWaveError(`Unsupported Noise protocol version: ${serverHello.protocolVersion}`, import_core.ZWaveErrorCodes.Driver_InvalidOptions);
      }
      const handshakeState = new import_NoiseProtocol.NoiseHandshakeState(psk);
      const handshakeMsg1 = await handshakeState.writeMessage(new import_shared.Bytes(0));
      await sendNoiseFrame(import_shared.Bytes.concat([[0], handshakeMsg1]));
      const handshakeMsg2Payload = await readNoiseFrame(timeout);
      if (handshakeMsg2Payload.length === 0) {
        throw new import_core.ZWaveError("Noise handshake failed: empty response", import_core.ZWaveErrorCodes.Driver_SerialPortClosed);
      }
      const statusByte = handshakeMsg2Payload[0];
      if (statusByte === 1) {
        const errorMessage = new TextDecoder().decode(handshakeMsg2Payload.subarray(1));
        throw new import_core.ZWaveError(`Noise handshake failed: ${errorMessage}`, import_core.ZWaveErrorCodes.Driver_SerialPortClosed);
      } else if (statusByte !== 0) {
        throw new import_core.ZWaveError(`Noise handshake failed: unexpected status byte 0x${statusByte.toString(16).padStart(2, "0")}`, import_core.ZWaveErrorCodes.Driver_SerialPortClosed);
      }
      await handshakeState.readMessage(handshakeMsg2Payload.subarray(1));
      const { sendCipher: sc, receiveCipher: rc } = await handshakeState.split();
      sendCipher = sc;
      return rc;
    }
    __name(performNoiseHandshake, "performNoiseHandshake");
    async function performESPHomeHandshake(reader) {
      async function readExpected(expectedType) {
        while (true) {
          const message = await readWithTimeout(reader, timeout);
          if (message instanceof expectedType) {
            return message;
          }
        }
      }
      __name(readExpected, "readExpected");
      const helloRequest = new import_HelloMessages.HelloRequest({
        clientInfo: "zwave-js",
        apiVersionMajor: 1,
        apiVersionMinor: 0
      });
      await sendMessage(helloRequest);
      await readExpected(import_HelloMessages.HelloResponse);
      const deviceInfoRequest = new import_DeviceInfoMessages.DeviceInfoRequest();
      await sendMessage(deviceInfoRequest);
      deviceInfo = await readExpected(import_DeviceInfoMessages.DeviceInfoResponse);
      if (!deviceInfo.hasZWaveProxySupport) {
        throw new import_core.ZWaveError("ESPHome device does not support Z-Wave proxy functionality", import_core.ZWaveErrorCodes.Driver_SerialPortClosed);
      }
      const subscribeRequest = new import_ZWaveProxyMessages.ZWaveProxyRequest({
        type: import_ZWaveProxyMessages.ESPHomeZWaveProxyRequestType.Subscribe
      });
      await sendMessage(subscribeRequest);
    }
    __name(performESPHomeHandshake, "performESPHomeHandshake");
    async function readWithTimeout(reader, timeoutMs) {
      const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new import_core.ZWaveError(`Timeout waiting for message`, import_core.ZWaveErrorCodes.Driver_SerialPortClosed)), timeoutMs);
      });
      const readPromise = reader.read().then(({ value, done }) => {
        if (done || value === void 0) {
          throw new import_core.ZWaveError("Stream closed unexpectedly", import_core.ZWaveErrorCodes.Driver_SerialPortClosed);
        }
        return value;
      });
      return Promise.race([readPromise, timeoutPromise]);
    }
    __name(readWithTimeout, "readWithTimeout");
    function processMessages(reader) {
      void (async () => {
        try {
          while (isOpen) {
            const { value: message, done } = await reader.read();
            if (done)
              break;
            if (message instanceof import_ZWaveProxyMessages.ZWaveProxyFrame) {
              if (sourceController) {
                sourceController.enqueue(message.data);
              }
            }
          }
        } catch {
        }
      })();
    }
    __name(processMessages, "processMessages");
    await new Promise((resolve, reject) => {
      const onClose = /* @__PURE__ */ __name(() => {
        removeListeners();
        socket.destroy();
        reject(new import_core.ZWaveError(`ESPHome connection closed unexpectedly!`, import_core.ZWaveErrorCodes.Driver_SerialPortClosed));
      }, "onClose");
      const onError = /* @__PURE__ */ __name((err) => {
        removeListeners();
        socket.destroy();
        reject(err);
      }, "onError");
      const onTimeout = /* @__PURE__ */ __name(() => {
        removeListeners();
        socket.destroy();
        reject(new import_core.ZWaveError(`Connection timed out after ${timeout}ms`, import_core.ZWaveErrorCodes.Driver_SerialPortClosed));
      }, "onTimeout");
      const onConnect = /* @__PURE__ */ __name(async () => {
        removeListeners();
        socket.setKeepAlive(true, 1e3);
        socket.setNoDelay();
        try {
          const socketReadable = new ReadableStream({
            start(controller) {
              socket.on("data", (data) => {
                controller.enqueue(new import_shared.Bytes(data));
              });
              socket.on("close", () => {
                try {
                  controller.close();
                } catch {
                }
              });
              socket.on("error", (e) => {
                try {
                  controller.error(e);
                } catch {
                }
              });
            }
          });
          let parserReadable;
          let receiveCipher;
          if (useEncryption) {
            const frameParser = new import_NoiseFrameParser.NoiseFrameParser();
            const noiseFramesStream = socketReadable.pipeThrough(frameParser);
            const frameReader = noiseFramesStream.getReader();
            receiveCipher = await performNoiseHandshake(frameReader);
            frameReader.releaseLock();
            const decryptTransform = new import_NoiseDecryptTransform.NoiseDecryptTransform(receiveCipher);
            const messageParser = new import_ESPHomeMessageParser.ESPHomeMessageParser({
              noiseMode: true
            });
            parserReadable = noiseFramesStream.pipeThrough(decryptTransform).pipeThrough(messageParser);
          } else {
            const messageParser = new import_ESPHomeMessageParser.ESPHomeMessageParser();
            parserReadable = socketReadable.pipeThrough(messageParser);
          }
          const reader = parserReadable.getReader();
          await performESPHomeHandshake(reader);
          isOpen = true;
          processMessages(reader);
          resolve();
        } catch (error) {
          socket.destroy();
          reject(error instanceof Error ? error : new Error(String(error)));
        }
      }, "onConnect");
      socket.setTimeout(timeout);
      socket.once("close", onClose);
      socket.once("error", onError);
      socket.once("timeout", onTimeout);
      socket.once("connect", onConnect);
      socket.connect(port, host);
    });
    async function close() {
      try {
        if (isOpen) {
          const disconnectRequest = new import_ConnectionMessages.DisconnectRequest();
          await sendMessage(disconnectRequest);
        }
      } catch {
      }
      return new Promise((resolve) => {
        removeListeners();
        isOpen = false;
        if (socket.destroyed) {
          resolve();
        } else {
          socket.once("close", () => resolve()).destroy();
        }
      });
    }
    __name(close, "close");
    const sink = {
      async write(data, controller) {
        if (!isOpen) {
          controller.error(new Error("ESPHome connection is not ready!"));
          return;
        }
        if (!deviceInfo?.hasZWaveProxySupport) {
          controller.error(new Error("Z-Wave proxy support not available!"));
          return;
        }
        try {
          const writeRequest = new import_ZWaveProxyMessages.ZWaveProxyFrame({
            data: new import_shared.Bytes(data)
          });
          await sendMessage(writeRequest);
        } catch (error) {
          controller.error(error);
        }
      },
      close() {
        return close();
      },
      abort(_reason) {
        return close();
      }
    };
    const source = {
      start(controller) {
        sourceController = controller;
        socket.on("close", () => {
          isOpen = false;
          controller.error(new import_core.ZWaveError(`ESPHome connection closed unexpectedly!`, import_core.ZWaveErrorCodes.Driver_SerialPortClosed));
        });
        socket.on("error", (_e) => {
          isOpen = false;
          controller.error(new import_core.ZWaveError(`ESPHome connection error!`, import_core.ZWaveErrorCodes.Driver_SerialPortClosed));
        });
      },
      cancel() {
        sourceController = void 0;
        socket.removeAllListeners();
      }
    };
    return { source, sink };
  };
}
__name(createESPHomeFactory, "createESPHomeFactory");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  createESPHomeFactory
});
//# sourceMappingURL=ESPHomeSocket.js.map
