package org.implab.gradle.variants.artifacts.internal;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.Action;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.Directory;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Sync;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.implab.gradle.common.core.lang.FilePaths;
import org.implab.gradle.variants.artifacts.ArtifactAssembly;
import org.implab.gradle.variants.artifacts.ArtifactAssemblySpec;
import org.implab.gradle.variants.artifacts.ArtifactSlot;
import org.implab.gradle.variants.sources.CompileUnit;
import org.implab.gradle.variants.sources.CompileUnitsView;
import org.implab.gradle.variants.sources.RoleProjectionsView;
import org.implab.gradle.variants.sources.SourceSetMaterializer;

/**
 * Adapts slot contribution declarations to materialized {@link ArtifactAssembly}
 * handles.
 *
 * <p>The handler creates one {@link Sync} task per {@link ArtifactSlot}. The task
 * copies all collected slot inputs into a single output directory. That output
 * directory is then registered in {@link ArtifactAssemblyRegistry} as the
 * published artifact for the slot.
 *
 * <p>Input collection uses {@link SlotContributionVisitor}. Each contribution is
 * converted to a {@link SlotInputKey}; duplicate keys are ignored so that repeated
 * topology-based selections do not add the same input twice.
 */
@NonNullByDefault
public class ArtifactAssemblyHandler {
    private final ObjectFactory objects;

    private final ArtifactAssemblyRegistry assemblyRegistry;

    private final DirectoryProperty assembliesDirectory;

    private final TaskContainer tasks;

    private final CompileUnitsView compileUnitsView;

    private final RoleProjectionsView roleProjectionsView;

    private final SourceSetMaterializer sourceSetMaterializer;

    private final Map<ArtifactSlot, SlotAssembly> slotInputs = new HashMap<>();

    public ArtifactAssemblyHandler(
            ObjectFactory objects,
            TaskContainer tasks,
            ArtifactAssemblyRegistry assemblyRegistry,
            CompileUnitsView compileUnitsView,
            RoleProjectionsView roleProjectionsView,
            SourceSetMaterializer sourceSetMaterializer) {
        this.objects = objects;
        this.tasks = tasks;
        this.assemblyRegistry = assemblyRegistry;
        this.compileUnitsView = compileUnitsView;
        this.roleProjectionsView = roleProjectionsView;
        this.sourceSetMaterializer = sourceSetMaterializer;

        assembliesDirectory = objects.directoryProperty();
    }

    public DirectoryProperty getAssembliesDirectory() {
        return assembliesDirectory;
    }

    public void configureAssembly(ArtifactSlot artifactSlot, Action<? super ArtifactAssemblySpec> action) {
        var visitor = contributionVisitor(artifactSlot);
        var spec = new DefaultArtifactAssemblySpec(objects, c -> c.accept(visitor));
        action.execute(spec);
    }

    public SlotContributionVisitor contributionVisitor(ArtifactSlot artifactSlot) {
        var assembly = slotInputs.computeIfAbsent(artifactSlot, this::createSlotAssembly);
        return new ContributionVisitor(artifactSlot, assembly);
    }

    /**
     * Creates the assembly task for the given slot and registers its output artifact.
     */
    private SlotAssembly createSlotAssembly(ArtifactSlot artifactSlot) {
        var assembly = new SlotAssembly();
        var fileCollection = assembly.inputs();

        var outputDirectory = outputDirectory(artifactSlot);

        var task = tasks.register(assembleTaskName(artifactSlot), Sync.class, copy -> {
            copy.setGroup(LifecycleBasePlugin.BUILD_GROUP);
            copy.into(outputDirectory);
            copy.from(fileCollection);
        });

        assemblyRegistry.register(artifactSlot, task, t -> outputDirectory);

        return assembly;
    }

    private String assembleTaskName(ArtifactSlot artifactSlot) {
        var variantName = artifactSlot.variant().getName();
        var slotName = artifactSlot.slot().getName();

        return "assembleVariantArtifactSlot"
                + "_v" + variantName.length() + "_" + variantName
                + "_s" + slotName.length() + "_" + slotName;
    }

    private Provider<Directory> outputDirectory(ArtifactSlot artifactSlot) {
        return assembliesDirectory.dir(
                FilePaths.cat(
                        artifactSlot.variant().getName(),
                        artifactSlot.slot().getName()));
    }

    /**
     * Collects slot contributions into one {@link ConfigurableFileCollection}.
     */
    private class ContributionVisitor implements SlotContributionVisitor {
        // artifact slot for this assembly
        private final ArtifactSlot artifactSlot;

        // seen inputs, used for deduplication
        private final SlotAssembly assembly;

        ContributionVisitor(ArtifactSlot artifactSlot, SlotAssembly assembly) {
            this.artifactSlot = artifactSlot;
            this.assembly = assembly;
        }

        @Override
        public void visit(DirectContribution contribution) {
            contribute(
                    SlotInputKey.newUniqueKey("Direct input for " + artifactSlot),
                    contribution.input());
        }

        @Override
        public void visit(VariantOutputsContribution contribution) {
            var units = compileUnitsView.getUnitsForVariant(artifactSlot.variant());
            contributeCompileUnits(units, contribution.outputs());
        }

        @Override
        public void visit(RoleOutputsContribution contribution) {
            var roleProjection = roleProjectionsView.requireProjection(artifactSlot.variant(),
                    contribution.role());
            var units = roleProjectionsView.getUnits(roleProjection);

            contributeCompileUnits(units, contribution.outputs());

        }

        @Override
        public void visit(LayerOutputsContribution contribution) {
            var unit = compileUnitsView.requireUnit(artifactSlot.variant(), contribution.layer());
            contributeCompileUnits(Set.of(unit), contribution.outputs());
        }

        private void contributeCompileUnits(Set<CompileUnit> units, Set<String> outputs) {
            units.stream()
                    // expand variant compile units, make (compileUnit, outputName) pairs
                    .flatMap(unit -> outputs.stream()
                            .map(output -> new CompileUnitOutputKey(unit, output)))
                    .forEach(key -> contribute(
                            key,
                            sourceSetMaterializer.getSourceSet(key.unit())
                                    .map(s -> s.output(key.outputName()))));
        }

        private void contribute(SlotInputKey key, Object resolvedInput) {
            assembly.addSlotInput(key, resolvedInput);
        }
    }

    /** Mutable input state for one slot assembly. */
    class SlotAssembly {
        private final ConfigurableFileCollection inputs = objects.fileCollection();
        private final Set<SlotInputKey> seen = new HashSet<>();

        public void addSlotInput(SlotInputKey key, Object input) {
            if (!seen.add(key))
                return;
            inputs.from(input);
        }

        public FileCollection inputs() {
            return inputs;
        }
    }
}
