/**
 * cli/terminal/socket-client.mjs — JSON-line client for AperantIpcServer.
 *
 * Pure node:net + node:readline. No external deps. Used by the apt-tools
 * `terminal` subcommand to talk to the Electron main process's
 * AperantIpcServer over the per-user, per-pid Unix-domain socket.
 *
 * Each request gets a UUID id and is correlated to the response on the
 * same id. The default timeout is 10 s — on timeout the client destroys
 * the underlying socket and rejects the request with `{ code: 'timeout' }`.
 *
 * Usage:
 *   const client = await connectSocket('/tmp/aperant-andre-12345.sock')
 *   const res = await client.request('health', {})
 *   client.close()
 */

import { randomUUID } from 'node:crypto'
import { createConnection } from 'node:net'
import { createInterface } from 'node:readline'

/**
 * Open a connection to the given Unix-domain socket and return a thin
 * request/response wrapper. Resolves once the underlying socket has
 * emitted `connect`. Rejects on connect failure (ENOENT, ECONNREFUSED).
 *
 * @param {string} socketPath
 * @returns {Promise<{ request: (cmd: string, payload?: any, timeoutMs?: number) => Promise<any>, close: () => void }>}
 */
export function connectSocket(socketPath) {
	return new Promise((resolve, reject) => {
		if (typeof socketPath !== 'string' || socketPath.length === 0) {
			reject(new Error('connectSocket: socketPath must be a non-empty string'))
			return
		}

		const sock = createConnection(socketPath)
		/** @type {Map<string, { resolve: (v: any) => void, reject: (e: any) => void, timer: NodeJS.Timeout }>} */
		const pending = new Map()
		let connected = false
		let closed = false

		const onConnect = () => {
			connected = true
			sock.off('error', onConnectError)
			const rl = createInterface({ input: sock, crlfDelay: Infinity })
			rl.on('line', (line) => {
				const trimmed = line.trim()
				if (!trimmed) return
				let msg
				try {
					msg = JSON.parse(trimmed)
				} catch {
					return // ignore garbage
				}
				const id = msg && typeof msg.id === 'string' ? msg.id : null
				if (!id) return
				const slot = pending.get(id)
				if (!slot) return
				clearTimeout(slot.timer)
				pending.delete(id)
				slot.resolve(msg)
			})
			resolve({
				request(cmd, payload = {}, timeoutMs = 10000) {
					if (closed) return Promise.reject(new Error('socket closed'))
					const id = randomUUID()
					const envelope = JSON.stringify({ id, cmd, payload })
					return new Promise((rs, rj) => {
						const timer = setTimeout(() => {
							pending.delete(id)
							try {
								sock.destroy()
							} catch {
								/* ignore */
							}
							rj({ code: 'timeout', message: `request '${cmd}' timed out after ${timeoutMs}ms` })
						}, timeoutMs)
						pending.set(id, { resolve: rs, reject: rj, timer })
						try {
							sock.write(`${envelope}\n`)
						} catch (err) {
							clearTimeout(timer)
							pending.delete(id)
							rj({
								code: 'write_failed',
								message: err instanceof Error ? err.message : String(err),
							})
						}
					})
				},
				close() {
					closed = true
					for (const [, slot] of pending) {
						clearTimeout(slot.timer)
						slot.reject({ code: 'closed', message: 'socket closed by caller' })
					}
					pending.clear()
					try {
						sock.destroy()
					} catch {
						/* ignore */
					}
				},
			})
		}

		const onConnectError = (err) => {
			if (connected) return
			reject(err)
		}

		sock.once('connect', onConnect)
		sock.once('error', onConnectError)
		// If the server side disconnects after we are connected, fail every
		// pending request with a `disconnected` code.
		sock.once('close', () => {
			if (!connected) return
			for (const [, slot] of pending) {
				clearTimeout(slot.timer)
				slot.reject({ code: 'disconnected', message: 'socket closed by server' })
			}
			pending.clear()
			closed = true
		})
	})
}
