pragma Singleton

import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Services.System

Singleton {
  id: root

  // Rate limiting for volume feedback (minimum 100ms between sounds)
  property var lastVolumeFeedbackTime: 0
  readonly property int minVolumeFeedbackInterval: 100

  // Track the last sink that produced volume feedback so we can suppress the
  // initial onVolumeChanged that fires on startup and device switches.
  property PwNode _lastFeedbackSink: null

  // Devices
  readonly property var sink: Pipewire.ready ? Pipewire.defaultAudioSink : null
  readonly property var source: validatedSource
  readonly property bool hasInput: !!source
  readonly property list<PwNode> sinks: deviceNodes.sinks
  readonly property list<PwNode> sources: deviceNodes.sources
  readonly property real maxVolume: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
  readonly property real epsilon: 0.005

  // Fallback state sourced from wpctl when PipeWire node values go stale.
  property bool wpctlAvailable: false
  property bool wpctlStateValid: false
  property real wpctlOutputVolume: 0
  property bool wpctlOutputMuted: true
  property bool wpctlInputStateValid: false
  property real wpctlInputVolume: 0
  property bool wpctlInputMuted: true

  signal volumeAtMaximum
  signal volumeAtMinimum

  function clampOutputVolume(vol: real): real {
    if (vol === undefined || isNaN(vol)) {
      return 0;
    }
    return Math.max(0, Math.min(root.maxVolume, vol));
  }

  function refreshWpctlOutputState(): void {
    if (!wpctlAvailable || wpctlStateProcess.running) {
      return;
    }
    wpctlStateProcess.command = ["wpctl", "get-volume", "@DEFAULT_AUDIO_SINK@"];
    wpctlStateProcess.running = true;
  }

  function refreshWpctlInputState(): void {
    if (!wpctlAvailable || wpctlInputStateProcess.running) {
      return;
    }
    wpctlInputStateProcess.command = ["wpctl", "get-volume", "@DEFAULT_AUDIO_SOURCE@"];
    wpctlInputStateProcess.running = true;
  }

  function applyWpctlOutputState(raw: string): bool {
    const text = String(raw || "").trim();
    const match = text.match(/Volume:\s*([0-9]*\.?[0-9]+)/i);
    if (!match || match.length < 2) {
      return false;
    }

    const parsedVolume = Number(match[1]);
    if (isNaN(parsedVolume)) {
      return false;
    }

    wpctlOutputVolume = clampOutputVolume(parsedVolume);
    wpctlOutputMuted = /\[MUTED\]/i.test(text);
    wpctlStateValid = true;
    return true;
  }

  function applyWpctlInputState(raw: string): bool {
    const text = String(raw || "").trim();
    const match = text.match(/Volume:\s*([0-9]*\.?[0-9]+)/i);
    if (!match || match.length < 2) {
      return false;
    }

    const parsedVolume = Number(match[1]);
    if (isNaN(parsedVolume)) {
      return false;
    }

    wpctlInputVolume = Math.max(0, Math.min(root.maxVolume, parsedVolume));
    wpctlInputMuted = /\[MUTED\]/i.test(text);
    wpctlInputStateValid = true;
    return true;
  }

  // Output volume (prefer wpctl state when available)
  readonly property real volume: {
    if (wpctlAvailable && wpctlStateValid) {
      return clampOutputVolume(wpctlOutputVolume);
    }

    if (!sink?.audio) {
      return 0;
    }
    return clampOutputVolume(sink.audio.volume);
  }
  readonly property bool muted: {
    if (wpctlAvailable && wpctlStateValid) {
      return wpctlOutputMuted;
    }
    return sink?.audio?.muted ?? true;
  }

  // Input volume (prefer wpctl state when available — matches set-volume % round-trip)
  readonly property real inputVolume: {
    if (wpctlAvailable && wpctlInputStateValid) {
      return Math.max(0, Math.min(root.maxVolume, wpctlInputVolume));
    }
    if (!source?.audio) {
      return 0;
    }
    const vol = source.audio.volume;
    if (vol === undefined || isNaN(vol)) {
      return 0;
    }
    return Math.max(0, Math.min(root.maxVolume, vol));
  }
  readonly property bool inputMuted: {
    if (wpctlAvailable && wpctlInputStateValid) {
      return wpctlInputMuted;
    }
    return source?.audio?.muted ?? true;
  }

  // Allow callers to skip the next OSD notification when they are already
  // presenting volume state (e.g. the Audio Panel UI). We track this as a short
  // time window so suppression applies to every monitor, not just the first one
  // that receives the signal.
  property double outputOSDSuppressedUntilMs: 0
  property double inputOSDSuppressedUntilMs: 0

  function suppressOutputOSD(durationMs = 400) {
    const target = Date.now() + durationMs;
    outputOSDSuppressedUntilMs = Math.max(outputOSDSuppressedUntilMs, target);
  }

  function suppressInputOSD(durationMs = 400) {
    const target = Date.now() + durationMs;
    inputOSDSuppressedUntilMs = Math.max(inputOSDSuppressedUntilMs, target);
  }

  function consumeOutputOSDSuppression(): bool {
    return Date.now() < outputOSDSuppressedUntilMs;
  }

  function consumeInputOSDSuppression(): bool {
    return Date.now() < inputOSDSuppressedUntilMs;
  }

  readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0

  // Filtered device nodes (non-stream sinks and sources)
  readonly property var deviceNodes: Pipewire.ready ? Pipewire.nodes.values.reduce((acc, node) => {
                                                                                     if (!node.isStream) {
                                                                                       // Filter out quickshell nodes (unlikely to be devices, but for consistency)
                                                                                       const name = node.name || "";
                                                                                       const mediaName = (node.properties && node.properties["media.name"]) || "";
                                                                                       if (name === "quickshell" || mediaName === "quickshell") {
                                                                                         return acc;
                                                                                       }

                                                                                       if (node.isSink) {
                                                                                         acc.sinks.push(node);
                                                                                       } else if (node.audio) {
                                                                                         acc.sources.push(node);
                                                                                       }
                                                                                     }
                                                                                     return acc;
                                                                                   }, {
                                                                                     "sources": [],
                                                                                     "sinks": []
                                                                                   }) : {
                                                        "sources": [],
                                                        "sinks": []
                                                      }

  // Validated source (ensures it's a proper audio source, not a sink)
  readonly property var validatedSource: {
    if (!Pipewire.ready) {
      return null;
    }
    const raw = Pipewire.defaultAudioSource;
    if (!raw || raw.isSink || !raw.audio) {
      return null;
    }
    // Optional: check type if available (type reflects media.class per docs)
    if (raw.type && typeof raw.type === "string" && !raw.type.startsWith("Audio/Source")) {
      return null;
    }
    return raw;
  }

  // Internal state for feedback loop prevention
  property bool isSettingOutputVolume: false
  property bool isSettingInputVolume: false

  // Bind default sink and source to ensure their properties are available
  PwObjectTracker {
    id: sinkTracker
    objects: root.sink ? [root.sink] : []
  }

  PwObjectTracker {
    id: sourceTracker
    objects: root.source ? [root.source] : []
  }

  // Track links to the default sink to find active streams
  PwNodeLinkTracker {
    id: sinkLinkTracker
  }

  onSinkChanged: {
    if (root.sink) {
      sinkLinkTracker.node = root.sink;
    }
  }

  // Track all streams globally to prevent binding loops for filtered out streams
  readonly property var streamNodes: Pipewire.ready ? Pipewire.nodes.values.filter(n => n && n.isStream) : []

  // Find application streams that are connected to the default sink
  readonly property var appStreams: {
    if (!Pipewire.ready || !root.sink) {
      return [];
    }

    var connectedStreamIds = {};
    var connectedStreams = [];

    // Use PwNodeLinkTracker to get properly bound link groups
    if (!sinkLinkTracker.linkGroups) {
      return [];
    }

    var linkGroupsCount = 0;
    if (sinkLinkTracker.linkGroups.length !== undefined) {
      linkGroupsCount = sinkLinkTracker.linkGroups.length;
    } else if (sinkLinkTracker.linkGroups.count !== undefined) {
      linkGroupsCount = sinkLinkTracker.linkGroups.count;
    } else {
      return [];
    }

    if (linkGroupsCount === 0) {
      return [];
    }

    var intermediateNodeIds = {};
    var nodesToCheck = [];

    for (var i = 0; i < linkGroupsCount; i++) {
      var linkGroup;
      if (sinkLinkTracker.linkGroups.get) {
        linkGroup = sinkLinkTracker.linkGroups.get(i);
      } else {
        linkGroup = sinkLinkTracker.linkGroups[i];
      }

      if (!linkGroup || !linkGroup.source) {
        continue;
      }

      var sourceNode = linkGroup.source;

      // Filter out quickshell
      const name = sourceNode.name || "";
      const mediaName = (sourceNode.properties && sourceNode.properties["media.name"]) || "";
      if (name === "quickshell" || mediaName === "quickshell") {
        continue;
      }

      // Filter out filter (intermediate) streams
      const isVirtual = (sourceNode.properties && sourceNode.properties["node.virtual"]) || "";
      // If it's an application stream node, add it directly
      if (sourceNode.isStream && sourceNode.audio && !isVirtual) {
        if (!connectedStreamIds[sourceNode.id]) {
          connectedStreamIds[sourceNode.id] = true;
          connectedStreams.push(sourceNode);
        }
      } else {
        // Not a stream - this is an intermediate node, track it
        intermediateNodeIds[sourceNode.id] = true;
        nodesToCheck.push(sourceNode);
      }
    }

    // If we found intermediate nodes, we need to find streams connected to them
    if (nodesToCheck.length > 0 || connectedStreams.length === 0) {
      try {
        var allNodes = Pipewire.nodes.values || [];

        // Find all stream nodes
        for (var j = 0; j < allNodes.length; j++) {
          var node = allNodes[j];
          if (!node || !node.isStream || !node.audio) {
            continue;
          }

          // Filter out quickshell
          const nodeName = node.name || "";
          const nodeMediaName = (node.properties && node.properties["media.name"]) || "";
          if (nodeName === "quickshell" || nodeMediaName === "quickshell") {
            continue;
          }

          // Filter out filter streams
          const nodeIsVirtual = (node.properties && node.properties["node.virtual"]) || "";
          if (nodeIsVirtual) {
            continue;
          }

          var streamId = node.id;
          if (connectedStreamIds[streamId]) {
            continue;
          }

          if (Object.keys(intermediateNodeIds).length > 0) {
            connectedStreamIds[streamId] = true;
            connectedStreams.push(node);
          } else if (connectedStreams.length === 0) {
            connectedStreamIds[streamId] = true;
            connectedStreams.push(node);
          }
        }
      } catch (e) {}
    }

    return connectedStreams;
  }

  // Bind all devices to ensure their properties are available
  PwObjectTracker {
    objects: [...root.sinks, ...root.sources]
  }

  // Per-stream volume overrides (app + media identity) so concurrent browser streams do not share one entry.
  property var appVolumeOverrides: ({})
  property var _knownAppStreamIds: ({})
  property bool _isApplyingAppOverride: false

  PwObjectTracker {
    objects: root.streamNodes
  }

  // Keep appVolumeOverrides aligned with PipeWire when apps change volume/mute.
  Item {
    width: 0
    height: 0
    visible: false

    Repeater {
      model: root.appStreams

      delegate: Item {
        required property var modelData

        Connections {
          target: modelData?.audio ?? null

          function onVolumeChanged() {
            if (root._isApplyingAppOverride || !modelData?.audio) {
              return;
            }
            var key = root.getAppKey(modelData);
            if (key) {
              root.setAppStreamVolume(key, modelData.audio.volume);
            }
          }

          function onMutedChanged() {
            if (root._isApplyingAppOverride || !modelData?.audio) {
              return;
            }
            var key = root.getAppKey(modelData);
            if (key) {
              root.setAppStreamMuted(key, modelData.audio.muted);
            }
          }
        }
      }
    }
  }

  function getAppKey(node): string {
    if (!node || !node.properties) {
      return "";
    }
    var props = node.properties;
    var base = "";
    var binary = props["application.process.binary"] || "";
    if (binary) {
      var parts = binary.split("/");
      base = parts[parts.length - 1].toLowerCase();
    }
    if (!base) {
      var appName = props["application.name"] || "";
      if (appName) {
        base = appName.toLowerCase();
      }
    }
    if (!base) {
      var appId = props["application.id"] || "";
      if (appId) {
        base = appId.toLowerCase();
      }
    }
    if (!base) {
      return "";
    }

    var mediaName = (props["media.name"] || "").trim().toLowerCase();
    var mediaRole = (props["media.role"] || "").trim().toLowerCase();
    var tagParts = [];
    if (mediaName) {
      tagParts.push(mediaName);
    }
    if (mediaRole) {
      tagParts.push(mediaRole);
    }
    if (tagParts.length > 0) {
      return base + "\u001f" + tagParts.join("\u001e");
    }
    return base + "\u001f" + String(node.id);
  }

  function setAppStreamVolume(appKey: string, volume: real): void {
    if (!appKey) {
      return;
    }
    var o = appVolumeOverrides;
    if (!o[appKey]) {
      o[appKey] = {};
    }

    o[appKey].volume = volume;
    appVolumeOverrides = o;
  }

  function setAppStreamMuted(appKey: string, muted: bool): void {
    if (!appKey) {
      return;
    }
    var o = appVolumeOverrides;
    if (!o[appKey]) {
      o[appKey] = {};
    }
    o[appKey].muted = muted;
    appVolumeOverrides = o;
  }

  function getAppVolumeOverride(appKey: string) {
    return appKey ? (appVolumeOverrides[appKey] || null) : null;
  }

  function _applyAppOverrides(): void {
    var streams = root.appStreams;
    if (!streams) {
      return;
    }

    var prevKnown = root._knownAppStreamIds;
    var currentIds = {};
    _isApplyingAppOverride = true;
    for (var i = 0; i < streams.length; i++) {
      var s = streams[i];
      if (!s) {
        continue;
      }

      currentIds[s.id] = true;
      var key = getAppKey(s);
      var ov = key ? appVolumeOverrides[key] : null;

      // New stream node (reload, app restart, etc.): adopt PipeWire state into
      // overrides so we do not force an outdated Noctalia-only value.
      if (key && s.audio && !prevKnown[s.id]) {
        var seeded = appVolumeOverrides;
        if (!seeded[key]) {
          seeded[key] = {};
        }
        seeded[key].volume = s.audio.volume;
        seeded[key].muted = s.audio.muted;
        appVolumeOverrides = seeded;
        ov = seeded[key];
      }

      if (!ov || !s.audio) {
        continue;
      }
      if (ov.volume !== undefined && Math.abs(s.audio.volume - ov.volume) > root.epsilon) {
        s.audio.volume = ov.volume;
      }
      if (ov.muted !== undefined && s.audio.muted !== ov.muted) {
        s.audio.muted = ov.muted;
      }
    }
    _knownAppStreamIds = currentIds;
    _isApplyingAppOverride = false;
  }

  Connections {
    target: root
    function onAppStreamsChanged() {
      _appOverrideTimer.restart();
    }
  }

  Timer {
    id: _appOverrideTimer
    interval: 50
    onTriggered: root._applyAppOverrides()
  }

  Timer {
    id: _appOverrideEnforcer
    interval: 1000
    running: Object.keys(root.appVolumeOverrides).length > 0 && root.appStreams.length > 0
    repeat: true
    onTriggered: root._applyAppOverrides()
  }

  Component.onCompleted: {
    wpctlAvailabilityProcess.running = true;
  }

  Connections {
    target: root
    function onSinkChanged() {
      if (root.wpctlAvailable) {
        root.refreshWpctlOutputState();
      }
    }

    function onSourceChanged() {
      if (root.wpctlAvailable) {
        root.refreshWpctlInputState();
      }
    }
  }

  Timer {
    id: wpctlPollTimer
    // Safety net only; regular updates are event-driven from sink audio signals.
    interval: 20000
    running: root.wpctlAvailable
    repeat: true
    onTriggered: {
      root.refreshWpctlOutputState();
      root.refreshWpctlInputState();
    }
  }

  Process {
    id: wpctlAvailabilityProcess
    command: ["sh", "-c", "command -v wpctl"]
    running: false

    onExited: function (code) {
      root.wpctlAvailable = (code === 0);
      root.wpctlStateValid = false;
      root.wpctlInputStateValid = false;
      if (root.wpctlAvailable) {
        root.refreshWpctlOutputState();
        root.refreshWpctlInputState();
      }
    }
  }

  Process {
    id: wpctlStateProcess
    running: false

    onExited: function (code) {
      if (code !== 0 || !root.applyWpctlOutputState(stdout.text)) {
        root.wpctlStateValid = false;
      }
    }

    stdout: StdioCollector {}
  }

  Process {
    id: wpctlSetVolumeProcess
    running: false

    onExited: function (code) {
      root.isSettingOutputVolume = false;
      if (code !== 0) {
        Logger.w("AudioService", "wpctl set-volume failed, falling back to PipeWire node audio");
        if (root.sink?.audio) {
          root.sink.audio.muted = false;
          root.sink.audio.volume = root.clampOutputVolume(root.wpctlOutputVolume);
        }
      }
      root.refreshWpctlOutputState();
    }
  }

  Process {
    id: wpctlSetMuteProcess
    running: false

    onExited: function (_code) {
      root.refreshWpctlOutputState();
    }
  }

  Process {
    id: wpctlInputStateProcess
    running: false

    onExited: function (code) {
      if (code !== 0 || !root.applyWpctlInputState(stdout.text)) {
        root.wpctlInputStateValid = false;
      }
    }

    stdout: StdioCollector {}
  }

  Process {
    id: wpctlSetInputVolumeProcess
    running: false

    onExited: function (code) {
      root.isSettingInputVolume = false;
      if (code !== 0) {
        Logger.w("AudioService", "wpctl set-volume failed for default source, falling back to PipeWire node audio");
        if (root.source?.audio) {
          root.source.audio.muted = false;
          root.source.audio.volume = Math.max(0, Math.min(root.maxVolume, root.wpctlInputVolume));
        }
      }
      root.refreshWpctlInputState();
    }
  }

  Process {
    id: wpctlSetInputMuteProcess
    running: false

    onExited: function (_code) {
      root.refreshWpctlInputState();
    }
  }

  // Watch output device changes for clamping
  Connections {
    target: sink?.audio ?? null

    function onVolumeChanged() {
      if (root.wpctlAvailable) {
        root.refreshWpctlOutputState();
      }

      // Ignore volume changes if we're the one setting it (to prevent feedback loop)
      if (root.isSettingOutputVolume) {
        return;
      }

      if (!root.sink?.audio) {
        return;
      }

      const vol = root.sink.audio.volume;
      if (vol === undefined || isNaN(vol)) {
        return;
      }

      if (root.sink !== root._lastFeedbackSink) {
        root._lastFeedbackSink = root.sink;
      } else {
        playVolumeFeedback(clampOutputVolume(vol));
      }

      // If volume exceeds max, clamp it (but only if we didn't just set it)
      if (vol > root.maxVolume) {
        root.isSettingOutputVolume = true;
        Qt.callLater(() => {
                       if (root.sink?.audio && root.sink.audio.volume > root.maxVolume) {
                         root.sink.audio.volume = root.maxVolume;
                       }
                       root.isSettingOutputVolume = false;
                     });
      }
    }

    function onMutedChanged() {
      if (root.wpctlAvailable) {
        root.refreshWpctlOutputState();
      }
    }
  }

  // Watch input device changes for clamping
  Connections {
    target: source?.audio ?? null

    function onVolumeChanged() {
      if (root.wpctlAvailable) {
        root.refreshWpctlInputState();
      }

      // Ignore volume changes if we're the one setting it (to prevent feedback loop)
      if (root.isSettingInputVolume) {
        return;
      }

      if (!root.source?.audio) {
        return;
      }

      const vol = root.source.audio.volume;
      if (vol === undefined || isNaN(vol)) {
        return;
      }

      // If volume exceeds max, clamp it (but only if we didn't just set it)
      if (vol > root.maxVolume) {
        root.isSettingInputVolume = true;
        Qt.callLater(() => {
                       if (root.source?.audio && root.source.audio.volume > root.maxVolume) {
                         root.source.audio.volume = root.maxVolume;
                       }
                       root.isSettingInputVolume = false;
                     });
      }
    }

    function onMutedChanged() {
      if (root.wpctlAvailable) {
        root.refreshWpctlInputState();
      }
    }
  }

  // Output Control
  function increaseVolume() {
    if (!Pipewire.ready || (!sink?.audio && !wpctlAvailable)) {
      return;
    }
    if (volume >= root.maxVolume) {
      volumeAtMaximum();
      return;
    }
    setVolume(Math.min(root.maxVolume, volume + stepVolume));
  }

  function decreaseVolume() {
    if (!Pipewire.ready || (!sink?.audio && !wpctlAvailable)) {
      return;
    }
    if (volume <= 0) {
      volumeAtMinimum();
      return;
    }
    setVolume(Math.max(0, volume - stepVolume));
  }

  function setVolume(newVolume: real) {
    if (!Pipewire.ready || (!sink?.audio && !wpctlAvailable)) {
      Logger.w("AudioService", "No sink available or not ready");
      return;
    }

    const clampedVolume = clampOutputVolume(newVolume);
    const delta = Math.abs(clampedVolume - volume);
    if (delta < root.epsilon) {
      return;
    }

    if (wpctlAvailable) {
      if (wpctlSetVolumeProcess.running) {
        return;
      }

      isSettingOutputVolume = true;
      wpctlOutputMuted = false;
      wpctlOutputVolume = clampedVolume;
      wpctlStateValid = true;

      const volumePct = Math.round(clampedVolume * 10000) / 100;
      wpctlSetVolumeProcess.command = ["sh", "-c", "wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 && wpctl set-volume @DEFAULT_AUDIO_SINK@ " + volumePct + "%"];
      wpctlSetVolumeProcess.running = true;

      playVolumeFeedback(clampedVolume);
      return;
    }

    if (!sink?.ready || !sink?.audio) {
      Logger.w("AudioService", "No sink available or not ready");
      return;
    }

    // Set flag to prevent feedback loop, then set the actual volume
    isSettingOutputVolume = true;
    sink.audio.muted = false;
    sink.audio.volume = clampedVolume;

    playVolumeFeedback(clampedVolume);

    // Clear flag after a short delay to allow external changes to be detected
    Qt.callLater(() => {
                   isSettingOutputVolume = false;
                 });
  }

  function setOutputMuted(muted: bool) {
    if (!Pipewire.ready || (!sink?.audio && !wpctlAvailable)) {
      Logger.w("AudioService", "No sink available or Pipewire not ready");
      return;
    }

    if (wpctlAvailable) {
      if (wpctlSetMuteProcess.running) {
        return;
      }

      wpctlOutputMuted = muted;
      wpctlStateValid = true;
      wpctlSetMuteProcess.command = ["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", muted ? "1" : "0"];
      wpctlSetMuteProcess.running = true;
      return;
    }

    sink.audio.muted = muted;
  }

  function getOutputIcon() {
    if (muted) {
      return "volume-mute";
    }

    const clampedVolume = Math.max(0, Math.min(volume, root.maxVolume));

    // Show volume-x icon when volume is effectively 0% (within rounding threshold)
    if (clampedVolume < root.epsilon) {
      return "volume-x";
    }
    if (clampedVolume <= 0.5) {
      return "volume-low";
    }
    return "volume-high";
  }

  // Input Control
  function increaseInputVolume() {
    if (!Pipewire.ready || (!source?.audio && !wpctlAvailable)) {
      return;
    }
    if (inputVolume >= root.maxVolume) {
      return;
    }
    setInputVolume(Math.min(root.maxVolume, inputVolume + stepVolume));
  }

  function decreaseInputVolume() {
    if (!Pipewire.ready || (!source?.audio && !wpctlAvailable)) {
      return;
    }
    setInputVolume(Math.max(0, inputVolume - stepVolume));
  }

  function setInputVolume(newVolume: real) {
    if (!Pipewire.ready) {
      return;
    }

    const clampedVolume = Math.max(0, Math.min(root.maxVolume, newVolume));
    var currentVol = 0;
    if (wpctlAvailable && wpctlInputStateValid) {
      currentVol = wpctlInputVolume;
    } else if (source?.audio && source.audio.volume !== undefined && !isNaN(source.audio.volume)) {
      currentVol = source.audio.volume;
    }
    const delta = Math.abs(clampedVolume - currentVol);
    if (delta < root.epsilon) {
      return;
    }

    if (wpctlAvailable) {
      if (wpctlSetInputVolumeProcess.running) {
        return;
      }

      isSettingInputVolume = true;
      wpctlInputMuted = false;
      wpctlInputVolume = clampedVolume;
      wpctlInputStateValid = true;

      const volumePct = Math.round(clampedVolume * 10000) / 100;
      wpctlSetInputVolumeProcess.command = ["sh", "-c", "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 0 && wpctl set-volume @DEFAULT_AUDIO_SOURCE@ " + volumePct + "%"];
      wpctlSetInputVolumeProcess.running = true;
      return;
    }

    if (!source?.ready || !source?.audio) {
      Logger.w("AudioService", "No source available or not ready");
      return;
    }

    isSettingInputVolume = true;
    source.audio.muted = false;
    source.audio.volume = clampedVolume;

    Qt.callLater(() => {
                   isSettingInputVolume = false;
                 });
  }

  function playVolumeFeedback(currentVolume: real) {
    if (!SoundService.multimediaAvailable) {
      return;
    }

    if (!Settings.data.audio.volumeFeedback) {
      return;
    }

    const now = Date.now();
    if (now - lastVolumeFeedbackTime < minVolumeFeedbackInterval) {
      return;
    }
    lastVolumeFeedbackTime = now;

    const feedbackVolume = currentVolume;
    const configuredSoundFile = Settings.data.audio.volumeFeedbackSoundFile;
    const soundFile = (configuredSoundFile && configuredSoundFile.trim() !== "") ? configuredSoundFile : "volume-change.wav";

    SoundService.playSound(soundFile, {
                             volume: feedbackVolume,
                             fallback: false,
                             repeat: false
                           });
  }

  function setInputMuted(muted: bool) {
    if (!Pipewire.ready) {
      Logger.w("AudioService", "Pipewire not ready");
      return;
    }

    if (wpctlAvailable) {
      if (wpctlSetInputMuteProcess.running) {
        return;
      }

      wpctlInputMuted = muted;
      wpctlInputStateValid = true;
      wpctlSetInputMuteProcess.command = ["wpctl", "set-mute", "@DEFAULT_AUDIO_SOURCE@", muted ? "1" : "0"];
      wpctlSetInputMuteProcess.running = true;
      return;
    }

    if (!source?.audio) {
      Logger.w("AudioService", "No source available");
      return;
    }

    source.audio.muted = muted;
  }

  function getInputIcon() {
    if (inputMuted || inputVolume <= Number.EPSILON) {
      return "microphone-mute";
    }
    return "microphone";
  }

  // Device Selection
  function setAudioSink(newSink: PwNode): void {
    if (!Pipewire.ready) {
      Logger.w("AudioService", "Pipewire not ready");
      return;
    }
    Pipewire.preferredDefaultAudioSink = newSink;
  }

  function setAudioSource(newSource: PwNode): void {
    if (!Pipewire.ready) {
      Logger.w("AudioService", "Pipewire not ready");
      return;
    }
    Pipewire.preferredDefaultAudioSource = newSource;
  }
}
