const crypto = require("crypto");

const PROTOCOL_VERSION = 1;
const AUDIO_MAGIC = Buffer.from("LACP");
const AUDIO_HEADER_BYTES = 64;
const MAX_JSON_BYTES = 64 * 1024;
const MAX_AUDIO_PAYLOAD_BYTES = 6400;
const CLIENT_TYPES = new Set(["hello", "ping", "source_update", "obs_state", "readiness", "start", "stop", "ack"]);

function parseEnvelope(input) {
  const bytes = Buffer.isBuffer(input) ? input : Buffer.from(String(input || ""), "utf8");
  if (!bytes.length || bytes.length > MAX_JSON_BYTES) throw protocolError("MESSAGE_SIZE", "Structured message size is invalid.");
  let value;
  try { value = JSON.parse(bytes.toString("utf8")); }
  catch { throw protocolError("INVALID_JSON", "Structured message is not valid JSON."); }
  if (!value || Array.isArray(value) || typeof value !== "object") throw protocolError("INVALID_ENVELOPE", "Message envelope must be an object.");
  if (value.version !== PROTOCOL_VERSION) throw protocolError("INCOMPATIBLE_VERSION", `Protocol version ${value.version} is unsupported.`);
  if (!CLIENT_TYPES.has(value.type)) throw protocolError("UNKNOWN_TYPE", "Message type is not allowed.");
  if (!isUuid(value.id)) throw protocolError("INVALID_ID", "Message id must be a UUID.");
  if (!value.sent_at || !Number.isFinite(Date.parse(value.sent_at))) throw protocolError("INVALID_TIME", "Message sent_at must be an ISO timestamp.");
  if (value.session_id != null && !isUuid(value.session_id)) throw protocolError("INVALID_SESSION", "Session id must be a UUID.");
  if (value.payload != null && (Array.isArray(value.payload) || typeof value.payload !== "object")) throw protocolError("INVALID_PAYLOAD", "Message payload must be an object.");
  return value;
}

function validateHello(envelope) {
  if (envelope.type !== "hello") throw protocolError("HELLO_REQUIRED", "The first message must be hello.");
  const payload = envelope.payload || {};
  if (!shortVersion(payload.companion_version) || !shortVersion(payload.plugin_version)) throw protocolError("INVALID_HELLO", "Companion and plugin versions are required.");
  if (!Array.isArray(payload.capabilities) || payload.capabilities.length > 32) throw protocolError("INVALID_CAPABILITIES", "Capabilities are invalid.");
  if (!payload.capabilities.includes("transcription.capture.v1")) throw protocolError("MISSING_CAPABILITY", "The transcription capture capability is required.");
  const audio = payload.audio || {};
  if (audio.codec !== "pcm_s16le" || audio.sample_rate !== 16000 || audio.channels !== 1 || audio.bits !== 16) {
    throw protocolError("UNSUPPORTED_AUDIO", "Protocol v1 requires 16 kHz mono signed 16-bit PCM.");
  }
  if (payload.resume_session_id != null && !isUuid(payload.resume_session_id)) throw protocolError("INVALID_RESUME", "Resume session id is invalid.");
  return payload;
}

function parseAudioFrame(input) {
  const frame = Buffer.from(input || []);
  if (frame.length < AUDIO_HEADER_BYTES || frame.length > AUDIO_HEADER_BYTES + MAX_AUDIO_PAYLOAD_BYTES) {
    throw protocolError("AUDIO_SIZE", "Audio frame size is invalid.");
  }
  if (!frame.subarray(0, 4).equals(AUDIO_MAGIC)) throw protocolError("AUDIO_MAGIC", "Audio frame magic is invalid.");
  const version = frame.readUInt8(4);
  const flags = frame.readUInt8(5);
  const headerBytes = frame.readUInt16LE(6);
  const payloadBytes = frame.readUInt32LE(60);
  if (version !== PROTOCOL_VERSION || headerBytes !== AUDIO_HEADER_BYTES || frame.length !== headerBytes + payloadBytes) {
    throw protocolError("AUDIO_HEADER", "Audio frame header is invalid.");
  }
  if (payloadBytes > MAX_AUDIO_PAYLOAD_BYTES || payloadBytes % 2 !== 0) throw protocolError("AUDIO_PAYLOAD", "PCM payload length is invalid.");
  if (frame.readUInt32LE(52) !== 16000 || frame.readUInt16LE(56) !== 1 || frame.readUInt16LE(58) !== 16) {
    throw protocolError("UNSUPPORTED_AUDIO", "Audio format is unsupported.");
  }
  return {
    version,
    active: Boolean(flags & 1),
    muted: Boolean(flags & 2),
    sequence: frame.readUInt32LE(8),
    capture_timestamp_us: Number(frame.readBigUInt64LE(12)),
    session_id: bytesToUuid(frame.subarray(20, 36)),
    source_uuid: bytesToUuid(frame.subarray(36, 52)),
    sample_rate: 16000,
    channels: 1,
    bits: 16,
    pcm: frame.subarray(AUDIO_HEADER_BYTES)
  };
}

function encodeAudioFrame(value) {
  const pcm = Buffer.from(value.pcm || []);
  if (pcm.length > MAX_AUDIO_PAYLOAD_BYTES || pcm.length % 2) throw protocolError("AUDIO_PAYLOAD", "PCM payload length is invalid.");
  const frame = Buffer.alloc(AUDIO_HEADER_BYTES + pcm.length);
  AUDIO_MAGIC.copy(frame, 0);
  frame.writeUInt8(PROTOCOL_VERSION, 4);
  frame.writeUInt8((value.active === false ? 0 : 1) | (value.muted ? 2 : 0), 5);
  frame.writeUInt16LE(AUDIO_HEADER_BYTES, 6);
  frame.writeUInt32LE(Number(value.sequence) >>> 0, 8);
  frame.writeBigUInt64LE(BigInt(value.capture_timestamp_us || 0), 12);
  uuidToBytes(value.session_id).copy(frame, 20);
  uuidToBytes(value.source_uuid).copy(frame, 36);
  frame.writeUInt32LE(16000, 52);
  frame.writeUInt16LE(1, 56);
  frame.writeUInt16LE(16, 58);
  frame.writeUInt32LE(pcm.length, 60);
  pcm.copy(frame, AUDIO_HEADER_BYTES);
  return frame;
}

function envelope(type, payload = {}, sessionId = null) {
  return { version: 1, type, id: crypto.randomUUID(), sent_at: new Date().toISOString(), session_id: sessionId, payload };
}

function protocolError(code, message) {
  const error = new Error(message);
  error.code = code;
  return error;
}

function isUuid(value) {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(value || ""));
}

function shortVersion(value) { return typeof value === "string" && value.length > 0 && value.length <= 32; }
function uuidToBytes(value) { if (!isUuid(value)) throw protocolError("INVALID_UUID", "UUID is invalid."); return Buffer.from(value.replace(/-/g, ""), "hex"); }
function bytesToUuid(value) { const hex = Buffer.from(value).toString("hex"); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; }

module.exports = { PROTOCOL_VERSION, AUDIO_HEADER_BYTES, MAX_JSON_BYTES, MAX_AUDIO_PAYLOAD_BYTES, parseEnvelope, validateHello, parseAudioFrame, encodeAudioFrame, envelope, protocolError, isUuid };
