import {
  ChildProcess,
  execFile,
  spawn
} from 'child_process';
import { randomUUID } from 'crypto';
import { WebContents } from 'electron';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);
const SCREEN_SHARE_SINK_NAME = 'metoyou_screenshare_sink';
const SCREEN_SHARE_MONITOR_SOURCE_NAME = `${SCREEN_SHARE_SINK_NAME}.monitor`;
const VOICE_SINK_NAME = 'metoyou_voice_sink';
const REROUTE_INTERVAL_MS = 750;
const MONITOR_CAPTURE_SAMPLE_RATE = 48_000;
const MONITOR_CAPTURE_CHANNEL_COUNT = 2;
const MONITOR_CAPTURE_BITS_PER_SAMPLE = 16;
const MONITOR_CAPTURE_STOP_TIMEOUT_MS = 1_000;
const MONITOR_AUDIO_CHUNK_CHANNEL = 'linux-screen-share-monitor-audio-chunk';
const MONITOR_AUDIO_ENDED_CHANNEL = 'linux-screen-share-monitor-audio-ended';

interface ShortSinkEntry {
  index: string;
  name: string;
}

interface ShortSinkInputEntry {
  index: string;
  sinkIndex: string;
}

interface SinkInputDetails extends ShortSinkInputEntry {
  properties: Record<string, string>;
}

interface DescendantProcessInfo {
  ids: ReadonlySet<string>;
  binaryNames: ReadonlySet<string>;
}

interface PactlJsonSinkInputEntry {
  index?: number | string;
  properties?: Record<string, unknown>;
  sink?: number | string;
}

interface LinuxScreenShareAudioRoutingState {
  active: boolean;
  restoreSinkName: string | null;
  screenShareLoopbackModuleId: string | null;
  voiceLoopbackModuleId: string | null;
  rerouteIntervalId: ReturnType<typeof setInterval> | null;
  subscribeProcess: ChildProcess | null;
}

interface LinuxScreenShareMonitorCaptureState {
  captureId: string | null;
  process: ChildProcess | null;
  stderr: string;
  stopRequested: boolean;
  targetWebContents: WebContents | null;
}

export interface LinuxScreenShareAudioRoutingInfo {
  available: boolean;
  active: boolean;
  monitorCaptureSupported: boolean;
  screenShareSinkName: string;
  screenShareMonitorSourceName: string;
  voiceSinkName: string;
  reason?: string;
}

export interface LinuxScreenShareMonitorCaptureInfo {
  bitsPerSample: number;
  captureId: string;
  channelCount: number;
  sampleRate: number;
  sourceName: string;
}

const routingState: LinuxScreenShareAudioRoutingState = {
  active: false,
  restoreSinkName: null,
  screenShareLoopbackModuleId: null,
  voiceLoopbackModuleId: null,
  rerouteIntervalId: null,
  subscribeProcess: null
};
const monitorCaptureState: LinuxScreenShareMonitorCaptureState = {
  captureId: null,
  process: null,
  stderr: '',
  stopRequested: false,
  targetWebContents: null
};

let pactlAvailableCache: boolean | null = null;

export async function prepareLinuxScreenShareAudioRouting(): Promise<LinuxScreenShareAudioRoutingInfo> {
  if (process.platform !== 'linux') {
    return buildRoutingInfo(false, false, 'Linux-only audio routing is unavailable on this platform.');
  }

  if (!await isPactlAvailable()) {
    return buildRoutingInfo(false, false, 'pactl is unavailable; falling back to standard desktop audio capture.');
  }

  await ensureNullSink(SCREEN_SHARE_SINK_NAME);
  await ensureNullSink(VOICE_SINK_NAME);

  return buildRoutingInfo(true, routingState.active);
}

export async function activateLinuxScreenShareAudioRouting(): Promise<LinuxScreenShareAudioRoutingInfo> {
  const prepared = await prepareLinuxScreenShareAudioRouting();

  if (!prepared.available) {
    return prepared;
  }

  if (routingState.active) {
    return buildRoutingInfo(true, true);
  }

  const restoreSinkName = await getPreferredRestoreSinkName();

  if (!restoreSinkName) {
    return buildRoutingInfo(false, false, 'Unable to determine a playback sink for Linux screen-share audio routing.');
  }

  try {
    routingState.restoreSinkName = restoreSinkName;
    routingState.screenShareLoopbackModuleId = await loadLoopbackModule(SCREEN_SHARE_MONITOR_SOURCE_NAME, restoreSinkName);
    routingState.voiceLoopbackModuleId = await loadLoopbackModule(`${VOICE_SINK_NAME}.monitor`, restoreSinkName);

    // Set the default sink to the voice sink so that new app audio
    // streams (received WebRTC voice) never land on the screenshare
    // capture sink.  This prevents the feedback loop where remote
    // voice audio was picked up by parec before the reroute interval
    // could move the stream away.
    await setDefaultSink(VOICE_SINK_NAME);

    routingState.active = true;

    // Let the combined reroute decide placement for every existing
    // stream. This avoids briefly shoving the app's own playback to the
    // screenshare sink before ownership detection can move it back.
    await rerouteSinkInputs();
    startSinkInputRerouteLoop();
    startSubscribeWatcher();

    return buildRoutingInfo(true, true);
  } catch (error) {
    await deactivateLinuxScreenShareAudioRouting();

    return buildRoutingInfo(
      false,
      false,
      error instanceof Error ? error.message : 'Failed to activate Linux screen-share audio routing.'
    );
  }
}

export async function deactivateLinuxScreenShareAudioRouting(): Promise<boolean> {
  const restoreSinkName = routingState.restoreSinkName;

  stopSubscribeWatcher();
  stopSinkInputRerouteLoop();
  await stopLinuxScreenShareMonitorCapture();

  try {
    if (restoreSinkName) {
      await setDefaultSink(restoreSinkName);
      await moveSinkInputs(restoreSinkName, (sinkName) => sinkName === SCREEN_SHARE_SINK_NAME || sinkName === VOICE_SINK_NAME);
    }
  } catch {
    // Best-effort cleanup only.
  }

  await Promise.all([unloadModuleIfLoaded(routingState.screenShareLoopbackModuleId), unloadModuleIfLoaded(routingState.voiceLoopbackModuleId)]);

  routingState.active = false;
  routingState.restoreSinkName = null;
  routingState.screenShareLoopbackModuleId = null;
  routingState.voiceLoopbackModuleId = null;
  routingState.subscribeProcess = null;

  return true;
}

export async function cleanupLinuxScreenShareAudioRouting(): Promise<void> {
  await deactivateLinuxScreenShareAudioRouting();
}

export async function startLinuxScreenShareMonitorCapture(
  targetWebContents: WebContents
): Promise<LinuxScreenShareMonitorCaptureInfo> {
  if (process.platform !== 'linux') {
    throw new Error('Linux screen-share monitor capture is unavailable on this platform.');
  }

  if (!routingState.active) {
    throw new Error('Linux screen-share audio routing must be active before monitor capture starts.');
  }

  await stopLinuxScreenShareMonitorCapture();

  const captureId = randomUUID();
  const captureProcess = spawn('parec', [
    '--device',
    SCREEN_SHARE_MONITOR_SOURCE_NAME,
    '--raw',
    '--format=s16le',
    '--rate',
    `${MONITOR_CAPTURE_SAMPLE_RATE}`,
    '--channels',
    `${MONITOR_CAPTURE_CHANNEL_COUNT}`
  ], {
    env: process.env,
    stdio: [
      'ignore',
      'pipe',
      'pipe'
    ]
  });

  monitorCaptureState.captureId = captureId;
  monitorCaptureState.process = captureProcess;
  monitorCaptureState.stderr = '';
  monitorCaptureState.stopRequested = false;
  monitorCaptureState.targetWebContents = targetWebContents;

  let started = false;

  const startPromise = new Promise<void>((resolve, reject) => {
    const onError = (error: Error): void => {
      if (!started) {
        cleanupMonitorCaptureState(captureId, error.message);
        reject(error);
        return;
      }

      cleanupMonitorCaptureState(captureId, error.message);
    };

    captureProcess.on('error', onError);
    captureProcess.once('spawn', () => {
      started = true;
      resolve();
    });
  });

  captureProcess.stdout.on('data', (chunk: Buffer) => {
    if (monitorCaptureState.captureId !== captureId) {
      return;
    }

    const target = monitorCaptureState.targetWebContents;

    if (!target || target.isDestroyed()) {
      return;
    }

    target.send(MONITOR_AUDIO_CHUNK_CHANNEL, {
      captureId,
      chunk: Uint8Array.from(chunk)
    });
  });

  captureProcess.stderr.on('data', (chunk: Buffer) => {
    if (monitorCaptureState.captureId !== captureId) {
      return;
    }

    const nextStderr = `${monitorCaptureState.stderr}${chunk.toString()}`;

    monitorCaptureState.stderr = nextStderr.slice(-4_096);
  });

  captureProcess.once('close', (code, signal) => {
    const reason = buildMonitorCaptureCloseReason(captureId, code, signal);

    cleanupMonitorCaptureState(captureId, reason);
  });

  await startPromise;

  return {
    bitsPerSample: MONITOR_CAPTURE_BITS_PER_SAMPLE,
    captureId,
    channelCount: MONITOR_CAPTURE_CHANNEL_COUNT,
    sampleRate: MONITOR_CAPTURE_SAMPLE_RATE,
    sourceName: SCREEN_SHARE_MONITOR_SOURCE_NAME
  };
}

export async function stopLinuxScreenShareMonitorCapture(captureId?: string): Promise<boolean> {
  if (!monitorCaptureState.captureId || !monitorCaptureState.process) {
    return true;
  }

  if (captureId && captureId !== monitorCaptureState.captureId) {
    return false;
  }

  const currentCaptureId = monitorCaptureState.captureId;
  const captureProcess = monitorCaptureState.process;

  monitorCaptureState.stopRequested = true;

  await new Promise<void>((resolve) => {
    const forceKillTimeout = setTimeout(() => {
      if (!captureProcess.killed) {
        captureProcess.kill('SIGKILL');
      }
    }, MONITOR_CAPTURE_STOP_TIMEOUT_MS);

    captureProcess.once('close', () => {
      clearTimeout(forceKillTimeout);
      resolve();
    });

    if (!captureProcess.killed) {
      captureProcess.kill('SIGTERM');
      return;
    }

    clearTimeout(forceKillTimeout);
    resolve();
  });

  return monitorCaptureState.captureId !== currentCaptureId;
}

function buildRoutingInfo(
  available: boolean,
  active: boolean,
  reason?: string
): LinuxScreenShareAudioRoutingInfo {
  return {
    available,
    active,
    monitorCaptureSupported: true,
    screenShareSinkName: SCREEN_SHARE_SINK_NAME,
    screenShareMonitorSourceName: SCREEN_SHARE_MONITOR_SOURCE_NAME,
    voiceSinkName: VOICE_SINK_NAME,
    ...(reason ? { reason } : {})
  };
}

async function isPactlAvailable(): Promise<boolean> {
  if (pactlAvailableCache !== null) {
    return pactlAvailableCache;
  }

  try {
    await runPactl('info');
    pactlAvailableCache = true;
  } catch {
    pactlAvailableCache = false;
  }

  return pactlAvailableCache;
}

async function runPactl(...args: string[]): Promise<string> {
  const { stdout } = await execFileAsync('pactl', args, {
    env: process.env
  });

  return stdout.trim();
}

async function ensureNullSink(sinkName: string): Promise<void> {
  const sinks = await listSinks();

  if (sinks.some((sink) => sink.name === sinkName)) {
    return;
  }

  await runPactl(
    'load-module',
    'module-null-sink',
    `sink_name=${sinkName}`,
    `sink_properties=device.description=${sinkName}`
  );
}

async function loadLoopbackModule(sourceName: string, sinkName: string): Promise<string> {
  const moduleId = await runPactl(
    'load-module',
    'module-loopback',
    `source=${sourceName}`,
    `sink=${sinkName}`,
    'latency_msec=10',
    'source_dont_move=true',
    'sink_dont_move=true'
  );

  return moduleId.split(/\s+/)[0] || moduleId;
}

async function unloadModuleIfLoaded(moduleId: string | null): Promise<void> {
  if (!moduleId) {
    return;
  }

  try {
    await runPactl('unload-module', moduleId);
  } catch {
    // Module may have already been unloaded externally.
  }
}

async function getPreferredRestoreSinkName(): Promise<string | null> {
  const defaultSinkName = await getDefaultSinkName();

  if (defaultSinkName && defaultSinkName !== SCREEN_SHARE_SINK_NAME && defaultSinkName !== VOICE_SINK_NAME) {
    return defaultSinkName;
  }

  const sinks = await listSinks();

  return sinks.find((sink) => sink.name !== SCREEN_SHARE_SINK_NAME && sink.name !== VOICE_SINK_NAME)?.name ?? null;
}

async function getDefaultSinkName(): Promise<string | null> {
  const info = await runPactl('info');
  const defaultSinkLine = info
    .split(/\r?\n/)
    .find((line) => line.startsWith('Default Sink:'));

  if (!defaultSinkLine) {
    return null;
  }

  const sinkName = defaultSinkLine.replace('Default Sink:', '').trim();

  return sinkName || null;
}

async function setDefaultSink(sinkName: string): Promise<void> {
  await runPactl('set-default-sink', sinkName);
}

/**
 * Combined reroute that enforces sink placement in both directions:
 * - App-owned sink inputs that are NOT on the voice sink are moved there.
 * - Non-app sink inputs that ARE on the voice sink are moved to the
 *   screenshare sink so they are captured by parec.
 *
 * This two-way approach, combined with the voice sink being the PulseAudio
 * default, ensures that received WebRTC voice audio can never leak into the
 * screenshare monitor source.
 */
async function rerouteSinkInputs(): Promise<void> {
  const [
    sinks,
    sinkInputs,
    descendantProcessInfo
  ] = await Promise.all([
    listSinks(),
    listSinkInputDetails(),
    collectDescendantProcessInfo(process.pid)
  ]);
  const sinkNamesByIndex = new Map(sinks.map((sink) => [sink.index, sink.name]));

  await Promise.all(
    sinkInputs.map(async (sinkInput) => {
      const sinkName = sinkNamesByIndex.get(sinkInput.sinkIndex) ?? null;
      const appOwned = isAppOwnedSinkInput(sinkInput, descendantProcessInfo);

      // App-owned streams must stay on the voice sink.
      if (appOwned && sinkName !== VOICE_SINK_NAME) {
        try {
          await runPactl('move-sink-input', sinkInput.index, VOICE_SINK_NAME);
        } catch {
          // Streams can disappear or be recreated while rerouting.
        }

        return;
      }

      // Non-app streams sitting on the voice sink should be moved to the
      // screenshare sink for desktop-audio capture.
      if (!appOwned && sinkName === VOICE_SINK_NAME) {
        try {
          await runPactl('move-sink-input', sinkInput.index, SCREEN_SHARE_SINK_NAME);
        } catch {
          // Streams can disappear or be recreated while rerouting.
        }
      }
    })
  );
}

function cleanupMonitorCaptureState(captureId: string, reason?: string): void {
  if (monitorCaptureState.captureId !== captureId) {
    return;
  }

  const target = monitorCaptureState.targetWebContents;

  monitorCaptureState.captureId = null;
  monitorCaptureState.process = null;
  monitorCaptureState.stderr = '';
  monitorCaptureState.stopRequested = false;
  monitorCaptureState.targetWebContents = null;

  if (!target || target.isDestroyed()) {
    return;
  }

  target.send(MONITOR_AUDIO_ENDED_CHANNEL, {
    captureId,
    ...(reason ? { reason } : {})
  });
}

function buildMonitorCaptureCloseReason(
  captureId: string,
  code: number | null,
  signal: NodeJS.Signals | null
): string | undefined {
  if (monitorCaptureState.captureId !== captureId) {
    return undefined;
  }

  if (monitorCaptureState.stopRequested) {
    return undefined;
  }

  if (monitorCaptureState.stderr.trim()) {
    return monitorCaptureState.stderr.trim();
  }

  if (signal) {
    return `Linux screen-share monitor capture stopped with signal ${signal}.`;
  }

  if (typeof code === 'number' && code !== 0) {
    return `Linux screen-share monitor capture exited with code ${code}.`;
  }

  return undefined;
}

function startSinkInputRerouteLoop(): void {
  if (routingState.rerouteIntervalId) {
    return;
  }

  routingState.rerouteIntervalId = setInterval(() => {
    void rerouteSinkInputs();
  }, REROUTE_INTERVAL_MS);
}

function stopSinkInputRerouteLoop(): void {
  if (!routingState.rerouteIntervalId) {
    return;
  }

  clearInterval(routingState.rerouteIntervalId);
  routingState.rerouteIntervalId = null;
}

/**
 * Spawns `pactl subscribe` to receive PulseAudio events in real time.
 * When a new or changed sink-input is detected, a reroute is triggered
 * immediately instead of waiting for the next interval tick.  This
 * drastically reduces the time non-app desktop audio spends on the
 * voice sink before being moved to the screenshare sink.
 */
function startSubscribeWatcher(): void {
  if (routingState.subscribeProcess) {
    return;
  }

  let proc: ChildProcess;

  try {
    proc = spawn('pactl', ['subscribe'], {
      env: process.env,
      stdio: [
        'ignore',
        'pipe',
        'ignore'
      ]
    });
  } catch {
    // If pactl subscribe fails to spawn, the interval loop still covers us.
    return;
  }

  routingState.subscribeProcess = proc;

  let pending = false;

  proc.stdout?.on('data', (chunk: Buffer) => {
    if (!routingState.active) {
      return;
    }

    const text = chunk.toString();

    if (/Event '(?:new|change)' on sink-input/.test(text)) {
      if (!pending) {
        pending = true;

        // Batch rapid-fire events with a short delay.
        setTimeout(() => {
          pending = false;
          void rerouteSinkInputs();
        }, 50);
      }
    }
  });

  proc.on('close', () => {
    if (routingState.subscribeProcess === proc) {
      routingState.subscribeProcess = null;
    }
  });

  proc.on('error', () => {
    if (routingState.subscribeProcess === proc) {
      routingState.subscribeProcess = null;
    }
  });
}

function stopSubscribeWatcher(): void {
  const proc = routingState.subscribeProcess;

  if (!proc) {
    return;
  }

  routingState.subscribeProcess = null;

  if (!proc.killed) {
    proc.kill('SIGTERM');
  }
}

function isAppOwnedSinkInput(
  sinkInput: SinkInputDetails,
  descendantProcessInfo: DescendantProcessInfo
): boolean {
  const processId = sinkInput.properties['application.process.id'];

  if (typeof processId === 'string' && descendantProcessInfo.ids.has(processId)) {
    return true;
  }

  const processBinary = normalizeProcessBinary(sinkInput.properties['application.process.binary']);

  if (processBinary && descendantProcessInfo.binaryNames.has(processBinary)) {
    return true;
  }

  const applicationName = normalizeProcessBinary(sinkInput.properties['application.name']);

  if (applicationName && descendantProcessInfo.binaryNames.has(applicationName)) {
    return true;
  }

  return false;
}

async function moveSinkInputs(
  targetSinkName: string,
  shouldMove: (sinkName: string | null) => boolean
): Promise<void> {
  const [sinks, sinkInputs] = await Promise.all([listSinks(), listSinkInputs()]);
  const sinkNamesByIndex = new Map(sinks.map((sink) => [sink.index, sink.name]));

  await Promise.all(
    sinkInputs.map(async (sinkInput) => {
      const sinkName = sinkNamesByIndex.get(sinkInput.sinkIndex) ?? null;

      if (!shouldMove(sinkName)) {
        return;
      }

      try {
        await runPactl('move-sink-input', sinkInput.index, targetSinkName);
      } catch {
        // Streams can disappear while iterating.
      }
    })
  );
}

async function listSinks(): Promise<ShortSinkEntry[]> {
  const output = await runPactl('list', 'short', 'sinks');

  return output
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => line.split(/\s+/))
    .filter((columns) => columns.length >= 2)
    .map((columns) => ({
      index: columns[0],
      name: columns[1]
    }));
}

async function listSinkInputs(): Promise<ShortSinkInputEntry[]> {
  const output = await runPactl('list', 'short', 'sink-inputs');

  return output
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => line.split(/\s+/))
    .filter((columns) => columns.length >= 2)
    .map((columns) => ({
      index: columns[0],
      sinkIndex: columns[1]
    }));
}

async function listSinkInputDetails(): Promise<SinkInputDetails[]> {
  try {
    const output = await runPactl('--format=json', 'list', 'sink-inputs');
    const entries = JSON.parse(output) as PactlJsonSinkInputEntry[];

    if (!Array.isArray(entries)) {
      return [];
    }

    return entries
      .map((entry) => {
        const index = typeof entry.index === 'number' || typeof entry.index === 'string'
          ? `${entry.index}`
          : '';
        const sinkIndex = typeof entry.sink === 'number' || typeof entry.sink === 'string'
          ? `${entry.sink}`
          : '';
        const properties = Object.fromEntries(
          Object.entries(entry.properties || {}).map(([key, value]) => [key, typeof value === 'string' ? value : `${value ?? ''}`])
        );

        return {
          index,
          sinkIndex,
          properties
        } satisfies SinkInputDetails;
      })
      .filter((entry) => !!entry.index && !!entry.sinkIndex);
  } catch {
    // Fall back to the legacy text format parser below.
  }

  const output = await runPactl('list', 'sink-inputs');
  const entries: SinkInputDetails[] = [];

  let currentEntry: SinkInputDetails | null = null;
  let parsingProperties = false;

  const pushCurrentEntry = (): void => {
    if (currentEntry) {
      entries.push(currentEntry);
    }
  };

  for (const rawLine of output.split(/\r?\n/)) {
    const sinkInputMatch = rawLine.match(/^Sink Input #(\d+)/);

    if (sinkInputMatch) {
      pushCurrentEntry();
      currentEntry = {
        index: sinkInputMatch[1],
        sinkIndex: '',
        properties: {}
      };

      parsingProperties = false;
      continue;
    }

    if (!currentEntry) {
      continue;
    }

    const sinkMatch = rawLine.match(/^\s*Sink:\s*(\d+)/);

    if (sinkMatch) {
      currentEntry.sinkIndex = sinkMatch[1];
      continue;
    }

    if (/^\s*Properties:\s*$/.test(rawLine)) {
      parsingProperties = true;
      continue;
    }

    if (!parsingProperties) {
      continue;
    }

    if (rawLine.trim().length === 0) {
      parsingProperties = false;
      continue;
    }

    const propertyLine = rawLine.trim();
    const separatorIndex = propertyLine.indexOf(' = ');

    if (separatorIndex === -1) {
      if (/^\S/.test(rawLine) || /^\s+\S[^=]*:\s*$/.test(rawLine)) {
        parsingProperties = false;
      }

      continue;
    }

    const key = propertyLine.slice(0, separatorIndex).trim();
    const rawValue = propertyLine.slice(separatorIndex + 3).trim();

    currentEntry.properties[key] = stripSurroundingQuotes(rawValue);
  }

  pushCurrentEntry();

  return entries.filter((entry) => !!entry.sinkIndex);
}

async function collectDescendantProcessInfo(rootProcessId: number): Promise<DescendantProcessInfo> {
  const { stdout } = await execFileAsync('ps', ['-eo', 'pid=,ppid=,comm='], {
    env: process.env
  });
  const childrenByParentId = new Map<string, string[]>();
  const binaryNameByProcessId = new Map<string, string>();

  stdout
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean)
    .forEach((line) => {
      const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);

      if (!match) {
        return;
      }

      const [
        ,
        pid,
        ppid,
        command
      ] = match;
      const siblings = childrenByParentId.get(ppid) ?? [];

      siblings.push(pid);
      childrenByParentId.set(ppid, siblings);

      const normalizedBinaryName = normalizeProcessBinary(command);

      if (normalizedBinaryName) {
        binaryNameByProcessId.set(pid, normalizedBinaryName);
      }
    });

  const rootId = `${rootProcessId}`;
  const descendantIds = new Set<string>([rootId]);
  const descendantBinaryNames = new Set<string>();
  const queue = [rootId];

  while (queue.length > 0) {
    const currentId = queue.shift();

    if (!currentId) {
      continue;
    }

    const binaryName = binaryNameByProcessId.get(currentId);

    if (binaryName) {
      descendantBinaryNames.add(binaryName);
    }

    for (const childId of childrenByParentId.get(currentId) ?? []) {
      if (descendantIds.has(childId)) {
        continue;
      }

      descendantIds.add(childId);
      queue.push(childId);
    }
  }

  return {
    ids: descendantIds,
    binaryNames: descendantBinaryNames
  };
}

function normalizeProcessBinary(value: string | undefined): string | null {
  if (!value) {
    return null;
  }

  const trimmed = value.trim();

  if (!trimmed) {
    return null;
  }

  const basename = trimmed
    .split(/[\\/]/)
    .pop()
    ?.trim()
    .toLowerCase() ?? '';

  return basename || null;
}

function stripSurroundingQuotes(value: string): string {
  if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
    return value.slice(1, -1);
  }

  return value;
}
