pragma Singleton

import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import "../../Helpers/sha256.js" as Checksum
import qs.Commons

Singleton {
  id: root

  // -------------------------------------------------
  // Public Properties
  // -------------------------------------------------
  property bool imageMagickAvailable: false
  property bool initialized: false

  // Cache directories
  readonly property string baseDir: Settings.cacheDir + "images/"
  readonly property string wpThumbDir: baseDir + "wallpapers/thumbnails/"
  readonly property string wpLargeDir: baseDir + "wallpapers/large/"
  readonly property string notificationsDir: baseDir + "notifications/"
  readonly property string contributorsDir: baseDir + "contributors/"

  // Supported image formats - extended list when ImageMagick is available
  readonly property var basicImageFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp"]
  readonly property var extendedImageFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp", "*.webp", "*.avif", "*.heic", "*.heif", "*.tiff", "*.tif", "*.pnm", "*.pgm", "*.ppm", "*.pbm", "*.svg", "*.svgz", "*.ico", "*.icns", "*.jxl", "*.jp2", "*.j2k", "*.exr", "*.hdr", "*.dds", "*.tga"]
  readonly property var imageFilters: imageMagickAvailable ? extendedImageFilters : basicImageFilters

  // Check if a file format needs conversion (not natively supported by Qt)
  function needsConversion(filePath) {
    const ext = "*." + filePath.toLowerCase().split('.').pop();
    return !basicImageFilters.includes(ext);
  }

  // -------------------------------------------------
  // Internal State
  // -------------------------------------------------
  property var pendingRequests: ({})
  property var fallbackQueue: []
  property bool fallbackProcessing: false

  // Process queues to prevent "too many open files" errors
  property var utilityProcessQueue: []
  property int runningUtilityProcesses: 0
  readonly property int maxConcurrentUtilityProcesses: 16

  // Separate queue for heavy ImageMagick processing (lower concurrency)
  property var imageMagickQueue: []
  property int runningImageMagickProcesses: 0
  readonly property int maxConcurrentImageMagickProcesses: 4

  // -------------------------------------------------
  // Signals
  // -------------------------------------------------
  signal cacheHit(string cacheKey, string cachedPath)
  signal cacheMiss(string cacheKey)
  signal processingComplete(string cacheKey, string cachedPath)
  signal processingFailed(string cacheKey, string error)

  // -------------------------------------------------
  // Initialization
  // -------------------------------------------------
  function init() {
    Logger.i("ImageCache", "Service started");
    createDirectories();
    cleanupOldCache();
    checkMagickProcess.running = true;
  }

  function createDirectories() {
    Quickshell.execDetached(["mkdir", "-p", wpThumbDir]);
    Quickshell.execDetached(["mkdir", "-p", wpLargeDir]);
    Quickshell.execDetached(["mkdir", "-p", notificationsDir]);
    Quickshell.execDetached(["mkdir", "-p", contributorsDir]);
  }

  function cleanupOldCache() {
    const dirs = [wpThumbDir, wpLargeDir, notificationsDir, contributorsDir];
    dirs.forEach(function (dir) {
      Quickshell.execDetached(["find", dir, "-type", "f", "-mtime", "+15", "-delete"]);
    });
    Logger.d("ImageCache", "Cleanup triggered for files older than 15 days");
  }

  // -------------------------------------------------
  // Public API: Get Thumbnail (384x384)
  // -------------------------------------------------
  function getThumbnail(sourcePath, callback) {
    if (!sourcePath || sourcePath === "") {
      callback("", false);
      return;
    }

    getMtime(sourcePath, function (mtime) {
      const cacheKey = generateThumbnailKey(sourcePath, mtime);
      const cachedPath = wpThumbDir + cacheKey + ".png";

      processRequest(cacheKey, cachedPath, sourcePath, callback, function () {
        if (imageMagickAvailable) {
          startThumbnailProcessing(sourcePath, cachedPath, cacheKey);
        } else {
          queueFallbackProcessing(sourcePath, cachedPath, cacheKey, 384);
        }
      });
    });
  }

  // -------------------------------------------------
  // Public API: Get Large Image (scaled to specified dimensions)
  // -------------------------------------------------
  function getLarge(sourcePath, width, height, callback) {
    if (!sourcePath || sourcePath === "") {
      callback("", false);
      return;
    }

    if (Settings.data.wallpaper.useOriginalImages && !needsConversion(sourcePath)) {
      callback(sourcePath, false);
      return;
    }

    if (!imageMagickAvailable) {
      Logger.d("ImageCache", "ImageMagick not available, using original:", sourcePath);
      callback(sourcePath, false);
      return;
    }

    // Fast dimension check - skip processing if image fits screen AND format is Qt-native
    getImageDimensions(sourcePath, function (imgWidth, imgHeight) {
      const fitsScreen = imgWidth > 0 && imgHeight > 0 && imgWidth <= width && imgHeight <= height;

      if (fitsScreen) {
        // Only skip if format is natively supported by Qt
        if (!needsConversion(sourcePath)) {
          Logger.d("ImageCache", `Image ${imgWidth}x${imgHeight} fits screen ${width}x${height}, using original`);
          callback(sourcePath, false);
          return;
        }
        Logger.d("ImageCache", `Image needs conversion despite fitting screen`);
      }

      // Use actual image dimensions if it fits (convert without upscaling), otherwise use screen dimensions
      const targetWidth = fitsScreen ? imgWidth : width;
      const targetHeight = fitsScreen ? imgHeight : height;

      getMtime(sourcePath, function (mtime) {
        const cacheKey = generateLargeKey(sourcePath, width, height, mtime);
        const cachedPath = wpLargeDir + cacheKey + ".png";

        processRequest(cacheKey, cachedPath, sourcePath, callback, function () {
          startLargeProcessing(sourcePath, cachedPath, targetWidth, targetHeight, cacheKey);
        });
      });
    });
  }

  // -------------------------------------------------
  // Public API: Get Notification Icon (64x64)
  // -------------------------------------------------
  function getNotificationIcon(imageUri, appName, summary, callback) {
    if (!imageUri || imageUri === "") {
      callback("", false);
      return;
    }

    // Resolve bare file path for temp check
    const filePath = imageUri.startsWith("file://") ? imageUri.substring(7) : imageUri;

    // File paths in persistent locations are used directly, not cached
    if ((imageUri.startsWith("/") || imageUri.startsWith("file://")) && !isTemporaryPath(filePath)) {
      callback(imageUri, false);
      return;
    }

    const cacheKey = generateNotificationKey(imageUri, appName, summary);
    const cachedPath = notificationsDir + cacheKey + ".png";

    // Temporary file paths are copied to cache before the source is cleaned up
    if (imageUri.startsWith("/") || imageUri.startsWith("file://")) {
      processRequest(cacheKey, cachedPath, imageUri, callback, function () {
        copyTempFileToCache(filePath, cachedPath, cacheKey);
      });
      return;
    }

    processRequest(cacheKey, cachedPath, imageUri, callback, function () {
      // Notifications always use Qt fallback (image:// URIs can't be read by ImageMagick)
      queueFallbackProcessing(imageUri, cachedPath, cacheKey, 64);
    });
  }

  // Check if a path is in a temporary directory that may be cleaned up
  function isTemporaryPath(path) {
    return path.startsWith("/tmp/");
  }

  // Copy a temporary file to the cache directory
  function copyTempFileToCache(sourcePath, destPath, cacheKey) {
    const srcEsc = sourcePath.replace(/'/g, "'\\''");
    const dstEsc = destPath.replace(/'/g, "'\\''");

    const processString = `
      import QtQuick
      import Quickshell.Io
      Process {
        command: ["cp", "--", "${srcEsc}", "${dstEsc}"]
        stdout: StdioCollector {}
        stderr: StdioCollector {}
      }
    `;

    queueUtilityProcess({
                          name: "CopyTempFile_" + cacheKey,
                          processString: processString,
                          onComplete: function (exitCode) {
                            if (exitCode === 0) {
                              Logger.d("ImageCache", "Temp file cached:", destPath);
                              notifyCallbacks(cacheKey, destPath, true);
                            } else {
                              Logger.w("ImageCache", "Failed to cache temp file:", sourcePath);
                              notifyCallbacks(cacheKey, "", false);
                            }
                          },
                          onError: function () {
                            Logger.e("ImageCache", "Error caching temp file:", sourcePath);
                            notifyCallbacks(cacheKey, "", false);
                          }
                        });
  }

  // -------------------------------------------------
  // Public API: Get Circular Avatar (256x256)
  // -------------------------------------------------
  function getCircularAvatar(url, username, callback) {
    if (!url || !username) {
      callback("", false);
      return;
    }

    const cacheKey = username;
    const cachedPath = contributorsDir + username + "_circular.png";

    processRequest(cacheKey, cachedPath, url, callback, function () {
      if (imageMagickAvailable) {
        downloadAndProcessAvatar(url, username, cachedPath, cacheKey);
      } else {
        // No fallback for circular avatars without ImageMagick
        Logger.w("ImageCache", "Circular avatars require ImageMagick");
        notifyCallbacks(cacheKey, "", false);
      }
    });
  }

  // -------------------------------------------------
  // Cache Key Generation
  // -------------------------------------------------
  function generateThumbnailKey(sourcePath, mtime) {
    const keyString = sourcePath + "@384x384@" + (mtime || "unknown");
    return Checksum.sha256(keyString);
  }

  function generateLargeKey(sourcePath, width, height, mtime) {
    const keyString = sourcePath + "@" + width + "x" + height + "@" + (mtime || "unknown");
    return Checksum.sha256(keyString);
  }

  function generateNotificationKey(imageUri, appName, summary) {
    if (imageUri.startsWith("image://qsimage/")) {
      return Checksum.sha256(appName + "|" + summary);
    }
    return Checksum.sha256(imageUri);
  }

  // -------------------------------------------------
  // Request Processing (with coalescing)
  // -------------------------------------------------
  function processRequest(cacheKey, cachedPath, sourcePath, callback, processFn) {
    // Check if already processing this request
    if (pendingRequests[cacheKey]) {
      pendingRequests[cacheKey].callbacks.push(callback);
      Logger.d("ImageCache", "Coalescing request for:", cacheKey);
      return;
    }

    // Check cache first
    checkFileExists(cachedPath, function (exists) {
      if (exists) {
        Logger.d("ImageCache", "Cache hit:", cachedPath);
        callback(cachedPath, true);
        cacheHit(cacheKey, cachedPath);
        return;
      }

      // Re-check pendingRequests (race condition fix)
      if (pendingRequests[cacheKey]) {
        pendingRequests[cacheKey].callbacks.push(callback);
        return;
      }

      // Start new processing
      Logger.d("ImageCache", "Cache miss, processing:", sourcePath);
      cacheMiss(cacheKey);
      pendingRequests[cacheKey] = {
        callbacks: [callback],
        sourcePath: sourcePath
      };

      processFn();
    });
  }

  function notifyCallbacks(cacheKey, path, success) {
    const request = pendingRequests[cacheKey];
    if (request) {
      request.callbacks.forEach(function (cb) {
        cb(path, success);
      });
      delete pendingRequests[cacheKey];
    }

    if (success) {
      processingComplete(cacheKey, path);
    } else {
      processingFailed(cacheKey, "Processing failed");
    }
  }

  // -------------------------------------------------
  // ImageMagick Processing: Thumbnail
  // -------------------------------------------------
  function startThumbnailProcessing(sourcePath, outputPath, cacheKey) {
    const srcEsc = sourcePath.replace(/'/g, "'\\''");
    const dstEsc = outputPath.replace(/'/g, "'\\''");

    // Use Lanczos filter for high-quality downscaling, subtle unsharp mask, and PNG for lossless output
    const command = `magick '${srcEsc}' -auto-orient -filter Lanczos -resize '384x384^' -gravity center -extent 384x384 -unsharp 0x0.5 '${dstEsc}'`;

    runProcess(command, cacheKey, outputPath, sourcePath);
  }

  // -------------------------------------------------
  // ImageMagick Processing: Large
  // -------------------------------------------------
  function startLargeProcessing(sourcePath, outputPath, width, height, cacheKey) {
    const srcEsc = sourcePath.replace(/'/g, "'\\''");
    const dstEsc = outputPath.replace(/'/g, "'\\''");

    // Use Lanczos filter for high-quality downscaling and PNG for lossless output
    const command = `magick '${srcEsc}' -auto-orient -filter Lanczos -resize '${width}x${height}^' '${dstEsc}'`;

    runProcess(command, cacheKey, outputPath, sourcePath);
  }

  // -------------------------------------------------
  // ImageMagick Processing: Circular Avatar
  // -------------------------------------------------
  function downloadAndProcessAvatar(url, username, outputPath, cacheKey) {
    const tempPath = contributorsDir + username + "_temp.png";
    const tempEsc = tempPath.replace(/'/g, "'\\''");
    const urlEsc = url.replace(/'/g, "'\\''");

    // Download first (uses utility queue since curl/wget are lightweight)
    const downloadCmd = `curl -L -s -o '${tempEsc}' '${urlEsc}' || wget -q -O '${tempEsc}' '${urlEsc}'`;

    const processString = `
      import QtQuick
      import Quickshell.Io
      Process {
        command: ["sh", "-c", "${downloadCmd.replace(/"/g, '\\"')}"]
        stdout: StdioCollector {}
        stderr: StdioCollector {}
      }
    `;

    queueUtilityProcess({
                          name: "DownloadProcess_" + cacheKey,
                          processString: processString,
                          onComplete: function (exitCode) {
                            if (exitCode !== 0) {
                              Logger.e("ImageCache", "Failed to download avatar for", username);
                              notifyCallbacks(cacheKey, "", false);
                              return;
                            }
                            // Now process with ImageMagick
                            processCircularAvatar(tempPath, outputPath, cacheKey);
                          },
                          onError: function () {
                            notifyCallbacks(cacheKey, "", false);
                          }
                        });
  }

  function processCircularAvatar(inputPath, outputPath, cacheKey) {
    const srcEsc = inputPath.replace(/'/g, "'\\''");
    const dstEsc = outputPath.replace(/'/g, "'\\''");

    // ImageMagick command for circular crop with alpha
    const command = `magick '${srcEsc}' -resize 256x256^ -gravity center -extent 256x256 -alpha set \\( +clone -channel A -evaluate set 0 +channel -fill white -draw 'circle 128,128 128,0' \\) -compose DstIn -composite '${dstEsc}'`;

    queueImageMagickProcess({
                              command: command,
                              cacheKey: cacheKey,
                              onComplete: function (exitCode) {
                                // Clean up temp file
                                Quickshell.execDetached(["rm", "-f", inputPath]);

                                if (exitCode !== 0) {
                                  Logger.e("ImageCache", "Failed to create circular avatar");
                                  notifyCallbacks(cacheKey, "", false);
                                } else {
                                  Logger.d("ImageCache", "Circular avatar created:", outputPath);
                                  notifyCallbacks(cacheKey, outputPath, true);
                                }
                              },
                              onError: function () {
                                Quickshell.execDetached(["rm", "-f", inputPath]);
                                notifyCallbacks(cacheKey, "", false);
                              }
                            });
  }

  // -------------------------------------------------
  // Generic Process Runner (with queue for ImageMagick)
  // -------------------------------------------------

  // Queue an ImageMagick process and run it when a slot is available
  function queueImageMagickProcess(request) {
    imageMagickQueue.push(request);
    processImageMagickQueue();
  }

  // Process queued ImageMagick requests up to the concurrency limit
  function processImageMagickQueue() {
    while (runningImageMagickProcesses < maxConcurrentImageMagickProcesses && imageMagickQueue.length > 0) {
      const request = imageMagickQueue.shift();
      runImageMagickProcess(request);
    }
  }

  // Actually run an ImageMagick process
  function runImageMagickProcess(request) {
    runningImageMagickProcesses++;

    const processString = `
      import QtQuick
      import Quickshell.Io
      Process {
        command: ["sh", "-c", ""]
        stdout: StdioCollector {}
        stderr: StdioCollector {}
      }
    `;

    try {
      const processObj = Qt.createQmlObject(processString, root, "ImageProcess_" + request.cacheKey);
      processObj.command = ["sh", "-c", request.command];

      processObj.exited.connect(function (exitCode) {
        processObj.destroy();
        runningImageMagickProcesses--;
        request.onComplete(exitCode, processObj);
        processImageMagickQueue();
      });

      processObj.running = true;
    } catch (e) {
      Logger.e("ImageCache", "Failed to create process:", e);
      runningImageMagickProcesses--;
      request.onError(e);
      processImageMagickQueue();
    }
  }

  function runProcess(command, cacheKey, outputPath, sourcePath) {
    queueImageMagickProcess({
                              command: command,
                              cacheKey: cacheKey,
                              onComplete: function (exitCode, proc) {
                                if (exitCode !== 0) {
                                  const stderrText = proc.stderr.text || "";
                                  Logger.e("ImageCache", "Processing failed:", stderrText);
                                  notifyCallbacks(cacheKey, sourcePath, false);
                                } else {
                                  Logger.d("ImageCache", "Processing complete:", outputPath);
                                  notifyCallbacks(cacheKey, outputPath, true);
                                }
                              },
                              onError: function () {
                                notifyCallbacks(cacheKey, sourcePath, false);
                              }
                            });
  }

  // -------------------------------------------------
  // Qt Fallback Renderer
  // -------------------------------------------------
  PanelWindow {
    id: fallbackRenderer
    implicitWidth: 0
    implicitHeight: 0
    WlrLayershell.exclusionMode: ExclusionMode.Ignore
    WlrLayershell.namespace: "noctalia-image-cache-renderer"
    color: "transparent"
    mask: Region {}

    Image {
      id: fallbackImage
      property string cacheKey: ""
      property string destPath: ""
      property int targetSize: 256

      width: targetSize
      height: targetSize
      visible: true
      cache: false
      asynchronous: true
      fillMode: Image.PreserveAspectCrop
      mipmap: true
      antialiasing: true

      onStatusChanged: {
        if (!cacheKey)
        return;

        if (status === Image.Ready) {
          grabToImage(function (result) {
            if (result.saveToFile(destPath)) {
              Logger.d("ImageCache", "Fallback cache created:", destPath);
              root.notifyCallbacks(cacheKey, destPath, true);
            } else {
              Logger.e("ImageCache", "Failed to save fallback cache");
              root.notifyCallbacks(cacheKey, "", false);
            }
            processNextFallback();
          });
        } else if (status === Image.Error) {
          Logger.e("ImageCache", "Fallback image load failed");
          root.notifyCallbacks(cacheKey, "", false);
          processNextFallback();
        }
      }

      function processNextFallback() {
        cacheKey = "";
        destPath = "";
        source = "";

        if (fallbackQueue.length > 0) {
          const next = fallbackQueue.shift();
          cacheKey = next.cacheKey;
          destPath = next.destPath;
          targetSize = next.size;
          source = next.sourcePath;
        } else {
          fallbackProcessing = false;
        }
      }
    }
  }

  function queueFallbackProcessing(sourcePath, destPath, cacheKey, size) {
    fallbackQueue.push({
                         sourcePath: sourcePath,
                         destPath: destPath,
                         cacheKey: cacheKey,
                         size: size
                       });

    if (!fallbackProcessing) {
      fallbackProcessing = true;
      const item = fallbackQueue.shift();
      fallbackImage.cacheKey = item.cacheKey;
      fallbackImage.destPath = item.destPath;
      fallbackImage.targetSize = item.size;
      fallbackImage.source = item.sourcePath;
    }
  }

  // -------------------------------------------------
  // Utility Functions (with process queue to prevent fd exhaustion)
  // -------------------------------------------------

  // Queue a utility process and run it when a slot is available
  function queueUtilityProcess(request) {
    utilityProcessQueue.push(request);
    processUtilityQueue();
  }

  // Process queued utility requests up to the concurrency limit
  function processUtilityQueue() {
    while (runningUtilityProcesses < maxConcurrentUtilityProcesses && utilityProcessQueue.length > 0) {
      const request = utilityProcessQueue.shift();
      runUtilityProcess(request);
    }
  }

  // Actually run a utility process
  function runUtilityProcess(request) {
    runningUtilityProcesses++;

    try {
      const processObj = Qt.createQmlObject(request.processString, root, request.name);

      processObj.exited.connect(function (exitCode) {
        processObj.destroy();
        runningUtilityProcesses--;
        request.onComplete(exitCode, processObj);
        processUtilityQueue();
      });

      processObj.running = true;
    } catch (e) {
      Logger.e("ImageCache", "Failed to create " + request.name + ":", e);
      runningUtilityProcesses--;
      request.onError(e);
      processUtilityQueue();
    }
  }

  function getMtime(filePath, callback) {
    const pathEsc = filePath.replace(/'/g, "'\\''");
    const processString = `
      import QtQuick
      import Quickshell.Io
      Process {
        command: ["stat", "-c", "%Y", "${pathEsc}"]
        stdout: StdioCollector {}
        stderr: StdioCollector {}
      }
    `;

    queueUtilityProcess({
                          name: "MtimeProcess",
                          processString: processString,
                          onComplete: function (exitCode, proc) {
                            const mtime = exitCode === 0 ? proc.stdout.text.trim() : "";
                            callback(mtime);
                          },
                          onError: function () {
                            callback("");
                          }
                        });
  }

  function checkFileExists(filePath, callback) {
    const pathEsc = filePath.replace(/'/g, "'\\''");
    const processString = `
      import QtQuick
      import Quickshell.Io
      Process {
        command: ["test", "-f", "${pathEsc}"]
        stdout: StdioCollector {}
        stderr: StdioCollector {}
      }
    `;

    queueUtilityProcess({
                          name: "FileExistsProcess",
                          processString: processString,
                          onComplete: function (exitCode) {
                            callback(exitCode === 0);
                          },
                          onError: function () {
                            callback(false);
                          }
                        });
  }

  function getImageDimensions(filePath, callback) {
    const pathEsc = filePath.replace(/'/g, "'\\''");
    const processString = `
      import QtQuick
      import Quickshell.Io
      Process {
        command: ["identify", "-ping", "-format", "%w %h", "${pathEsc}[0]"]
        stdout: StdioCollector {}
        stderr: StdioCollector {}
      }
    `;

    queueUtilityProcess({
                          name: "IdentifyProcess",
                          processString: processString,
                          onComplete: function (exitCode, proc) {
                            let width = 0, height = 0;
                            if (exitCode === 0) {
                              const parts = proc.stdout.text.trim().split(" ");
                              if (parts.length >= 2) {
                                width = parseInt(parts[0], 10) || 0;
                                height = parseInt(parts[1], 10) || 0;
                              }
                            }
                            callback(width, height);
                          },
                          onError: function () {
                            callback(0, 0);
                          }
                        });
  }

  // -------------------------------------------------
  // Cache Invalidation
  // -------------------------------------------------
  function invalidateThumbnail(sourcePath) {
    Logger.i("ImageCache", "Invalidating thumbnail for:", sourcePath);
    // Since cache keys include hash, we'd need to track mappings
    // For simplicity, clear all thumbnails
    clearThumbnails();
  }

  function invalidateLarge(sourcePath) {
    Logger.i("ImageCache", "Invalidating large for:", sourcePath);
    clearLarge();
  }

  function invalidateNotification(imageId) {
    const path = notificationsDir + imageId + ".png";
    Quickshell.execDetached(["rm", "-f", path]);
  }

  function invalidateAvatar(username) {
    const path = contributorsDir + username + "_circular.png";
    Quickshell.execDetached(["rm", "-f", path]);
  }

  // -------------------------------------------------
  // Clear Cache Functions
  // -------------------------------------------------
  function clearAll() {
    Logger.i("ImageCache", "Clearing all cache");
    clearThumbnails();
    clearLarge();
    clearNotifications();
    clearContributors();
  }

  function clearThumbnails() {
    Logger.i("ImageCache", "Clearing thumbnails cache");
    Quickshell.execDetached(["rm", "-rf", wpThumbDir]);
    Quickshell.execDetached(["mkdir", "-p", wpThumbDir]);
  }

  function clearLarge() {
    Logger.i("ImageCache", "Clearing large cache");
    Quickshell.execDetached(["rm", "-rf", wpLargeDir]);
    Quickshell.execDetached(["mkdir", "-p", wpLargeDir]);
  }

  function clearNotifications() {
    Logger.i("ImageCache", "Clearing notifications cache");
    Quickshell.execDetached(["rm", "-rf", notificationsDir]);
    Quickshell.execDetached(["mkdir", "-p", notificationsDir]);
  }

  function clearContributors() {
    Logger.i("ImageCache", "Clearing contributors cache");
    Quickshell.execDetached(["rm", "-rf", contributorsDir]);
    Quickshell.execDetached(["mkdir", "-p", contributorsDir]);
  }

  // -------------------------------------------------
  // ImageMagick Detection
  // -------------------------------------------------
  Process {
    id: checkMagickProcess
    command: ["sh", "-c", "command -v magick"]
    running: false

    stdout: StdioCollector {}
    stderr: StdioCollector {}

    onExited: function (exitCode) {
      root.imageMagickAvailable = (exitCode === 0);
      root.initialized = true;
      if (root.imageMagickAvailable) {
        Logger.i("ImageCache", "ImageMagick available");
      } else {
        Logger.w("ImageCache", "ImageMagick not found, using Qt fallback");
      }
    }
  }
}
