package org.implab.gradle.common.sources;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.gradle.api.GradleException;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ConfigurationPublications;
import org.gradle.api.artifacts.ConfigurationVariant;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.implab.gradle.common.core.lang.Strings;

public abstract class VariantArtifactsPlugin implements Plugin<Project> {
    private static final Logger logger = Logging.getLogger(VariantArtifactsPlugin.class);
    public static final String VARIANT_ARTIFACTS_EXTENSION_NAME = "variantArtifacts";

    @Override
    public void apply(Project target) {
        logger.debug("Registering '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());

        target.getPluginManager().apply(VariantsSourcesPlugin.class);

        var variants = VariantsPlugin.getVariantsExtension(target);
        var variantSources = target.getExtensions().getByType(VariantSourcesExtension.class);
        var variantArtifacts = target.getExtensions()
                .create(VARIANT_ARTIFACTS_EXTENSION_NAME, VariantArtifactsExtension.class);
        var variantArtifactsResolver = new VariantArtifactsResolver(target.getObjects());
        var artifactAssemblies = new ArtifactAssemblyRegistry(target.getObjects(), target.getTasks());

        // Bind variant artifacts resolution to variant sources registration, so that
        // artifact resolution can be performed
        variantSources.whenBound(variantArtifactsResolver::recordBinding);

        variants.whenFinalized(model -> {
            logger.debug("Finalizing variantArtifacts model on project '{}'", target.getPath());
            variantArtifacts.finalizeModel(model);
            materializeOutgoingVariants(target, model, variantArtifacts, variantArtifactsResolver, artifactAssemblies);
            logger.debug("variantArtifacts model finalized on project '{}'", target.getPath());
        });
    }

    public static VariantArtifactsExtension getVariantArtifactsExtension(Project target) {
        var extension = target.getExtensions().findByType(VariantArtifactsExtension.class);

        if (extension == null) {
            logger.error("variantArtifacts extension '{}' isn't found on project '{}'",
                    VARIANT_ARTIFACTS_EXTENSION_NAME,
                    target.getPath());
            throw new GradleException("variantArtifacts extension isn't found");
        }

        logger.debug("Resolved '{}' extension on project '{}'", VARIANT_ARTIFACTS_EXTENSION_NAME, target.getPath());

        return extension;
    }

    private static void materializeOutgoingVariants(
            Project project,
            BuildVariantsExtension topology,
            VariantArtifactsExtension variantArtifacts,
            VariantArtifactsResolver variantArtifactsResolver,
            ArtifactAssemblyRegistry artifactAssemblies) {
        variantArtifacts.getVariants().stream()
                .filter(variantArtifact -> !variantArtifact.getSlots().isEmpty())
                .forEach(variantArtifact -> materializeOutgoingVariant(
                        project,
                        topology.require(variantArtifact.getName()),
                        variantArtifact,
                        variantArtifactsResolver,
                        artifactAssemblies,
                        variantArtifacts));
    }

    private static void materializeOutgoingVariant(
            Project project,
            BuildVariant topologyVariant,
            VariantArtifact variantArtifact,
            VariantArtifactsResolver variantArtifactsResolver,
            ArtifactAssemblyRegistry artifactAssemblies,
            VariantArtifactsExtension variantArtifacts) {
        var assemblies = variantArtifact.getSlots().stream()
                .collect(Collectors.toMap(
                        VariantArtifactSlot::getName,
                        slot -> registerAssembly(project, variantArtifactsResolver, artifactAssemblies, variantArtifact,
                                slot),
                        (left, right) -> left,
                        LinkedHashMap::new));

        var primarySlot = variantArtifact.requirePrimarySlot();
        var configuration = createOutgoingConfiguration(project, variantArtifact.getName(), primarySlot.getName());
        var primaryAssembly = assemblies.get(primarySlot.getName());
        publishPrimaryArtifact(configuration, primaryAssembly);
        var primaryPublication = new OutgoingArtifactSlotPublication(
                primarySlot.getName(),
                true,
                primarySlot,
                primaryAssembly,
                configuration);
        var secondarySlots = variantArtifact.getSlots().stream()
                .filter(slot -> !slot.getName().equals(primarySlot.getName()))
                .map(slot -> new SecondarySlot(slot, assemblies.get(slot.getName())))
                .toList();
        var secondaryPublications = new ArrayList<OutgoingArtifactSlotPublication>(secondarySlots.size());
        secondarySlots.forEach(secondarySlot -> {
            var secondaryVariant = configuration.getOutgoing().getVariants().create(secondarySlot.slot().getName());
            publishSecondaryArtifact(secondaryVariant, secondarySlot.assembly());
            secondaryPublications.add(new OutgoingArtifactSlotPublication(
                    secondarySlot.slot().getName(),
                    false,
                    secondarySlot.slot(),
                    secondarySlot.assembly(),
                    secondaryVariant));
        });

        var slotPublications = Stream.concat(
                Stream.of(primaryPublication),
                secondaryPublications.stream())
                .toList();

        variantArtifacts.notifyOutgoingVariant(new OutgoingVariantPublication(
                variantArtifact.getName(),
                topologyVariant,
                variantArtifact,
                configuration,
                primaryPublication,
                slotPublications));
    }

    private static ArtifactAssembly registerAssembly(
            Project project,
            VariantArtifactsResolver variantArtifactsResolver,
            ArtifactAssemblyRegistry artifactAssemblies,
            VariantArtifact variantArtifact,
            VariantArtifactSlot slot) {
        String assemblyName = variantArtifact.getName() + Strings.capitalize(slot.getName());
        return artifactAssemblies.register(
                assemblyName,
                "process" + Strings.capitalize(assemblyName),
                project.getLayout().getBuildDirectory()
                        .dir("variant-artifacts/" + variantArtifact.getName() + "/" + slot.getName()),
                files -> files.from(variantArtifactsResolver.files(variantArtifact.getName(), slot)));
    }

    private static Configuration createOutgoingConfiguration(
            Project project,
            String variantName,
            String primarySlotName) {
        var configName = variantName + "Elements";
        return project.getConfigurations().consumable(configName, config -> {
            config.setVisible(true);
            config.setDescription("Consumable assembled artifacts for variant '" + variantName
                    + "' with primary slot '" + primarySlotName + "'");
        }).get();
    }

    private static void publishPrimaryArtifact(Configuration configuration, ArtifactAssembly assembly) {
        publishArtifact(configuration.getOutgoing(), assembly);
    }

    private static void publishSecondaryArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
        publishArtifact(variant, assembly);
    }

    private static void publishArtifact(ConfigurationPublications outgoing, ArtifactAssembly assembly) {
        outgoing.artifact(assembly.output().getSingleFile(), published -> {
            published.builtBy(assembly.output().getBuildDependencies());
        });
    }

    private static void publishArtifact(ConfigurationVariant variant, ArtifactAssembly assembly) {
        variant.artifact(assembly.output().getSingleFile(), published -> {
            published.builtBy(assembly.output().getBuildDependencies());
        });
    }

    private record SecondarySlot(VariantArtifactSlot slot, ArtifactAssembly assembly) {
    }
}
