package org.implab.gradle.common.sources;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import javax.inject.Inject;

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

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

/**
 * Root DSL and lifecycle holder for the {@code variantArtifacts} model.
 *
 * <p>This extension sits on top of the build topology defined by
 * {@link BuildVariantsExtension} and the source-set materialization performed by
 * {@link VariantSourcesExtension}:
 * <ul>
 * <li>{@link #variant(String, Action)} declares one outgoing artifact model per
 * topology variant;</li>
 * <li>each {@link VariantArtifact} declares one or more
 * {@link VariantArtifactSlot slots};</li>
 * <li>after topology finalization this extension validates the artifact model,
 * freezes it and later receives replayable outgoing-publication callbacks via
 * {@link #whenOutgoingVariant(Action)}.</li>
 * </ul>
 */
@NonNullByDefault
public abstract class VariantArtifactsExtension {
    private final NamedDomainObjectContainer<VariantArtifact> variants;
    private final ObjectFactory objects;
    private final List<Action<? super OutgoingVariantPublication>> outgoingVariantActions = new ArrayList<>();
    private final List<OutgoingVariantPublication> outgoingVariants = new ArrayList<>();
    private boolean finalized;

    @Inject
    public VariantArtifactsExtension(ObjectFactory objects) {
        this.objects = objects;
        variants = objects.domainObjectContainer(VariantArtifact.class, this::newVariantArtifact);

        variants.all(variant -> {
            if (finalized)
                throw new InvalidUserDataException(
                        "variantArtifacts model is finalized and cannot add variant '" + variant.getName() + "'");
        });
    }

    public NamedDomainObjectContainer<VariantArtifact> getVariants() {
        return variants;
    }

    public VariantArtifact variant(String name) {
        return variant(name, variant -> {
        });
    }

    public VariantArtifact variant(String name, Action<? super VariantArtifact> configure) {
        ensureMutable("configure variants");
        var variant = variants.maybeCreate(VariantArtifact.normalize(name, "variant name must not be null or blank"));
        configure.execute(variant);
        return variant;
    }

    public VariantArtifact variant(
            String name,
            @DelegatesTo(value = VariantArtifact.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
        return variant(name, Closures.action(configure));
    }

    public Optional<VariantArtifact> findVariant(String variantName) {
        return Optional
                .ofNullable(variants.findByName(VariantArtifact.normalize(variantName, "variant name must not be null or blank")));
    }

    public VariantArtifact requireVariant(String variantName) {
        var normalizedVariantName = VariantArtifact.normalize(variantName, "variant name must not be null or blank");
        return findVariant(normalizedVariantName)
                .orElseThrow(() -> new InvalidUserDataException(
                        "Variant artifacts do not declare variant '" + normalizedVariantName + "'"));
    }

    public void whenOutgoingVariant(Action<? super OutgoingVariantPublication> action) {
        outgoingVariantActions.add(action);
        for (var publication : outgoingVariants)
            action.execute(publication);
    }

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

    public boolean isFinalized() {
        return finalized;
    }

    void finalizeModel(BuildVariantsExtension topology) {
        if (finalized)
            return;

        validate(topology);

        for (var variant : variants)
            variant.finalizeModel();

        finalized = true;
    }

    void notifyOutgoingVariant(OutgoingVariantPublication publication) {
        outgoingVariants.add(publication);
        for (var action : outgoingVariantActions)
            action.execute(publication);
    }

    private VariantArtifact newVariantArtifact(String name) {
        return objects.newInstance(VariantArtifact.class, name, objects.domainObjectContainer(VariantArtifactSlot.class));
    }

    private void validate(BuildVariantsExtension topology) {
        var errors = new ArrayList<String>();

        for (var variantArtifact : variants)
            validateVariantArtifact(topology, variantArtifact, errors);

        throwIfInvalid(errors);
    }

    private static void validateVariantArtifact(
            BuildVariantsExtension topology,
            VariantArtifact variantArtifact,
            List<String> errors) {
        var topologyVariant = topology.find(variantArtifact.getName());
        if (topologyVariant.isEmpty()) {
            errors.add("Variant artifact '" + variantArtifact.getName() + "' references unknown variant '"
                    + variantArtifact.getName() + "'");
            return;
        }

        var topologyScope = TopologyScope.from(topologyVariant.get());
        validateTopologyReferences(variantArtifact, topologyScope, errors);
        validatePrimarySlot(variantArtifact, errors);
    }

    private static void validateTopologyReferences(
            VariantArtifact variantArtifact,
            TopologyScope topologyScope,
            List<String> errors) {
        for (var slot : variantArtifact.getSlots()) {
            validateSlotReferences(variantArtifact, slot, "role", slot.referencedRoleNames(), topologyScope.roleNames(), errors);
            validateSlotReferences(variantArtifact, slot, "layer", slot.referencedLayerNames(), topologyScope.layerNames(), errors);
        }
    }

    private static void validatePrimarySlot(VariantArtifact variantArtifact, List<String> errors) {
        if (variantArtifact.getSlots().isEmpty())
            return;

        if (variantArtifact.findPrimarySlotName().isEmpty()) {
            errors.add("Variant artifact '" + variantArtifact.getName()
                    + "' must declare primary slot because it has multiple slots");
            return;
        }

        var primarySlotName = variantArtifact.requirePrimarySlotName();
        if (variantArtifact.findSlot(primarySlotName).isEmpty()) {
            errors.add("Variant artifact '" + variantArtifact.getName()
                    + "' declares unknown primary slot '" + primarySlotName + "'");
        }
    }

    private static void validateSlotReferences(
            VariantArtifact variantArtifact,
            VariantArtifactSlot slot,
            String referenceKind,
            Set<String> referencedNames,
            Set<String> knownNames,
            List<String> errors) {
        for (var referencedName : referencedNames) {
            if (!knownNames.contains(referencedName)) {
                errors.add("Variant artifact '" + variantArtifact.getName() + "', slot '" + slot.getName()
                        + "' references unknown " + referenceKind + " '" + referencedName + "'");
            }
        }
    }

    private static void throwIfInvalid(List<String> errors) {
        if (errors.isEmpty())
            return;

        var message = new StringBuilder("Invalid variantArtifacts model:");
        for (var error : errors)
            message.append("\n - ").append(error);

        throw new InvalidUserDataException(message.toString());
    }

    private record TopologyScope(Set<String> roleNames, Set<String> layerNames) {
        private static TopologyScope from(BuildVariant topologyVariant) {
            var roleNames = new LinkedHashSet<String>();
            var layerNames = new LinkedHashSet<String>();

            for (var role : topologyVariant.getRoles()) {
                roleNames.add(role.getName());
                layerNames.addAll(role.getLayers().getOrElse(List.of()));
            }

            return new TopologyScope(Set.copyOf(roleNames), Set.copyOf(layerNames));
        }
    }

    private void ensureMutable(String operation) {
        if (finalized)
            throw new InvalidUserDataException("variantArtifacts model is finalized and cannot " + operation);
    }
}
