// Local same-user transport. No public listener and no second HTTP authentication scheme.
import { createHash } from "node:crypto";
import { mkdirSync, chmodSync, unlinkSync } from "node:fs";
import { createConnection, createServer } from "node:net";
import { homedir } from "node:os";
import { join } from "node:path";

function socketPath(sessionId: string): string {
  const directory = process.env.DEVICE_LAB_CONTROL_DIR ?? join(homedir(), ".config", "esp32-device-gateway", "runtime");
  mkdirSync(directory, { recursive: true, mode: 0o700 });
  chmodSync(directory, 0o700);
  return join(directory, createHash("sha256").update(sessionId).digest("hex").slice(0, 24) + ".sock");
}

export async function controlRequest(sessionId: string, request: object): Promise<any> {
  return new Promise((resolve, reject) => {
    const socket = createConnection(socketPath(sessionId));
    let buffer = "";
    socket.setTimeout(45000, () => socket.destroy(new Error("Pi control timeout; input outcome unknown")));
    socket.on("error", reject);
    socket.on("connect", () => socket.write(JSON.stringify(request) + "\n"));
    socket.on("data", data => {
      buffer += data.toString();
      if (buffer.length > 65536) socket.destroy(new Error("oversized Pi response"));
      if (buffer.includes("\n")) {
        socket.destroy();
        try { resolve(JSON.parse(buffer.split("\n", 1)[0])); } catch (error) { reject(error); }
      }
    });
    socket.on("end", () => { if (!buffer.includes("\n")) reject(new Error("Pi control closed without acknowledgment")); });
  });
}

export async function probeControl(sessionId: string): Promise<any | null> {
  try { return await controlRequest(sessionId, { method: "ping" }); }
  catch (error: any) {
    if (error.code === "ENOENT" || error.code === "ECONNREFUSED") return null;
    throw error; // A timeout is not proof that a runtime is absent.
  }
}

export async function openControl(sessionId: string, handler: (request: any) => Promise<any>) {
  const path = socketPath(sessionId);
  // Linux abstract socket acts as an atomic, kernel-released ownership lock.
  // It carries no commands/data. Unlike a PID/lock file, a crash cannot leave it
  // stale, and two adapters cannot unlink each other's live filesystem socket.
  const lease = createServer(socket => socket.destroy());
  const leasePath = "\0device-lab-pi-" + createHash("sha256").update(path).digest("hex");
  await new Promise<void>((resolve, reject) => {
    lease.once("error", reject);
    lease.listen(leasePath, () => { lease.removeListener("error", reject); resolve(); });
  });
  lease.on("error", () => {});
  try {
    if (await probeControl(sessionId)) throw new Error("Pi session already has a control owner");
    try { unlinkSync(path); } catch (error: any) { if (error.code !== "ENOENT") throw error; }
  } catch (error) { lease.close(); throw error; }
  const sockets = new Set<ReturnType<typeof createConnection>>();
  const server = createServer(socket => {
    sockets.add(socket);
    socket.on("close", () => sockets.delete(socket));
    socket.on("error", () => {});
    socket.setTimeout(45000, () => socket.destroy());
    let buffer = "";
    let handled = false;
    socket.on("data", async data => {
      if (handled) return;
      buffer += data.toString();
      if (buffer.length > 131072) { socket.destroy(); return; }
      if (!buffer.includes("\n")) return;
      handled = true;
      try {
        const request = JSON.parse(buffer.split("\n", 1)[0]);
        const result = await handler(request);
        socket.end(JSON.stringify(result) + "\n");
      } catch (error: any) { socket.end(JSON.stringify({ error: "Pi control rejected", status: error.status || 502 }) + "\n"); }
    });
  });
  try {
    await new Promise<void>((resolve, reject) => {
      server.once("error", reject);
      server.listen(path, () => { server.removeListener("error", reject); resolve(); });
    });
  } catch (error) { lease.close(); throw error; }
  server.on("error", () => {});
  chmodSync(path, 0o600);
  return async () => {
    for (const socket of sockets) socket.destroy();
    await new Promise<void>(resolve => server.close(() => resolve()));
    await new Promise<void>(resolve => lease.close(() => resolve()));
  };
}
