const { EventEmitter } = require("events");
const WebSocket = require("ws");

const DEFAULT_LOCAL_LLM_BASE_URL = "http://127.0.0.1:1234/v1";
const DEFAULT_LOCAL_STT_URL = "ws://127.0.0.1:8765/v1/asr/stream";
const DEFAULT_LOCAL_STT_LANGUAGE = "en-US";

function normalizeOpenAiBaseUrl(baseUrl) {
  const trimmed = (baseUrl || DEFAULT_LOCAL_LLM_BASE_URL)
    .trim()
    .replace(/\/+$/, "");
  if (!trimmed) return DEFAULT_LOCAL_LLM_BASE_URL;
  return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
}

function isLoopbackUrl(value) {
  try {
    const url = new URL(value);
    const hostname = url.hostname.toLowerCase();
    return (
      hostname === "localhost" ||
      hostname === "127.0.0.1" ||
      hostname === "::1" ||
      hostname === "[::1]"
    );
  } catch (_) {
    return false;
  }
}

function buildChatMessages({
  systemPrompt,
  history = [],
  userText,
  imageBase64,
}) {
  const messages = [];

  if (systemPrompt && systemPrompt.trim()) {
    messages.push({ role: "system", content: systemPrompt.trim() });
  }

  for (const message of history) {
    if (!message || !message.role || !message.content) continue;
    if (
      message.role !== "user" &&
      message.role !== "assistant" &&
      message.role !== "system"
    )
      continue;
    messages.push({
      role: message.role,
      content: String(message.content),
    });
  }

  const text = (userText || "").trim();
  if (imageBase64) {
    messages.push({
      role: "user",
      content: [
        { type: "text", text },
        {
          type: "image_url",
          image_url: { url: `data:image/jpeg;base64,${imageBase64}` },
        },
      ],
    });
  } else if (text) {
    messages.push({ role: "user", content: text });
  }

  return messages;
}

function parseChatCompletionSseLine(line) {
  if (!line.startsWith("data: ")) return null;

  const data = line.slice(6).trim();
  if (!data || data === "[DONE]") return null;

  const parsed = JSON.parse(data);
  return parsed.choices?.[0]?.delta?.content || "";
}

async function streamLmStudioChat({
  baseUrl,
  apiKey,
  model,
  messages,
  temperature = 0.7,
  maxTokens = 2048,
  onToken,
}) {
  if (!model || !model.trim()) {
    throw new Error("LM Studio model id is required");
  }

  const normalizedBaseUrl = normalizeOpenAiBaseUrl(baseUrl);
  const response = await fetch(`${normalizedBaseUrl}/chat/completions`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...(apiKey && apiKey.trim()
        ? { Authorization: `Bearer ${apiKey.trim()}` }
        : {}),
    },
    body: JSON.stringify({
      model: model.trim(),
      messages,
      stream: true,
      temperature,
      max_tokens: maxTokens,
    }),
  });

  if (!response.ok) {
    const errorText = await response.text().catch(() => "");
    if (response.status === 400 && /image|vision|multimodal/i.test(errorText)) {
      throw new Error(
        "The selected local model does not appear to support image input",
      );
    }
    throw new Error(
      `LM Studio error ${response.status}: ${errorText || response.statusText}`,
    );
  }

  if (!response.body) {
    throw new Error("LM Studio response did not include a stream body");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let fullText = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split(/\r?\n/);
    buffer = lines.pop() || "";

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed) continue;

      let token = "";
      try {
        token = parseChatCompletionSseLine(trimmed) || "";
      } catch (_) {
        continue;
      }

      if (token) {
        fullText += token;
        if (onToken) onToken(token, fullText);
      }
    }
  }

  return fullText;
}

class NemotronSidecarClient extends EventEmitter {
  constructor({
    url = DEFAULT_LOCAL_STT_URL,
    language = DEFAULT_LOCAL_STT_LANGUAGE,
    sampleRate = 16000,
    channels = 1,
    encoding = "pcm_s16le",
  } = {}) {
    super();
    this.url = url;
    this.language = language;
    this.sampleRate = sampleRate;
    this.channels = channels;
    this.encoding = encoding;
    this.socket = null;
    this.connected = false;
  }

  connect() {
    if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
      return Promise.resolve();
    }

    return new Promise((resolve, reject) => {
      const socket = new WebSocket(this.url);
      this.socket = socket;

      const cleanup = () => {
        socket.removeListener("open", onOpen);
        socket.removeListener("error", onErrorBeforeOpen);
      };

      const onOpen = () => {
        cleanup();
        this.connected = true;
        this.emit("connected");
        this._sendStart();
        resolve();
      };

      const onErrorBeforeOpen = (error) => {
        cleanup();
        this.connected = false;
        this.emit("error", error);
        reject(error);
      };

      socket.once("open", onOpen);
      socket.once("error", onErrorBeforeOpen);

      socket.on("message", (data) => this._handleMessage(data));
      socket.on("close", (code, reason) => {
        this.connected = false;
        this.emit("close", { code, reason: reason?.toString?.() || "" });
      });
      socket.on("error", (error) => {
        this.connected = false;
        this.emit("error", error);
      });
    });
  }

  _sendStart() {
    if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return;
    this.socket.send(
      JSON.stringify({
        type: "start",
        sampleRate: this.sampleRate,
        channels: this.channels,
        encoding: this.encoding,
        language: this.language,
      }),
    );
  }

  _handleMessage(data) {
    let message;
    try {
      message = JSON.parse(data.toString("utf8"));
    } catch (error) {
      this.emit(
        "error",
        new Error(`Invalid ASR sidecar message: ${error.message}`),
      );
      return;
    }

    if (!message || !message.type) return;

    if (message.type === "partial") {
      this.emit("partial", message.text || "");
    } else if (message.type === "final") {
      this.emit("final", message.text || "");
    } else if (message.type === "ready") {
      this.emit("ready", message);
    } else if (message.type === "error") {
      this.emit(
        "error",
        new Error(message.error || message.message || "ASR sidecar error"),
      );
    } else {
      this.emit(message.type, message);
    }
  }

  sendAudio(pcm16kBuffer) {
    if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return false;
    if (!pcm16kBuffer || pcm16kBuffer.length === 0) return false;
    this.socket.send(pcm16kBuffer, { binary: true });
    return true;
  }

  close() {
    if (!this.socket) return;
    try {
      if (this.socket.readyState === WebSocket.OPEN) {
        this.socket.send(JSON.stringify({ type: "stop" }));
      }
      this.socket.close();
    } catch (_) {
      // Best-effort close.
    }
    this.connected = false;
    this.socket = null;
  }
}

module.exports = {
  DEFAULT_LOCAL_LLM_BASE_URL,
  DEFAULT_LOCAL_STT_LANGUAGE,
  DEFAULT_LOCAL_STT_URL,
  NemotronSidecarClient,
  buildChatMessages,
  isLoopbackUrl,
  normalizeOpenAiBaseUrl,
  parseChatCompletionSseLine,
  streamLmStudioChat,
};
