package org.implab.gradle.variants.core;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.InvalidUserDataException;

/**
 * A resolved view of declared variants, roles, layers, and their bindings.
 *
 * Built from {@link VariantDefinition} instances, this class materializes validated
 * {@link VariantRoleLayer} entries and provides lookup APIs grouped by variant,
 * role, or layer.
 *
 * Typical usage is to collect identities and variant definitions through
 * {@link Builder}, then use the resulting view to traverse resolved bindings.
 */
@NonNullByDefault
public class VariantsView {
    private final Set<Layer> layers;
    private final Set<Role> roles;
    private final Set<Variant> variants;
    private final Set<VariantRoleLayer> entries;

    private final Map<Variant, Set<VariantRoleLayer>> entriesByVariant;
    private final Map<Role, Set<VariantRoleLayer>> entriesByRole;
    private final Map<Layer, Set<VariantRoleLayer>> entriesByLayer;

    private VariantsView(Set<Layer> layers, Set<Role> roles, Set<Variant> variants, Set<VariantRoleLayer> entries) {
        this.layers = layers;
        this.roles = roles;
        this.variants = variants;
        this.entries = entries;
        this.entriesByVariant = entries.stream()
                .collect(Collectors.groupingBy(VariantRoleLayer::variant, Collectors.toSet()));
        this.entriesByRole = entries.stream()
                .collect(Collectors.groupingBy(VariantRoleLayer::role, Collectors.toSet()));
        this.entriesByLayer = entries.stream()
                .collect(Collectors.groupingBy(VariantRoleLayer::layer, Collectors.toSet()));
    }

    /**
     * Returns all declared layers included in this view.
     */
    public Set<Layer> getLayers() {
        return layers;
    }

    /**
     * Returns all declared roles included in this view.
     */
    public Set<Role> getRoles() {
        return roles;
    }

    public void assertRole(Role role) {
        Objects.requireNonNull(role, "The role can't be null");

        if (!roles.contains(role))
            throw new InvalidUserDataException("The specified role '" + role.getName() + "' isn't declared");
    }

    /**
     * Returns all declared variants included in this view.
     */
    public Set<Variant> getVariants() {
        return variants;
    }

    /**
     * Returns all resolved variant-role-layer bindings.
     */
    public Set<VariantRoleLayer> getEntries() {
        return entries;
    }

    /**
     * Returns all bindings associated with the specified variant.
     *
     * An empty set is returned when the variant has no bindings in this view.
     */
    public Set<VariantRoleLayer> getEntriesForVariant(Variant variant) {
        return entriesByVariant.getOrDefault(variant, Set.of());
    }

    /**
     * Returns all bindings associated with the specified layer.
     *
     * An empty set is returned when the layer has no bindings in this view.
     */
    public Set<VariantRoleLayer> getEntriesForLayer(Layer layer) {
        return entriesByLayer.getOrDefault(layer, Set.of());
    }

    /**
     * Returns all bindings associated with the specified role.
     *
     * An empty set is returned when the role has no bindings in this view.
     */
    public Set<VariantRoleLayer> getEntriesForRole(Role role) {
        return entriesByRole.getOrDefault(role, Set.of());
    }

    /**
     * A resolved binding between a variant, a role, and a layer.
     *
     * @param variant the resolved variant
     * @param role the resolved role
     * @param layer the resolved layer
     */
    public record VariantRoleLayer(Variant variant, Role role, Layer layer) {
    }

    /**
     * Creates a builder for assembling a {@link VariantsView}.
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Collects declared identities and variant definitions, then resolves them
     * into a {@link VariantsView}.
     */
    public static class Builder {

        private final Map<String, Layer> layers = new LinkedHashMap<>();
        private final Map<String, Role> roles = new LinkedHashMap<>();
        private final Map<String, Variant> variants = new LinkedHashMap<>();
        private final List<VariantDefinition> definitions = new ArrayList<>();

        private Builder() {
        }

        /**
         * Adds or replaces a role by its name.
         */
        public Builder addRole(Role role) {
            Objects.requireNonNull(role, "role can't be null");
            roles.put(role.getName(), role);
            return this;
        }

        /**
         * Adds or replaces a layer by its name.
         */
        public Builder addLayer(Layer layer) {
            Objects.requireNonNull(layer, "layer can't be null");
            layers.put(layer.getName(), layer);
            return this;
        }

        /**
         * Adds or replaces a variant by its name.
         */
        public Builder addVariant(Variant variant) {
            Objects.requireNonNull(variant, "variant can't be null");
            variants.put(variant.getName(), variant);
            return this;
        }

        /**
         * Adds a variant definition to be resolved during {@link #build()}.
         */
        public Builder addDefinition(VariantDefinition definition) {
            Objects.requireNonNull(definition, "definition can't be null");
            definitions.add(definition);
            return this;
        }

        /**
         * Resolves collected identities and definitions into an immutable view.
         *
         * Missing variant, role, or layer declarations referenced by definitions
         * cause {@link InvalidUserDataException}.
         */
        public VariantsView build() {

            var entries = definitions.stream()
                    .flatMap(def -> def.getRoleBindings().get().stream()
                            .map(layerRole -> createVariantRoleLayer(
                                    def.getName(), // variantName
                                    layerRole.roleName(),
                                    layerRole.layerName())))
                    .collect(Collectors.toSet());

            return new VariantsView(
                    Set.copyOf(layers.values()),
                    Set.copyOf(roles.values()),
                    Set.copyOf(variants.values()),
                    entries);
        }

        private VariantRoleLayer createVariantRoleLayer(String variantName, String roleName, String layerName) {
            return new VariantRoleLayer(
                    resolveVariant(variantName,
                            "Variant '" + variantName + "' isn't declared"),
                    resolveRole(roleName,
                            "Role '" + roleName + "' isn't declared, referenced in '" + variantName + "' variant"),
                    resolveLayer(layerName,
                            "Layer '" + layerName + "' isn't declared, referenced in '" + variantName
                                    + "' variant with '" + roleName + "' role"));
        }

        private Layer resolveLayer(String layerName, String errorMessage) {
            return Optional.ofNullable(layers.get(layerName))
                    .orElseThrow(() -> new InvalidUserDataException(errorMessage));
        }

        private Variant resolveVariant(String variantName, String errorMessage) {
            return Optional.ofNullable(variants.get(variantName))
                    .orElseThrow(() -> new InvalidUserDataException(errorMessage));
        }

        private Role resolveRole(String roleName, String errorMessage) {
            return Optional.ofNullable(roles.get(roleName))
                    .orElseThrow(() -> new InvalidUserDataException(errorMessage));
        }
    }
}
