/*
 * 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 org.yaml.snakeyaml.DumperOptions
import org.yaml.snakeyaml.Yaml

/**
 * Represents a module spec (meta.yml).
 *
 * @author Jorge Ejarque <jorge.ejarque@seqera.io>
 */
@Slf4j
@CompileStatic
class ModuleSpec {

    static final String TODO_TYPE = 'TODO: Add type'

    static final String TODO_DESCRIPTION = 'TODO: Add description'

    static final String YAML_HEADER = """\
        # This file was auto-generated by `nextflow module spec`.
        #
        # Review and complete all fields marked "TODO" before publishing.
        #
        """.stripIndent()

    /**
     * Represents a module input/output declaration.
     */
    static class ModuleParam {
        String name
        String type
        String description
        List<ModuleParam> components
        Map<String, Object> _passthrough

        boolean isTuple() {
            return components != null
        }
    }

    String name
    String version
    String description
    List<String> keywords
    String license
    List<String> authors
    List<String> maintainers
    Map<String, String> requires
    List<Map> tools
    List<ModuleParam> inputs
    List<ModuleParam> outputs
    List<ModuleParam> topics
    Map<String, Object> _passthrough

    /**
     * Validate the module spec for required fields
     *
     * @return List of validation errors (empty if valid)
     */
    List<String> validate() {
        final List<String> errors = []

        if( !name )
            errors << "Missing required field: name"

        if( !version )
            errors << "Missing required field: version"

        if( !description )
            errors << "Missing required field: description"

        if( !license )
            errors << "Missing required field: license"

        // Validate version format (semantic versioning)
        if( version && !version.matches(/^\d+\.\d+\.\d+(-[\w.-]+)?$/) )
            errors << "Invalid version format: ${version} (expected semantic versioning, e.g., 1.0.0)".toString()

        // Validate name format (namespace/name or namespace/path/to/name for nested modules)
        if( name && !name.matches(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/) )
            errors << "Invalid module name format: ${name} (expected namespace/name or namespace/path/to/name, e.g., nf-core/fastqc or nf-core/gfatools/gfa2fa)".toString()

        if( inputs ) {
            for( int i = 0; i < inputs.size(); i++ )
                validateModuleParam("input[${i}]", inputs[i], errors)
        }

        if( outputs ) {
            for( int i = 0; i < outputs.size(); i++ )
                validateModuleParam("output[${i}]", outputs[i], errors)
        }

        if( topics ) {
            for( int i = 0; i < topics.size(); i++ )
                validateModuleParam("topic[${i}]", topics[i], errors)
        }

        return errors
    }

    private void validateModuleParam(String name, ModuleParam param, List<String> errors) {
        if( param.isTuple() ) {
            for( int j = 0; j < param.components.size(); j++ )
                validateModuleParam("${name}[${j}]", param.components[j], errors)
            return
        }

        if( !param.type || param.type == TODO_TYPE )
            errors << "Missing type for ${name}${param.name ? " ($param.name)" : ''}".toString()

        if( !param.description || param.description == TODO_DESCRIPTION )
            errors << "Missing description for ${name}${param.name ? " ($param.name)" : ''}".toString()
    }

    /**
     * Check if the spec is valid
     *
     * @return true if valid, false otherwise
     */
    boolean isValid() {
        return validate().isEmpty()
    }

    /**
     * Render the module spec to YAML.
     */
    String toYaml() {
        final spec = asMap()
        final errors = validate()

        final dumperOpts = new DumperOptions()
        dumperOpts.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK)
        dumperOpts.setIndent(2)
        dumperOpts.setPrettyFlow(true)

        final sb = new StringBuilder()
        sb.append(YAML_HEADER)
        sb.append('\n')
        if( errors ) {
            sb.append('# TODO:\n')
            for( final err : errors )
                sb.append("#    - ${err}\n".toString())
            sb.append('#\n\n')
        }
        sb.append(new Yaml(dumperOpts).dump(spec))
        return sb.toString()
    }

    /**
     * Convert the module spec to a map.
     */
    Map<String, Object> asMap() {
        final result = new LinkedHashMap<String, Object>()
        result['$schema'] = 'https://raw.githubusercontent.com/nextflow-io/schemas/refs/heads/main/module/v1/schema.json'
        if( name )
            result['name'] = name
        if( version )
            result['version'] = version
        if( description )
            result['description'] = description
        if( keywords )
            result['keywords'] = keywords
        if( license )
            result['license'] = license
        if( authors )
            result['authors'] = authors
        if( maintainers )
            result['maintainers'] = maintainers
        if( requires )
            result['requires'] = requires
        if( tools )
            result['tools'] = tools
        if( inputs )
            result['input'] = moduleParamsAsList(inputs)
        if( outputs )
            result['output'] = moduleParamsAsList(outputs)
        if( topics )
            result['topics'] = moduleParamsAsList(topics)
        if( _passthrough )
            result.putAll(_passthrough)
        return result
    }

    private static List moduleParamsAsList(List<ModuleParam> params) {
        final result = []
        for( final param : params ) {
            if( param.isTuple() )
                result << param.components.collect { el -> moduleParamAsMap(el) }
            else
                result << moduleParamAsMap(param)
        }
        return result
    }

    private static Map moduleParamAsMap(ModuleParam param) {
        final Map<String,Object> result = [:]
        if( param.name )
            result.name = param.name
        result.type = param.type ?: TODO_TYPE
        result.description = param.description ?: TODO_DESCRIPTION
        if( param._passthrough )
            result.putAll(param._passthrough)
        return result
    }

}
