// SPDX-License-Identifier: Apache-2.0
import fs from 'node:fs';
import * as helpers from '../helpers.js';
import { Templates } from '../templates.js';
import { ShellRunner } from '../shell-runner.js';
import { MissingArgumentError } from '../errors/missing-argument-error.js';
import { SoloError } from '../errors/solo-error.js';
import { PathEx } from '../../business/utils/path-ex.js';
import { OperatingSystem } from '../../business/utils/operating-system.js';
import path from 'node:path';
import { SemanticVersion } from '../../business/utils/semantic-version.js';
/**
 * Base class for dependency managers that download and manage CLI tools
 * Common functionality for downloading, checking versions, and managing executables
 */
export class BaseDependencyManager extends ShellRunner {
    downloader;
    installationDirectory;
    requiredVersion;
    downloadBaseUrl;
    osArch;
    localExecutableWithPath;
    globalExecutablePath = '';
    artifactName;
    downloadURL;
    checksumURL;
    executableName;
    constructor(downloader, installationDirectory, osArch, requiredVersion, dependencyName, downloadBaseUrl) {
        super();
        this.downloader = downloader;
        this.installationDirectory = installationDirectory;
        this.requiredVersion = requiredVersion;
        this.downloadBaseUrl = downloadBaseUrl;
        if (!installationDirectory) {
            throw new MissingArgumentError('installation directory is required');
        }
        if (!downloader) {
            throw new MissingArgumentError('package downloader is required');
        }
        // Normalize architecture naming - many tools use 'amd64' instead of 'x64'
        this.osArch = ['x64', 'x86-64'].includes(osArch) ? 'amd64' : osArch;
        // Set the path to the local installation
        this.localExecutableWithPath = Templates.localInstallationExecutableForDependency(dependencyName, installationDirectory);
        this.executableName = path.basename(this.localExecutableWithPath);
        // Set artifact name and URLs - these will be overridden by child classes
        this.artifactName = this.getArtifactName();
        this.downloadURL = this.getDownloadURL();
        this.checksumURL = this.getChecksumURL();
    }
    getArch() {
        let arch = this.osArch;
        if (arch === 'x64') {
            arch = 'amd64';
        }
        else if (arch === 'arm64' || arch === 'aarch64') {
            arch = 'arm64';
        }
        return arch;
    }
    /**
     * Get the executable to run
     */
    async getExecutable() {
        return this.executableName;
    }
    /**
     * Find the global executable by scanning PATH directories directly in Node.js.
     * This avoids spawning a shell subprocess (which, command -v, where) whose
     * behaviour varies across shells and CI runner environments.
     */
    getGlobalExecutableWithPath() {
        if (this.globalExecutablePath) {
            return this.globalExecutablePath;
        }
        const executableNames = OperatingSystem.isWin32()
            ? [`${this.executableName}.exe`, `${this.executableName}.cmd`, this.executableName]
            : [this.executableName];
        const pathDirectories = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean);
        this.logger.debug(`Searching PATH for ${this.executableName}: [${pathDirectories.join(', ')}]`);
        for (const directory of pathDirectories) {
            for (const name of executableNames) {
                const candidate = path.join(directory, name);
                try {
                    // On Windows X_OK is not supported and silently degrades to F_OK;
                    // executability is determined by file extension (.exe/.cmd) already.
                    fs.accessSync(candidate, OperatingSystem.isWin32() ? fs.constants.F_OK : fs.constants.X_OK);
                    this.logger.debug(`Found ${this.executableName} at ${candidate}`);
                    this.globalExecutablePath = candidate;
                    return candidate;
                }
                catch {
                    // not found or not executable in this directory — continue
                }
            }
        }
        this.logger.warn(`${this.executableName} was not found in PATH`);
        return false;
    }
    /**
     * Check if the given installation meets version requirements
     */
    async installationMeetsRequirements(executableWithPath) {
        let version;
        try {
            version = await this.getVersion(executableWithPath);
        }
        catch (error) {
            this.logger.debug(`Failed to get version for ${this.executableName} at ${executableWithPath}: ${error instanceof Error ? error.message : error}`);
            return false;
        }
        if (new SemanticVersion(version).greaterThanOrEqual(this.getRequiredVersion())) {
            return true;
        }
        this.logger.info(`Found version ${version} of ${this.executableName} at ${executableWithPath}, which does not meet the required version ${this.getRequiredVersion()}`);
        return false;
    }
    /**
     * Check if the tool is installed globally and meets requirements
     */
    async isInstalledGloballyAndMeetsRequirements() {
        const path = this.getGlobalExecutableWithPath();
        try {
            if (path && (await this.installationMeetsRequirements(path))) {
                return true;
            }
            else {
                this.logger.info(`${this.executableName}${path ? ` at ${path}` : ''} is not a compatible global installation`);
            }
        }
        catch (error) {
            this.logger.debug(`Global installation of ${this.executableName} does not meet version requirements: ${error instanceof Error ? error.message : error}`);
        }
        return false;
    }
    /**
     * Check if the tool is installed locally and meets requirements
     */
    async isInstalledLocallyAndMeetsRequirements() {
        try {
            if (!this.isInstalledLocally()) {
                this.logger.info(`${this.executableName} is not installed locally at ${this.localExecutableWithPath}`);
                return false;
            }
            if (await this.installationMeetsRequirements(this.localExecutableWithPath)) {
                return true;
            }
            this.logger.info(`${this.executableName} at ${this.localExecutableWithPath} is installed locally but does not meet version requirements`);
        }
        catch (error) {
            this.logger.debug(`Local installation of ${this.executableName} does not meet version requirements: ${error instanceof Error ? error.message : error}`);
        }
        return false;
    }
    /**
     * Check if the tool is installed locally
     */
    isInstalledLocally() {
        return fs.existsSync(this.localExecutableWithPath);
    }
    /**
     * Uninstall the local version
     */
    uninstallLocal() {
        if (this.isInstalledLocally()) {
            fs.rmSync(this.localExecutableWithPath);
        }
    }
    /**
     * Hook for any pre-installation steps
     */
    async preInstall() { }
    /**
     * Hook to determine if installation should proceed
     * Child classes can override this for custom logic
     */
    async shouldInstall() {
        return true;
    }
    /**
     * Determine if checksum verification should be performed
     * Child classes can override this if needed
     */
    getVerifyChecksum() {
        return true;
    }
    /**
     * Install the tool
     */
    async install(temporaryDirectory = helpers.getTemporaryDirectory()) {
        if (this.installationDirectory === temporaryDirectory) {
            throw new SoloError('Installation directory cannot be the same as temporary directory');
        }
        if (!(await this.shouldInstall())) {
            this.logger.debug(`Skipping installation of ${this.executableName}`);
            return true;
        }
        await this.preInstall();
        // Check if it is already installed locally
        if (await this.isInstalledLocallyAndMeetsRequirements()) {
            const localVersion = await this.getVersion(this.localExecutableWithPath).catch(() => this.getRequiredVersion());
            this.logger.showUser(`Compatible ${this.executableName} v${localVersion} found at ${this.localExecutableWithPath}`);
            return true;
        }
        // If it is installed globally and meets requirements, use the global installation
        if (await this.isInstalledGloballyAndMeetsRequirements()) {
            const globalVersion = await this.getVersion(this.globalExecutablePath).catch(() => this.getRequiredVersion());
            this.logger.showUser(`Compatible ${this.executableName} v${globalVersion} found at ${this.globalExecutablePath}`);
            return true;
        }
        // If not installed, download and install
        this.logger.showUser(`Compatible ${this.executableName} ${this.getRequiredVersion()} was not found locally or globally. ` +
            `Downloading and installing it into ${this.installationDirectory}...`);
        this.logger.debug(`Downloading and installing ${this.executableName} executable...`);
        const packageFile = await this.downloader.fetchPackage(this.getDownloadURL(), this.getChecksumURL(), temporaryDirectory, this.getVerifyChecksum());
        const processedFiles = await this.processDownloadedPackage(packageFile, temporaryDirectory);
        if (!fs.existsSync(this.installationDirectory)) {
            fs.mkdirSync(this.installationDirectory, { recursive: true });
        }
        // In case there is an existing local installation, which did not meet the requirements - remove it
        this.uninstallLocal();
        try {
            for (const processedFile of processedFiles) {
                const fileName = path.basename(processedFile);
                const localExecutable = PathEx.join(this.installationDirectory, fileName);
                fs.cpSync(processedFile, localExecutable);
                fs.chmodSync(localExecutable, 0o755);
            }
        }
        catch (error) {
            throw new SoloError(`Failed to install ${this.executableName}: ${error.message}`);
        }
        this.logger.showUser(`Installed ${this.executableName} ${this.getRequiredVersion()} into ${this.installationDirectory}.`);
        return this.isInstalledLocally();
    }
    /**
     * Get the tool's required version
     */
    getRequiredVersion() {
        return this.requiredVersion;
    }
    /**
     * Hook for setting up any configuration after installation
     * Child classes can override this if needed
     */
    setupConfig() { }
}
//# sourceMappingURL=base-dependency-manager.js.map