// Real JavaScript implementation for script downloading
import { join } from 'path';
import { writeFile, mkdir, access, readFile, unlink } from 'fs/promises';
import { downloadRawFile } from '../lib/gitProvider/index.js';

export class ScriptDownloaderService {
  constructor() {
    /** @type {string} */
    this.scriptsDirectory = join(process.cwd(), 'scripts');
    /** @type {string} */
    this.repoUrl = process.env.REPO_URL || 'https://github.com/community-scripts/ProxmoxVE';
  }

  /**
   * Normalize alpine CT slug to avoid double prefixing.
   * Example: "alpine-it-tools" stays "alpine-it-tools" (not "alpine-alpine-it-tools").
   * @param {string} slug
   * @returns {string}
   */
  normalizeAlpineSlug(slug) {
    const s = String(slug || '').trim();
    return s.startsWith('alpine-') ? s : `alpine-${s}`;
  }

  initializeConfig() {
    // Re-initialize if needed (for environment changes)
    this.scriptsDirectory = join(process.cwd(), 'scripts');
    this.repoUrl = process.env.REPO_URL || 'https://github.com/community-scripts/ProxmoxVE';
  }

  /**
   * Validates that a directory path doesn't contain nested directories with the same name
   * (e.g., prevents ct/ct or install/install)
   * @param {string} dirPath - The directory path to validate
   * @returns {boolean}
   */
  validateDirectoryPath(dirPath) {
    const normalizedPath = dirPath.replace(/\\/g, '/');
    const parts = normalizedPath.split('/');

    // Check for consecutive duplicate directory names
    for (let i = 0; i < parts.length - 1; i++) {
      if (parts[i] === parts[i + 1] && parts[i] !== '') {
        throw new Error(`Invalid directory path: nested directory detected (${parts[i]}/${parts[i + 1]}) in path: ${dirPath}`);
      }
    }

    return true;
  }

  /**
   * Validates that finalTargetDir doesn't contain nested directory names like ct/ct or install/install
   * @param {string} targetDir - The base target directory
   * @param {string} finalTargetDir - The final target directory to validate
   * @returns {string}
   */
  validateTargetDir(targetDir, finalTargetDir) {
    // Check if finalTargetDir contains nested directory names
    const normalized = finalTargetDir.replace(/\\/g, '/');
    const parts = normalized.split('/');

    // Check for consecutive duplicate directory names
    for (let i = 0; i < parts.length - 1; i++) {
      if (parts[i] === parts[i + 1]) {
        console.warn(`[Path Validation] Detected nested directory pattern "${parts[i]}/${parts[i + 1]}" in finalTargetDir: ${finalTargetDir}. Using base directory "${targetDir}" instead.`);
        return targetDir; // Return the base directory instead
      }
    }

    return finalTargetDir;
  }

  /**
   * Ensure a directory exists, creating it if necessary
   * @param {string} dirPath - The directory path to ensure exists
   * @returns {Promise<void>}
   */
  async ensureDirectoryExists(dirPath) {
    // Validate the directory path to prevent nested directories with the same name
    this.validateDirectoryPath(dirPath);

    try {
      console.log(`[Directory Creation] Ensuring directory exists: ${dirPath}`);
      await mkdir(dirPath, { recursive: true });
      console.log(`[Directory Creation] Directory created/verified: ${dirPath}`);
    } catch (/** @type {any} */ error) {
      if (error.code !== 'EEXIST') {
        console.error(`[Directory Creation] Error creating directory ${dirPath}:`, error.message);
        throw error;
      }
      // Directory already exists, which is fine
      console.log(`[Directory Creation] Directory already exists: ${dirPath}`);
    }
  }

  /**
   * Download a file from the repository (GitHub, GitLab, Bitbucket, or custom)
   * @param {string} repoUrl - The repository URL
   * @param {string} filePath - The file path within the repository
   * @param {string} [branch] - The branch to download from
   * @returns {Promise<string>}
   */
  async downloadFileFromRepo(repoUrl, filePath, branch = 'main') {
    if (!repoUrl) {
      throw new Error('Repository URL is not set');
    }
    console.log(`Downloading from repository: ${repoUrl} (${filePath})`);
    return downloadRawFile(repoUrl, filePath, branch);
  }

  /**
   * Get repository URL for a script.
   * Dev scripts (is_dev=true) use the ProxmoxVED repo.
   * User-defined local scripts may carry an explicit repository_url.
   * PocketBase-sourced community scripts use the default community repo.
   * @param {import('~/types/script').Script} script - The script object
   * @returns {string}
   */
  getRepoUrlForScript(script) {
    if (script.repository_url) {
      return script.repository_url;
    }
    this.initializeConfig();
    if (script.is_dev) {
      // Dev scripts live in the ProxmoxVED repository
      return this.repoUrl.replace(/\/ProxmoxVE\b/, '/ProxmoxVED');
    }
    return this.repoUrl;
  }

  /**
   * Derive the install script file path from the script type slug and method type.
   * Mirrors the convention used in ProxmoxVE-Frontend/lib/install-command.ts.
   *
   * Examples:
   *   ("ct", "default", "adguard")  → "ct/adguard.sh"
   *   ("ct", "alpine",  "adguard")  → "ct/alpine-adguard.sh"
   *   ("pve", "default", "foo")     → "tools/pve/foo.sh"
   *   ("addon", "default", "bar")   → "tools/addon/bar.sh"
   *   ("vm", "default", "ubuntu")   → "vm/ubuntu.sh"
   *
   * @param {string} scriptType  - Script type slug: "ct"|"lxc"|"pve"|"addon"|"vm"|"turnkey"
   * @param {string} methodType  - Install method type: "default"|"alpine"|…
   * @param {string} slug        - Script slug
   * @returns {string | null}
   */
  deriveScriptPath(scriptType, methodType, slug) {
    const type = (scriptType || 'ct').toLowerCase().trim();
    const method = (methodType || 'default').toLowerCase().trim();

    if (method === 'alpine') {
      // Alpine variants only exist for CT/LXC scripts
      if (type === 'ct' || type === 'lxc') {
        return `ct/${this.normalizeAlpineSlug(slug)}.sh`;
      }
      return null;
    }

    switch (type) {
      case 'ct':
      case 'lxc':
        return `ct/${slug}.sh`;
      case 'pve':
        return `tools/pve/${slug}.sh`;
      case 'addon':
        return `tools/addon/${slug}.sh`;
      case 'vm':
        return `vm/${slug}.sh`;
      case 'turnkey':
        return `turnkey/${slug}.sh`;
      default:
        return `ct/${slug}.sh`;
    }
  }

  /**
   * Resolve the install script file path for a method.
   * Prefers the explicit `method.script` field (user-defined local scripts),
   * otherwise derives the path from script type + method type + slug.
   *
   * @param {import('~/types/script').Script} script
   * @param {import('~/types/script').ScriptInstallMethod} method
   * @returns {string | null}
   */
  resolveScriptPath(script, method) {
    if (method.script) {
      return method.script;
    }
    return this.deriveScriptPath(script.type, method.type, script.slug);
  }

  /**
   * Modify script content to use local paths
   * @param {string} content - The script content
   * @returns {string}
   */
  modifyScriptContent(content) {
    // Replace the build.func source line
    const oldPattern = /source <\(curl -fsSL https:\/\/raw\.githubusercontent\.com\/community-scripts\/ProxmoxVE\/main\/misc\/build\.func\)/g;
    const newPattern = 'SCRIPT_DIR="$(dirname "$0")" \nsource "$SCRIPT_DIR/../core/build.func"';

    return content.replace(oldPattern, newPattern);
  }

  /**
   * Load a script by downloading its files
   * @param {import('~/types/script').Script} script - The script to load
   * @returns {Promise<{success: boolean, message: string, files: string[], error?: string}>}
   */
  async loadScript(script) {
    this.initializeConfig();
    try {
      /** @type {string[]} */
      const files = [];
      const repoUrl = this.getRepoUrlForScript(script);
      const branch = process.env.REPO_BRANCH || 'main';

      console.log(`Loading script "${script.name}" (${script.slug}) from repository: ${repoUrl}`);

      // Ensure directories exist
      await this.ensureDirectoryExists(join(this.scriptsDirectory, 'ct'));
      await this.ensureDirectoryExists(join(this.scriptsDirectory, 'install'));
      await this.ensureDirectoryExists(join(this.scriptsDirectory, 'tools'));
      await this.ensureDirectoryExists(join(this.scriptsDirectory, 'vm'));

      /** @type {string[]} */
      const methodErrors = [];

      if (script.install_methods?.length) {
        for (const method of script.install_methods) {
          const scriptPath = this.resolveScriptPath(script, method);
          if (scriptPath) {
            const fileName = scriptPath.split('/').pop();

            if (fileName) {
              try {
                console.log(`Downloading script file: ${scriptPath} from ${repoUrl}`);
                const content = await this.downloadFileFromRepo(repoUrl, scriptPath, branch);

                // Determine target directory based on script path
                let targetDir;
                let finalTargetDir;
                let filePath;

                if (scriptPath.startsWith('ct/')) {
                  targetDir = 'ct';
                  finalTargetDir = targetDir;
                  // Validate and sanitize finalTargetDir
                  finalTargetDir = this.validateTargetDir(targetDir, finalTargetDir);
                  // Modify the content for CT scripts
                  const modifiedContent = this.modifyScriptContent(content);
                  filePath = join(this.scriptsDirectory, targetDir, fileName);
                  await writeFile(filePath, modifiedContent, 'utf-8');
                } else if (scriptPath.startsWith('tools/')) {
                  targetDir = 'tools';
                  // Preserve subdirectory structure for tools scripts
                  const subPath = scriptPath.replace('tools/', '');
                  const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
                  finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
                  // Validate and sanitize finalTargetDir
                  finalTargetDir = this.validateTargetDir(targetDir, finalTargetDir);
                  // Ensure the subdirectory exists
                  await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
                  filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
                  await writeFile(filePath, content, 'utf-8');
                } else if (scriptPath.startsWith('vm/')) {
                  targetDir = 'vm';
                  // Preserve subdirectory structure for VM scripts
                  const subPath = scriptPath.replace('vm/', '');
                  const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
                  finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
                  // Validate and sanitize finalTargetDir
                  finalTargetDir = this.validateTargetDir(targetDir, finalTargetDir);
                  // Ensure the subdirectory exists
                  await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
                  filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
                  await writeFile(filePath, content, 'utf-8');
                } else {
                  // Handle other script types (fallback to ct directory)
                  targetDir = 'ct';
                  finalTargetDir = targetDir;
                  // Validate and sanitize finalTargetDir
                  finalTargetDir = this.validateTargetDir(targetDir, finalTargetDir);
                  const modifiedContent = this.modifyScriptContent(content);
                  filePath = join(this.scriptsDirectory, targetDir, fileName);
                  await writeFile(filePath, modifiedContent, 'utf-8');
                }

                files.push(`${finalTargetDir}/${fileName}`);
                console.log(`Successfully downloaded: ${finalTargetDir}/${fileName}`);
              } catch (methodError) {
                // Don't let one missing install-method file (e.g. a 404 for an
                // alpine/default variant) abort the whole script download —
                // record it and keep trying the remaining install methods.
                const msg = methodError instanceof Error ? methodError.message : String(methodError);
                console.error(`Failed to download script file ${scriptPath}: ${msg}`);
                methodErrors.push(`${scriptPath}: ${msg}`);
              }
            }
          }
        }
      }

      // Fallback: if install_methods is empty, derive the default path from script.type + slug.
      // This is required for some records where install_methods is missing (e.g. pve/tools scripts).
      const typeNorm = (script.type || 'ct').toLowerCase();
      const isCtType = typeNorm === 'ct' || typeNorm === 'lxc';
      if (!script.install_methods || script.install_methods.length === 0) {
        const fallbackPath = this.deriveScriptPath(script.type, 'default', script.slug);
        const fallbackFileName = `${script.slug}.sh`;

        if (fallbackPath) {
          try {
            console.log(`[Fallback] install_methods empty, downloading script: ${fallbackPath} from ${repoUrl}`);
            const content = await this.downloadFileFromRepo(repoUrl, fallbackPath, branch);

            if (fallbackPath.startsWith('ct/')) {
              const modifiedContent = this.modifyScriptContent(content);
              const filePath = join(this.scriptsDirectory, 'ct', fallbackFileName);
              await writeFile(filePath, modifiedContent, 'utf-8');
              files.push(`ct/${fallbackFileName}`);
              console.log(`[Fallback] Successfully downloaded: ct/${fallbackFileName}`);
            } else if (fallbackPath.startsWith('tools/')) {
              const subPath = fallbackPath.replace('tools/', '');
              const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
              const finalTargetDir = subDir ? join('tools', subDir) : 'tools';
              await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
              const filePath = join(this.scriptsDirectory, finalTargetDir, fallbackFileName);
              await writeFile(filePath, content, 'utf-8');
              files.push(`${finalTargetDir}/${fallbackFileName}`);
              console.log(`[Fallback] Successfully downloaded: ${finalTargetDir}/${fallbackFileName}`);
            } else if (fallbackPath.startsWith('vm/')) {
              const subPath = fallbackPath.replace('vm/', '');
              const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
              const finalTargetDir = subDir ? join('vm', subDir) : 'vm';
              await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
              const filePath = join(this.scriptsDirectory, finalTargetDir, fallbackFileName);
              await writeFile(filePath, content, 'utf-8');
              files.push(`${finalTargetDir}/${fallbackFileName}`);
              console.log(`[Fallback] Successfully downloaded: ${finalTargetDir}/${fallbackFileName}`);
            }
          } catch (error) {
            console.log(`[Fallback] Script not found in repository: ${fallbackPath}`);
          }
        }
      }

      // Only download install script for CT/LXC scripts
      const scriptTypeNorm = (script.type || 'ct').toLowerCase();
      const hasCtScript = scriptTypeNorm === 'ct' || scriptTypeNorm === 'lxc';
      if (hasCtScript) {
        const installScriptName = `${script.slug}-install.sh`;
        try {
          console.log(`Downloading install script: install/${installScriptName} from ${repoUrl}`);
          const installContent = await this.downloadFileFromRepo(repoUrl, `install/${installScriptName}`, branch);
          const localInstallPath = join(this.scriptsDirectory, 'install', installScriptName);
          await writeFile(localInstallPath, installContent, 'utf-8');
          files.push(`install/${installScriptName}`);
          console.log(`Successfully downloaded: install/${installScriptName}`);
        } catch (error) {
          // Install script might not exist, that's okay
          console.log(`Install script not found: install/${installScriptName}`);
        }
      }

      // Download alpine install script if alpine variant exists (only for CT/LXC scripts)
      const hasAlpineCtVariant = hasCtScript &&
        script.install_methods?.some(method => method.type === 'alpine');
      console.log(`[${script.slug}] Checking for alpine variant:`, {
        hasAlpineCtVariant,
        installMethods: script.install_methods?.map(m => ({ type: m.type }))
      });

      if (hasAlpineCtVariant) {
        const alpineInstallScriptName = `${this.normalizeAlpineSlug(script.slug)}-install.sh`;
        try {
          console.log(`[${script.slug}] Downloading alpine install script: install/${alpineInstallScriptName} from ${repoUrl}`);
          const alpineInstallContent = await this.downloadFileFromRepo(repoUrl, `install/${alpineInstallScriptName}`, branch);
          const localAlpineInstallPath = join(this.scriptsDirectory, 'install', alpineInstallScriptName);
          await writeFile(localAlpineInstallPath, alpineInstallContent, 'utf-8');
          files.push(`install/${alpineInstallScriptName}`);
          console.log(`[${script.slug}] Successfully downloaded: install/${alpineInstallScriptName}`);
        } catch (error) {
          // Alpine install script might not exist, that's okay
          console.error(`[${script.slug}] Alpine install script not found or error: install/${alpineInstallScriptName}`, error);
          if (error instanceof Error) {
            console.error(`[${script.slug}] Error details:`, error.message, error.stack);
          }
        }
      } else {
        console.log(`[${script.slug}] No alpine CT variant found, skipping alpine install script download`);
      }

      return {
        success: files.length > 0,
        message: files.length > 0
          ? `Successfully loaded ${files.length} script(s) for ${script.name}`
          : `No script files found in repository for ${script.name}. The script may not be available yet.`,
        files,
        ...(files.length === 0 && methodErrors.length > 0
          ? { error: `No script files could be downloaded for ${script.name}: ${methodErrors.join('; ')}` }
          : {}),
      };
    } catch (error) {
      console.error('Error loading script:', error);
      const errorMsg = error instanceof Error ? error.message : 'Failed to load script';
      return {
        success: false,
        message: errorMsg.includes('404')
          ? `Script "${script.name}" not found in repository. It may be a new script that hasn't been published yet.`
          : errorMsg,
        files: []
      };
    }
  }

  /**
   * Check if a script is downloaded
   * @param {import('~/types/script').Script} script - The script to check
   * @returns {Promise<boolean>}
   */
  async isScriptDownloaded(script) {
    if (!script.install_methods?.length) {
      // No install methods on record (common for PocketBase "pve"/"addon" tool
      // scripts) — fall back to the type-derived path used by loadScript's own
      // fallback download, instead of always reporting "not downloaded".
      const fallbackScriptPath = this.deriveScriptPath(script.type, 'default', script.slug);
      const fallbackFileName = fallbackScriptPath?.split('/').pop();
      if (!fallbackScriptPath || !fallbackFileName) return false;
      const fallbackSubDir = fallbackScriptPath.includes('/')
        ? fallbackScriptPath.substring(0, fallbackScriptPath.lastIndexOf('/'))
        : '';
      const fallbackPath = join(this.scriptsDirectory, fallbackSubDir, fallbackFileName);
      try {
        await import('fs/promises').then(fs => fs.readFile(fallbackPath, 'utf8'));
        return true;
      } catch {
        return false;
      }
    }

    // Check if ALL script files are downloaded
    for (const method of script.install_methods) {
      const scriptPath = this.resolveScriptPath(script, method);
      if (scriptPath) {
        const fileName = scriptPath.split('/').pop();

        if (fileName) {
          // Determine target directory based on script path
          let targetDir;
          let finalTargetDir;
          let filePath;

          if (scriptPath.startsWith('ct/')) {
            targetDir = 'ct';
            finalTargetDir = targetDir;
            filePath = join(this.scriptsDirectory, targetDir, fileName);
          } else if (scriptPath.startsWith('tools/')) {
            targetDir = 'tools';
            const subPath = scriptPath.replace('tools/', '');
            const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
            finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
            filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
          } else if (scriptPath.startsWith('vm/')) {
            targetDir = 'vm';
            const subPath = scriptPath.replace('vm/', '');
            const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
            finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
            filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
          } else {
            targetDir = 'ct';
            finalTargetDir = targetDir;
            filePath = join(this.scriptsDirectory, targetDir, fileName);
          }

          try {
            await import('fs/promises').then(fs => fs.readFile(filePath, 'utf8'));
            // File exists, continue checking other methods
          } catch {
            // File doesn't exist, script is not fully downloaded
            return false;
          }
        }
      }
    }

    // All files exist, script is downloaded
    return true;
  }

  /**
   * Check which script files exist locally
   * @param {import('~/types/script').Script} script - The script to check
   * @returns {Promise<{ctExists: boolean, installExists: boolean, files: string[]}>}
   */
  async checkScriptExists(script) {
    this.initializeConfig();
    const files = [];
    let ctExists = false;
    let installExists = false;

    try {
      // Check scripts based on their install methods
      if (script.install_methods?.length) {
        for (const method of script.install_methods) {
          const scriptPath = this.resolveScriptPath(script, method);
          if (scriptPath) {
            const fileName = scriptPath.split('/').pop();

            if (fileName) {
              let targetDir;
              let finalTargetDir;
              let filePath;

              if (scriptPath.startsWith('ct/')) {
                targetDir = 'ct';
                finalTargetDir = targetDir;
                filePath = join(this.scriptsDirectory, targetDir, fileName);
              } else if (scriptPath.startsWith('tools/')) {
                targetDir = 'tools';
                // Preserve subdirectory structure for tools scripts
                const subPath = scriptPath.replace('tools/', '');
                const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
                finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
                filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
              } else if (scriptPath.startsWith('vm/')) {
                targetDir = 'vm';
                // Preserve subdirectory structure for VM scripts
                const subPath = scriptPath.replace('vm/', '');
                const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
                finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
                filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
              } else {
                targetDir = 'ct'; // Default fallback
                finalTargetDir = targetDir;
                filePath = join(this.scriptsDirectory, targetDir, fileName);
              }

              try {
                await access(filePath);
                files.push(`${finalTargetDir}/${fileName}`);
                ctExists = true;
              } catch {
                // File doesn't exist
              }
            }
          }
        }
      }

      // Fallback: if install_methods is empty, still check for the main script
      // file at its type-derived path (may have been downloaded via loadScript's
      // own fallback path, which applies to every type, not just CT/LXC).
      const chkTypeNorm = (script.type || 'ct').toLowerCase();
      const hasCtScript = chkTypeNorm === 'ct' || chkTypeNorm === 'lxc';
      if (!script.install_methods || script.install_methods.length === 0) {
        const fallbackScriptPath = this.deriveScriptPath(script.type, 'default', script.slug);
        const fallbackFileName = fallbackScriptPath?.split('/').pop();
        if (fallbackScriptPath && fallbackFileName) {
          const fallbackSubDir = fallbackScriptPath.includes('/')
            ? fallbackScriptPath.substring(0, fallbackScriptPath.lastIndexOf('/'))
            : '';
          const fallbackPath = join(this.scriptsDirectory, fallbackSubDir, fallbackFileName);
          try {
            await access(fallbackPath);
            files.push(fallbackScriptPath);
            ctExists = true;
          } catch {
            // File doesn't exist
          }
        }
      }

      // Check for install script for CT/LXC scripts
      if (hasCtScript) {
        const installScriptName = `${script.slug}-install.sh`;
        const installPath = join(this.scriptsDirectory, 'install', installScriptName);

        try {
          await access(installPath);
          files.push(`install/${installScriptName}`);
          installExists = true;
        } catch {
          // Install script doesn't exist
        }
      }

      // Check alpine install script if alpine variant exists (only for CT/LXC scripts)
      const hasAlpineCtVariant = hasCtScript &&
        script.install_methods?.some(method => method.type === 'alpine');
      if (hasAlpineCtVariant) {
        const alpineInstallScriptName = `${this.normalizeAlpineSlug(script.slug)}-install.sh`;
        const alpineInstallPath = join(this.scriptsDirectory, 'install', alpineInstallScriptName);

        try {
          await access(alpineInstallPath);
          files.push(`install/${alpineInstallScriptName}`);
          installExists = true; // Mark as exists if alpine install script exists
        } catch {
          // File doesn't exist
        }
      }

      return { ctExists, installExists, files };
    } catch (error) {
      console.error('Error checking script existence:', error);
      return { ctExists: false, installExists: false, files: [] };
    }
  }

  /**
   * Delete a script's local files
   * @param {import('~/types/script').Script} script - The script to delete
   * @returns {Promise<{success: boolean, message: string, deletedFiles: string[]}>}
   */
  async deleteScript(script) {
    this.initializeConfig();
    const deletedFiles = [];

    try {
      // Get the list of files that exist for this script
      const fileCheck = await this.checkScriptExists(script);

      if (fileCheck.files.length === 0) {
        return {
          success: false,
          message: 'No script files found to delete',
          deletedFiles: []
        };
      }

      // Delete all files
      for (const filePath of fileCheck.files) {
        try {
          const fullPath = join(this.scriptsDirectory, filePath);
          await unlink(fullPath);
          deletedFiles.push(filePath);
        } catch (error) {
          // Log error but continue deleting other files
          console.error(`Error deleting file ${filePath}:`, error);
        }
      }

      if (deletedFiles.length === 0) {
        return {
          success: false,
          message: 'Failed to delete any script files',
          deletedFiles: []
        };
      }

      return {
        success: true,
        message: `Successfully deleted ${deletedFiles.length} file(s) for ${script.name}`,
        deletedFiles
      };
    } catch (error) {
      console.error('Error deleting script:', error);
      return {
        success: false,
        message: error instanceof Error ? error.message : 'Failed to delete script',
        deletedFiles
      };
    }
  }

  /**
   * Compare local script content with remote
   * @param {import('~/types/script').Script} script - The script to compare
   * @returns {Promise<{hasDifferences: boolean, differences: string[], error?: string}>}
   */
  async compareScriptContent(script) {
    this.initializeConfig();
    /** @type {string[]} */
    const differences = [];
    let hasDifferences = false;
    const repoUrl = this.getRepoUrlForScript(script);
    const branch = process.env.REPO_BRANCH || 'main';

    try {
      // First check if any local files exist
      const localFilesExist = await this.checkScriptExists(script);
      if (!localFilesExist.ctExists && !localFilesExist.installExists) {
        // No local files exist, so no comparison needed
        return { hasDifferences: false, differences: [] };
      }

      // If we have local files, proceed with comparison
      // Use Promise.all to run comparisons in parallel
      const comparisonPromises = [];

      // Compare scripts only if they exist locally
      if (localFilesExist.ctExists && script.install_methods?.length) {
        for (const method of script.install_methods) {
          const scriptPath = this.resolveScriptPath(script, method);
          if (scriptPath) {
            const fileName = scriptPath.split('/').pop();

            if (fileName) {
              let targetDir;
              let finalTargetDir;

              if (scriptPath.startsWith('ct/')) {
                targetDir = 'ct';
                finalTargetDir = targetDir;
              } else if (scriptPath.startsWith('tools/')) {
                targetDir = 'tools';
                // Preserve subdirectory structure for tools scripts
                const subPath = scriptPath.replace('tools/', '');
                const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
                finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
              } else if (scriptPath.startsWith('vm/')) {
                targetDir = 'vm';
                // Preserve subdirectory structure for VM scripts
                const subPath = scriptPath.replace('vm/', '');
                const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
                finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
              } else {
                continue; // Skip unknown script types
              }

              comparisonPromises.push(
                this.compareSingleFile(script, scriptPath, `${finalTargetDir}/${fileName}`)
                  .then(result => {
                    if (result.error) {
                      console.error(`[Comparison] Error comparing ${result.filePath}: ${result.error}`);
                    }
                    if (result.hasDifferences) {
                      hasDifferences = true;
                      differences.push(result.filePath);
                    }
                  })
                  .catch((error) => {
                    console.error(`[Comparison] Promise error for ${scriptPath}:`, error);
                  })
              );
            }
          }
        }
      }

      // Compare install script only if it exists locally
      if (localFilesExist.installExists) {
        const installScriptName = `${script.slug}-install.sh`;
        const installScriptPath = `install/${installScriptName}`;

        comparisonPromises.push(
          this.compareSingleFile(script, installScriptPath, installScriptPath)
            .then(result => {
              if (result.error) {
                console.error(`[Comparison] Error comparing ${result.filePath}: ${result.error}`);
              }
              if (result.hasDifferences) {
                hasDifferences = true;
                differences.push(result.filePath);
              }
            })
            .catch((error) => {
              console.error(`[Comparison] Promise error for ${installScriptPath}:`, error);
            })
        );
      }

      // Compare alpine install script if alpine variant exists (only for CT/LXC scripts)
      const cmpTypeNorm = (script.type || 'ct').toLowerCase();
      const hasAlpineCtVariant = (cmpTypeNorm === 'ct' || cmpTypeNorm === 'lxc') &&
        script.install_methods?.some(method => method.type === 'alpine');
      if (hasAlpineCtVariant) {
        const alpineInstallScriptName = `${this.normalizeAlpineSlug(script.slug)}-install.sh`;
        const alpineInstallScriptPath = `install/${alpineInstallScriptName}`;
        const localAlpineInstallPath = join(this.scriptsDirectory, alpineInstallScriptPath);

        // Check if alpine install script exists locally
        try {
          await access(localAlpineInstallPath);
          comparisonPromises.push(
            this.compareSingleFile(script, alpineInstallScriptPath, alpineInstallScriptPath)
              .then(result => {
                if (result.error) {
                  console.error(`[Comparison] Error comparing ${result.filePath}: ${result.error}`);
                }
                if (result.hasDifferences) {
                  hasDifferences = true;
                  differences.push(result.filePath);
                }
              })
              .catch((error) => {
                console.error(`[Comparison] Promise error for ${alpineInstallScriptPath}:`, error);
              })
          );
        } catch {
          // Alpine install script doesn't exist locally, skip comparison
        }
      }

      // Wait for all comparisons to complete
      await Promise.all(comparisonPromises);

      console.log(`[Comparison] Completed comparison for ${script.slug}: hasDifferences=${hasDifferences}, differences=${differences.length}`);
      return { hasDifferences, differences };
    } catch (/** @type {any} */ error) {
      console.error(`[Comparison] Error comparing script content for ${script.slug}:`, error);
      return { hasDifferences: false, differences: [], error: error.message };
    }
  }

  /**
   * Compare a single file with remote
   * @param {import('~/types/script').Script} script - The script object
   * @param {string} remotePath - The remote file path
   * @param {string} filePath - The local file path
   * @returns {Promise<{hasDifferences: boolean, filePath: string, error?: string}>}
   */
  async compareSingleFile(script, remotePath, filePath) {
    try {
      const localPath = join(this.scriptsDirectory, filePath);
      const repoUrl = this.getRepoUrlForScript(script);
      const branch = process.env.REPO_BRANCH || 'main';

      console.log(`[Comparison] Comparing ${filePath} from ${repoUrl} (branch: ${branch})`);

      // Read local content
      const localContent = await readFile(localPath, 'utf-8');
      console.log(`[Comparison] Local file size: ${localContent.length} bytes`);

      // Download remote content from the script's repository
      const remoteContent = await this.downloadFileFromRepo(repoUrl, remotePath, branch);
      console.log(`[Comparison] Remote file size: ${remoteContent.length} bytes`);

      // Apply modification only for CT scripts, not for other script types
      let modifiedRemoteContent;
      if (remotePath.startsWith('ct/')) {
        modifiedRemoteContent = this.modifyScriptContent(remoteContent);
        console.log(`[Comparison] Applied CT script modifications`);
      } else {
        modifiedRemoteContent = remoteContent; // Don't modify tools or vm scripts
      }

      // Compare content
      const hasDifferences = localContent !== modifiedRemoteContent;

      if (hasDifferences) {
        console.log(`[Comparison] Differences found in ${filePath}`);
      } else {
        console.log(`[Comparison] No differences in ${filePath}`);
      }

      return { hasDifferences, filePath };
    } catch (/** @type {any} */ error) {
      console.error(`[Comparison] Error comparing file ${filePath}:`, error.message);
      // Return error information so it can be handled upstream
      return { hasDifferences: false, filePath, error: error.message };
    }
  }

  /**
   * Get diff between local and remote script
   * @param {import('~/types/script').Script} script - The script object
   * @param {string} filePath - The file path to diff
   * @returns {Promise<{diff: string|null, localContent: string|null, remoteContent: string|null}>}
   */
  async getScriptDiff(script, filePath) {
    this.initializeConfig();
    try {
      const repoUrl = this.getRepoUrlForScript(script);
      const branch = process.env.REPO_BRANCH || 'main';
      let localContent = null;
      let remoteContent = null;

      if (filePath.startsWith('ct/')) {
        // Handle CT script
        const fileName = filePath.split('/').pop();
        if (fileName) {
          const localPath = join(this.scriptsDirectory, 'ct', fileName);
          try {
            localContent = await readFile(localPath, 'utf-8');
          } catch {
            // Error reading local CT script
          }

          try {
            // Find the corresponding script path in install_methods
            const method = script.install_methods?.find(
              m => this.resolveScriptPath(script, m) === filePath
            );
            if (method) {
              const resolvedPath = this.resolveScriptPath(script, method);
              if (resolvedPath) {
                const downloadedContent = await this.downloadFileFromRepo(repoUrl, resolvedPath, branch);
                remoteContent = this.modifyScriptContent(downloadedContent);
              }
            }
          } catch {
            // Error downloading remote CT script
          }
        }
      } else if (filePath.startsWith('install/')) {
        // Handle install script
        const localPath = join(this.scriptsDirectory, filePath);
        try {
          localContent = await readFile(localPath, 'utf-8');
        } catch {
          // Error reading local install script
        }

        try {
          remoteContent = await this.downloadFileFromRepo(repoUrl, filePath, branch);
        } catch {
          // Error downloading remote install script
        }
      }

      if (!localContent || !remoteContent) {
        return { diff: null, localContent, remoteContent };
      }

      // Generate diff using a simple line-by-line comparison
      const diff = this.generateDiff(localContent, remoteContent);
      return { diff, localContent, remoteContent };
    } catch (error) {
      console.error('Error getting script diff:', error);
      return { diff: null, localContent: null, remoteContent: null };
    }
  }

  /**
   * Generate a simple line-by-line diff
   * @param {string} localContent - The local file content
   * @param {string} remoteContent - The remote file content
   * @returns {string}
   */
  generateDiff(localContent, remoteContent) {
    const localLines = localContent.split('\n');
    const remoteLines = remoteContent.split('\n');

    let diff = '';
    let i = 0;
    let j = 0;

    while (i < localLines.length || j < remoteLines.length) {
      const localLine = localLines[i];
      const remoteLine = remoteLines[j];

      if (i >= localLines.length) {
        // Only remote lines left
        diff += `+${j + 1}: ${remoteLine}\n`;
        j++;
      } else if (j >= remoteLines.length) {
        // Only local lines left
        diff += `-${i + 1}: ${localLine}\n`;
        i++;
      } else if (localLine === remoteLine) {
        // Lines are the same
        diff += ` ${i + 1}: ${localLine}\n`;
        i++;
        j++;
      } else {
        // Lines are different - find the best match
        let found = false;
        for (let k = j + 1; k < Math.min(j + 10, remoteLines.length); k++) {
          if (localLine === remoteLines[k]) {
            // Found match in remote, local line was removed
            for (let l = j; l < k; l++) {
              diff += `+${l + 1}: ${remoteLines[l]}\n`;
            }
            diff += ` ${i + 1}: ${localLine}\n`;
            i++;
            j = k + 1;
            found = true;
            break;
          }
        }

        if (!found) {
          for (let k = i + 1; k < Math.min(i + 10, localLines.length); k++) {
            if (remoteLine === localLines[k]) {
              // Found match in local, remote line was added
              diff += `-${i + 1}: ${localLine}\n`;
              for (let l = i + 1; l < k; l++) {
                diff += `-${l + 1}: ${localLines[l]}\n`;
              }
              diff += `+${j + 1}: ${remoteLine}\n`;
              i = k + 1;
              j++;
              found = true;
              break;
            }
          }
        }

        if (!found) {
          // No match found, lines are different
          diff += `-${i + 1}: ${localLine}\n`;
          diff += `+${j + 1}: ${remoteLine}\n`;
          i++;
          j++;
        }
      }
    }

    return diff;
  }
}

export const scriptDownloaderService = new ScriptDownloaderService();

