package org.implab.gradle.common.sources;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;

import javax.inject.Inject;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.Action;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.Named;
import org.implab.gradle.common.core.lang.Closures;

import groovy.lang.Closure;
import groovy.lang.DelegatesTo;

/**
 * One artifact representation slot inside {@link VariantArtifact}.
 *
 * <p>
 * The DSL exposed by this type is topology-aware sugar over an internal
 * contribution model:
 * <ul>
 * <li>{@link #from(Object)} adds one direct contribution that does not depend
 * on {@link VariantSourcesExtension} bindings;</li>
 * <li>{@link #fromVariant(Action)}, {@link #fromRole(String, Action)} and
 * {@link #fromLayer(String, Action)} define where a contribution is active in
 * the variant/role/layer topology;</li>
 * <li>{@link OutputSelectionSpec#output(String)} defines which named output of
 * the matched {@link GenericSourceSet} should be added to the slot.</li>
 * </ul>
 *
 * <p>
 * Internally the slot stores contribution resolvers rather than raw output
 * names. Each contribution can later materialize itself against the
 * variant-specific source-set bindings and return:
 * <ul>
 * <li>a file notation object suitable for {@code files.from(...)}</li>
 * <li>a {@link BindingKey} used to deduplicate repeated logical inputs during
 * materialization</li>
 * </ul>
 *
 * <p>
 * Validation is intentionally separated from materialization: the slot keeps
 * topology references in {@link #referencedRoleNames()} and
 * {@link #referencedLayerNames()}, while the actual contribution pipeline is
 * exposed through {@link #bindings()}.
 */
@NonNullByDefault
public class VariantArtifactSlot implements Named {
    private final String name;
    private final List<BindingResolver> bindings = new ArrayList<>();
    private final Set<String> referencedRoleNames = new LinkedHashSet<>();
    private final Set<String> referencedLayerNames = new LinkedHashSet<>();
    private boolean finalized;

    @Inject
    public VariantArtifactSlot(String name) {
        this.name = VariantArtifact.normalize(name, "slot name must not be null or blank");
    }

    @Override
    public String getName() {
        return name;
    }

    public void fromVariant(Action<? super OutputSelectionSpec> configure) {
        addContributions(context -> true, configure);
    }

    public void fromVariant(
            @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
        fromVariant(Closures.action(configure));
    }

    public void fromRole(String roleName, Action<? super OutputSelectionSpec> configure) {
        var normalizedRoleName = VariantArtifact.normalize(roleName, "role name must not be null or blank");
        addContributions(context -> context.roleName().equals(normalizedRoleName), configure);
        referencedRoleNames.add(normalizedRoleName);
    }

    public void fromRole(
            String roleName,
            @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
        fromRole(roleName, Closures.action(configure));
    }

    public void fromLayer(String layerName, Action<? super OutputSelectionSpec> configure) {
        var normalizedLayerName = VariantArtifact.normalize(layerName, "layer name must not be null or blank");
        addContributions(context -> context.layerName().equals(normalizedLayerName), configure);
        referencedLayerNames.add(normalizedLayerName);
    }

    public void fromLayer(
            String layerName,
            @DelegatesTo(value = OutputSelectionSpec.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
        fromLayer(layerName, Closures.action(configure));
    }

    /**
     * Adds one direct slot contribution.
     *
     * <p>The supplied object is forwarded as-is to {@code files.from(...)}
     * during slot materialization and does not depend on
     * {@link VariantSourcesExtension#whenBound(Action)} callbacks.
     */
    public void from(Object files) {
        ensureMutable("configure sources");

        if (files == null)
            throw new InvalidUserDataException("slot source must not be null");

        var key = BindingKey.newUniqueKey("direct slot input for '" + name + "'");
        bindings.add((contexts, consumer) -> consumer.accept(new ResolvedBinding(key, files)));
    }

    List<BindingResolver> bindings() {
        return List.copyOf(bindings);
    }

    void acceptBindings(Consumer<? super BindingResolver> consumer) {
        bindings.forEach(consumer);
    }

    Set<String> referencedRoleNames() {
        return Set.copyOf(referencedRoleNames);
    }

    Set<String> referencedLayerNames() {
        return Set.copyOf(referencedLayerNames);
    }

    void finalizeModel() {
        finalized = true;
    }

    private void addContributions(
            Predicate<SourceSetUsageBinding> selector,
            Action<? super OutputSelectionSpec> configure) {
        ensureMutable("configure sources");

        var spec = new OutputSelectionSpec(selector);
        configure.execute(spec);
        spec.accept(bindings::add);
    }

    private void ensureMutable(String operation) {
        if (finalized)
            throw new InvalidUserDataException(
                    "Variant artifact slot '" + name + "' is finalized and cannot " + operation);
    }

    /**
     * Local DSL buffer for one {@code fromVariant/fromRole/fromLayer} block.
     *
     * <p>
     * The spec accumulates contributions locally and flushes them to the
     * owning slot only after the outer configure block completes successfully.
     */
    public final class OutputSelectionSpec {
        private final Predicate<SourceSetUsageBinding> selector;
        private final List<BindingResolver> bindings = new ArrayList<>();

        private OutputSelectionSpec(Predicate<SourceSetUsageBinding> selector) {
            this.selector = selector;
        }

        public void output(String name) {
            var outputName = VariantArtifact.normalize(name, "output name must not be null or blank");
            bindings.add((contexts, consumer) -> contexts.stream()
                    .filter(selector)
                    .map(context -> resolveOutput(context, outputName))
                    .forEach(consumer));
        }

        public void output(String name, String... extra) {
            Stream.concat(Stream.of(name), Stream.of(extra))
                    .forEach(this::output);
        }

        private ResolvedBinding resolveOutput(SourceSetUsageBinding context, String outputName) {
            var key = new SourceSetOutputKey(context.sourceSetName(), outputName);
            var files = context.sourceSet().map(sourceSet -> sourceSet.output(outputName));
            return new ResolvedBinding(key, files);
        }

        void accept(Consumer<? super BindingResolver> consumer) {
            bindings.forEach(consumer);
        }
    }

    @FunctionalInterface
    interface BindingResolver {
        void resolve(Collection<SourceSetUsageBinding> contexts, Consumer<? super ResolvedBinding> consumer);
    }

    /**
     * Materialized slot contribution for one concrete source-set binding.
     */
    record ResolvedBinding(BindingKey key, Object files) {
    }

    /**
     * Marker key for deduplicating logical slot inputs during materialization.
     *
     * <p>
     * Semantic keys such as {@link SourceSetOutputKey} collapse repeated
     * references to the same logical output. Identity keys created via
     * {@link #newUniqueKey()} or {@link #newUniqueKey(String)} can be used by contributions
     * that must flow through the same pipeline but should never be merged.
     */
    interface BindingKey {
        static BindingKey newUniqueKey(String hint) {
            return new BindingKey() {
                @Override
                public String toString() {
                    return hint;
                }
            };
        }

        static BindingKey newUniqueKey() {
            return newUniqueKey("unnamed");
        }
    }

    /**
     * Stable dedupe key for one named output of one resolved source set.
     */
    record SourceSetOutputKey(String sourceSetName, String outputName) implements BindingKey {
        @Override
        public String toString() {
            return "sourceSet '" + sourceSetName + "' output '" + outputName + "'";
        }
    }
}
