/*
 * Copyright 2013-2026, Seqera Labs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package nextflow.module

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.seqera.npr.client.RegistryClient

import nextflow.config.RegistryConfig
import nextflow.exception.AbortOperationException

import java.nio.file.Files
import java.nio.file.Path

/**
 * Core module resolution logic that coordinates registry, storage, and version management
 *
 * @author Jorge Ejarque <jorge.ejarque@seqera.io>
 */
@Slf4j
@CompileStatic
class ModuleResolver {

    private final RegistryClient registryClient
    private final ModuleStorage storage

    ModuleResolver(Path baseDir, RegistryClient registryClient) {
        this.registryClient = registryClient
        this.storage = new ModuleStorage(baseDir)
    }

    ModuleResolver(Path baseDir, RegistryConfig registryConfig = null) {
        this(baseDir, RegistryClientFactory.forConfig(registryConfig ?: new RegistryConfig()))

    }

    /**
     * Resolve a module reference to an installed module path
     *
     * @param reference The module reference
     * @param version Optional specific version (null = use config or latest)
     * @param autoInstall Whether to auto-install if not present (default: false)
     * @return Path to the module's main.nf file
     */
    Path resolve(ModuleReference reference, String version = null, boolean autoInstall = false) {

        // Check if module is already installed
        def installed = storage.getInstalledModule(reference)

        if( installed ) {
            // Check integrity
            def integrity = installed.integrity
            if( integrity == ModuleIntegrity.CORRUPTED ) {
                throw new AbortOperationException(
                    "Module ${reference} is corrupted (missing required files). " +
                        "Please remove and reinstall."
                )
            }

            if( integrity == ModuleIntegrity.MODIFIED ) {
                log.warn1 "Module ${reference} has local modifications (checksum mismatch)"
            } else if( integrity == ModuleIntegrity.NO_REMOTE_MODULE ) {
                log.warn1 "Module ${reference} has no registry origin (.module-info missing)"
            }

            // Check if version matches
            if( version && installed.installedVersion != version ) {
                if( autoInstall ) {
                    log.info "Upgrading module ${reference} from ${installed.installedVersion} to ${version}"
                    return installModule(reference, version)
                } else {
                    throw new AbortOperationException(
                        "Module ${reference} version mismatch: " +
                            "installed=${installed.installedVersion}, required=${version}. " +
                            "Run 'nextflow module install ${reference}@${version}' to update."
                    )
                }
            }

            // Module is installed and version matches
            return installed.mainFile
        }

        // Module not installed
        if( autoInstall ) {
            return installModule(reference, version)
        } else {
            throw new AbortOperationException(
                "Module ${reference} is not installed. " +
                    "Run 'nextflow module install ${reference}' to install."
            )
        }
    }

    String resolveVersion(ModuleReference reference) {
        final version = registryClient.getModule(reference.fullName)?.latest?.version
        if( !version ) {
            throw new AbortOperationException("Module ${reference} has no published versions")
        }
        return version
    }

    /**
     * Install or update a module
     *
     * @param reference The module reference
     * @param version Optional specific version (null = latest)
     * @param force Force reinstall even if already installed
     * @return Path to the installed module's main.nf file
     */
    Path installModule(ModuleReference reference, String version = null, boolean force = false) {
        // Check if already installed locally before hitting the registry
        if( storage.isInstalled(reference) ) {
            def installed = storage.getInstalledModule(reference)

            // No specific version requested -- use the local module as-is
            if( !version ) {
                log.debug "Module ${reference}@${installed.installedVersion} is already installed locally"
                return installed.mainFile
            }

            if( installed.installedVersion == version ) {
                log.debug "Module ${reference}@${installed.installedVersion} is already installed (version $version)"
                return installed.mainFile
            }

            // Version mismatch -- check for local modifications before overwriting
            def integrity = installed.integrity
            if( integrity == ModuleIntegrity.MODIFIED && !force ) {
                throw new AbortOperationException(
                    "Module ${reference} has local modifications. " +
                        "Use '-force' to override, or save your changes first."
                )
            }
            if( integrity == ModuleIntegrity.NO_REMOTE_MODULE && !force ) {
                throw new AbortOperationException(
                    "Folder 'modules/${reference}' already exists and is not a valid remote module. " +
                        "Use '-force' to override, or save your changes first."
                )
            }
        }

        if( !version )
            version = resolveVersion(reference)

        log.info "Installing module ${reference}@${version}..."

        // Download module package to temporary location
        Path tempFile = Files.createTempFile("nf-module-", ".tgz")
        try {
            // Download and validate integrity using server checksum
            def downloadUrl = registryClient.downloadModuleRelease(reference.fullName, version, tempFile)

            // Install to modules directory (will compute directory checksum for future integrity checks)
            InstalledModule installed = storage.installModule(reference, version, tempFile, downloadUrl)

            log.info "Module ${reference}@${version} installed successfully at ${installed.mainFile.parent.toAbsolutePath()}"
            return installed.mainFile
        }
        finally {
            // Clean up temporary file
            if( Files.exists(tempFile) ) {
                Files.delete(tempFile)
            }
        }
    }

}
