package org.keycloak.quarkus.runtime.configuration.mappers;

import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import jakarta.ws.rs.core.MultivaluedHashMap;
import org.jboss.logging.Logger;
import org.keycloak.common.util.CollectionUtil;
import org.keycloak.config.ConfigSupportLevel;
import org.keycloak.config.Option;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.Picocli;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.ShowConfig;
import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.quarkus.runtime.configuration.NestedPropertyMappingInterceptor;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.Set;
import java.util.stream.Collectors;

import static org.keycloak.quarkus.runtime.Environment.isParsedCommand;
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck;
import static org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider.isKeyStoreConfigSource;

public final class PropertyMappers {

    public static final String KC_SPI_PREFIX = "kc.spi";
    public static String VALUE_MASK = "*******";
    private static MappersConfig MAPPERS;
    private static final Logger log = Logger.getLogger(PropertyMappers.class);

    private PropertyMappers(){}

    static {
        reset();
    }

    public static void reset() {
        MAPPERS = new MappersConfig();
        MAPPERS.addAll(CachingPropertyMappers.getClusteringPropertyMappers());
        MAPPERS.addAll(DatabasePropertyMappers.getDatabasePropertyMappers());
        MAPPERS.addAll(HostnameV2PropertyMappers.getHostnamePropertyMappers());
        MAPPERS.addAll(HttpPropertyMappers.getHttpPropertyMappers());
        MAPPERS.addAll(HealthPropertyMappers.getHealthPropertyMappers());
        MAPPERS.addAll(ConfigKeystorePropertyMappers.getConfigKeystorePropertyMappers());
        MAPPERS.addAll(ManagementPropertyMappers.getManagementPropertyMappers());
        MAPPERS.addAll(MetricsPropertyMappers.getMetricsPropertyMappers());
        MAPPERS.addAll(EventPropertyMappers.getMetricsPropertyMappers());
        MAPPERS.addAll(ProxyPropertyMappers.getProxyPropertyMappers());
        MAPPERS.addAll(VaultPropertyMappers.getVaultPropertyMappers());
        MAPPERS.addAll(FeaturePropertyMappers.getMappers());
        MAPPERS.addAll(LoggingPropertyMappers.getMappers());
        MAPPERS.addAll(TracingPropertyMappers.getMappers());
        MAPPERS.addAll(TransactionPropertyMappers.getTransactionPropertyMappers());
        MAPPERS.addAll(ClassLoaderPropertyMappers.getMappers());
        MAPPERS.addAll(SecurityPropertyMappers.getMappers());
        MAPPERS.addAll(ExportPropertyMappers.getMappers());
        MAPPERS.addAll(ImportPropertyMappers.getMappers());
        MAPPERS.addAll(TruststorePropertyMappers.getMappers());
        MAPPERS.addAll(BootstrapAdminPropertyMappers.getMappers());
    }

    public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
        PropertyMapper<?> mapper = getMapper(name);

        // During re-aug do not resolve server runtime properties and avoid they included by quarkus in the default value config source.
        //
        // The special handling of log properties is because some logging runtime properties are requested during build time
        // and we need to resolve them. That should be fine as they are generally not considered security sensitive.
        // See https://github.com/quarkusio/quarkus/pull/42157
        if ((isRebuild() || Environment.isRebuildCheck()) && isKeycloakRuntime(name, mapper)
                && !NestedPropertyMappingInterceptor.getResolvingRoot().orElse(name).startsWith("quarkus.log.")) {
            return ConfigValue.builder().withName(name).build();
        }

        if (mapper == null) {
            return context.proceed(name);
        }
        return mapper.forKey(name).getConfigValue(name, context);
    }

    public static boolean isSpiBuildTimeProperty(String name) {
        return name.startsWith(KC_SPI_PREFIX) && (name.endsWith("-provider") || name.endsWith("-enabled") || name.endsWith("-provider-default"));
    }

    private static boolean isKeycloakRuntime(String name, PropertyMapper<?> mapper) {
        if (mapper == null) {
            return name.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK) && !isSpiBuildTimeProperty(name);
        }
        return mapper.isRunTime();
    }

    public static Map<OptionCategory, List<PropertyMapper<?>>> getRuntimeMappers() {
        return MAPPERS.getRuntimeMappers();
    }

    public static Map<OptionCategory, List<PropertyMapper<?>>> getBuildTimeMappers() {
        return MAPPERS.getBuildTimeMappers();
    }

    public static Map<String, PropertyMapper<?>> getDisabledMappers() {
        final var disabledMappers = new HashMap<>(getDisabledBuildTimeMappers());
        disabledMappers.putAll(getDisabledRuntimeMappers());
        return disabledMappers;
    }

    public static Map<String, PropertyMapper<?>> getDisabledRuntimeMappers() {
        return MAPPERS.getDisabledRuntimeMappers();
    }

    public static Map<String, PropertyMapper<?>> getDisabledBuildTimeMappers() {
        return MAPPERS.getDisabledBuildTimeMappers();
    }

    /**
     * Removes all disabled mappers from the runtime/buildtime mappers
     */
    public static void sanitizeDisabledMappers() {
        MAPPERS.sanitizeDisabledMappers();
    }

    public static String maskValue(String value, PropertyMapper<?> mapper) {
        return maskValue(value, null, mapper);
    }

    public static String maskValue(String value, String configSourceName, PropertyMapper<?> mapper) {
        if ((configSourceName != null && isKeyStoreConfigSource(configSourceName) || (mapper != null && mapper.isMask()))) {
            return VALUE_MASK;
        }

        return value;
    }

    private static PropertyMapper<?> getMapperOrDefault(String property, PropertyMapper<?> defaultMapper, OptionCategory category) {
        final var mappers = new ArrayList<>(MAPPERS.getOrDefault(property, Collections.emptyList()));
        if (category != null) {
            mappers.removeIf(m -> !m.getCategory().equals(category));
        }

        return switch (mappers.size()) {
            case 0 -> defaultMapper;
            case 1 -> mappers.get(0);
            default -> {
                log.debugf("Duplicated mappers for key '%s'. Used the first found.", property);
                yield mappers.get(0);
            }
        };
    }

    public static PropertyMapper<?> getMapper(String property, OptionCategory category) {
        return getMapperOrDefault(property, null, category);
    }

    public static PropertyMapper<?> getMapper(String property) {
        return getMapper(property, null);
    }

    public static PropertyMapper<?> getMapperByCliKey(String cliKey) {
        return getKcKeyFromCliKey(cliKey).map(PropertyMappers::getMapper).orElse(null);
    }

    public static Optional<String> getKcKeyFromCliKey(String cliKey) {
        if (!cliKey.startsWith(Picocli.ARG_PREFIX)) {
            return Optional.empty();
        }
        return Optional.ofNullable(
                MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + cliKey.substring(Picocli.ARG_PREFIX.length()));
    }

    /**
     * @return a mutable copy of all known mappers
     */
    public static Set<PropertyMapper<?>> getMappers() {
        return MAPPERS.values().stream().flatMap(Collection::stream).collect(Collectors.toCollection(LinkedHashSet::new));
    }

    public static Set<WildcardPropertyMapper<?>> getWildcardMappers() {
        return MAPPERS.getWildcardMappers();
    }

    public static WildcardPropertyMapper<?> getWildcardMappedFrom(Option<?> from) {
        return MAPPERS.wildcardMapFrom.get(from.getKey());
    }

    public static boolean isSupported(PropertyMapper<?> mapper) {
        ConfigSupportLevel supportLevel = mapper.getCategory().getSupportLevel();
        return supportLevel.equals(ConfigSupportLevel.SUPPORTED) || supportLevel.equals(ConfigSupportLevel.DEPRECATED);
    }

    public static Optional<PropertyMapper<?>> getDisabledMapper(String property) {
        if (property == null) {
            return Optional.empty();
        }

        PropertyMapper<?> mapper = getDisabledBuildTimeMappers().get(property);
        if (mapper == null) {
            mapper = getDisabledRuntimeMappers().get(property);
        }
        return Optional.ofNullable(mapper);
    }

    public static boolean isDisabledMapper(String property) {
        return getDisabledMapper(property).isPresent() && getMapper(property) == null;
    }

    private static Set<PropertyMapper<?>> filterDeniedCategories(List<PropertyMapper<?>> mappers) {
        final var allowedCategories = Environment.getParsedCommand()
                .map(AbstractCommand::getOptionCategories)
                .map(EnumSet::copyOf)
                .orElseGet(() -> EnumSet.allOf(OptionCategory.class));

        return mappers.stream().filter(f -> allowedCategories.contains(f.getCategory())).collect(Collectors.toSet());
    }

    private static class MappersConfig extends MultivaluedHashMap<String, PropertyMapper<?>> {

        private final Map<OptionCategory, List<PropertyMapper<?>>> buildTimeMappers = new EnumMap<>(OptionCategory.class);
        private final Map<OptionCategory, List<PropertyMapper<?>>> runtimeTimeMappers = new EnumMap<>(OptionCategory.class);

        private final Map<String, PropertyMapper<?>> disabledBuildTimeMappers = new HashMap<>();
        private final Map<String, PropertyMapper<?>> disabledRuntimeMappers = new HashMap<>();

        private final Set<WildcardPropertyMapper<?>> wildcardMappers = new HashSet<>();
        private final Map<String, WildcardPropertyMapper<?>> wildcardMapFrom = new HashMap<>();

        public void addAll(PropertyMapper<?>[] mappers) {
            for (PropertyMapper<?> mapper : mappers) {
                addMapper(mapper);

                if (mapper.isBuildTime()) {
                    addMapperByStage(mapper, buildTimeMappers);
                } else {
                    addMapperByStage(mapper, runtimeTimeMappers);
                }
            }
        }

        private static void addMapperByStage(PropertyMapper<?> mapper, Map<OptionCategory, List<PropertyMapper<?>>> mappers) {
            mappers.computeIfAbsent(mapper.getCategory(), c -> new ArrayList<>()).add(mapper);
        }

        public void addMapper(PropertyMapper<?> mapper) {
            if (mapper.hasWildcard()) {
                if (mapper.getMapFrom() != null) {
                    wildcardMapFrom.put(mapper.getMapFrom(), (WildcardPropertyMapper<?>) mapper);
                }
                wildcardMappers.add((WildcardPropertyMapper<?>)mapper);
            } else {
                handleMapper(mapper, this::add);
            }
        }

        public void removeMapper(PropertyMapper<?> mapper) {
            if (mapper.hasWildcard()) {
                wildcardMappers.remove(mapper);
                if (mapper.getFrom() != null) {
                    wildcardMapFrom.remove(mapper.getMapFrom());
                }
            } else {
                handleMapper(mapper, this::remove);
            }
        }

        private void remove(String key, PropertyMapper<?> mapper) {
            List<PropertyMapper<?>> list = get(key);
            if (CollectionUtil.isNotEmpty(list)) {
                list.remove(mapper);
            }
        }

        @Override
        @SuppressWarnings({"rawtypes", "unchecked"})
        public List<PropertyMapper<?>> get(Object key) {
            // First check the base mappings
            String strKey = (String) key;

            List ret = super.get(key);
            if (ret != null) {
                return ret;
            }

            // TODO: we may want to introduce a prefix tree here as we add more wildcardMappers
            // for now we'll just limit ourselves to searching wildcards when we see a quarkus or
            // keycloak key
            if (strKey.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX) || strKey.startsWith(MicroProfileConfigProvider.NS_QUARKUS_PREFIX)) {
                ret = wildcardMappers.stream()
                        .filter(m -> m.matchesWildcardOptionName(strKey))
                        .toList();
                if (!ret.isEmpty()) {
                    return ret;
                }
            }

            return null;
        }

        @Override
        public List<PropertyMapper<?>> remove(Object mapper) {
            return super.remove(mapper);
        }

        public Set<WildcardPropertyMapper<?>> getWildcardMappers() {
            return Collections.unmodifiableSet(wildcardMappers);
        }

        public void sanitizeDisabledMappers() {
            if (Environment.getParsedCommand().isEmpty()) {
                return; // do not sanitize when no command is present
            }

            DisabledMappersInterceptor.runWithDisabled(() -> { // We need to have the whole configuration available

                // Initialize profile in order to check state of features. Disable Persisted CS for re-augmentation
                if (isRebuildCheck()) {
                    PersistedConfigSource.getInstance().runWithDisabled(Environment::getCurrentOrCreateFeatureProfile);
                } else {
                    Environment.getCurrentOrCreateFeatureProfile();
                }

                sanitizeMappers(buildTimeMappers, disabledBuildTimeMappers);
                sanitizeMappers(runtimeTimeMappers, disabledRuntimeMappers);

                assertDuplicatedMappers();
            });
        }

        private void assertDuplicatedMappers() {
            final var duplicatedMappers = entrySet().stream()
                    .filter(e -> CollectionUtil.isNotEmpty(e.getValue()))
                    .filter(e -> e.getValue().size() > 1)
                    .toList();

            final var isBuildPhase = isRebuild() || isRebuildCheck() || isParsedCommand(Build.NAME);
            final var allowedForCommand = isParsedCommand(ShowConfig.NAME);

            if (!duplicatedMappers.isEmpty()) {
                duplicatedMappers.forEach(f -> {
                    final var filteredMappers = filterDeniedCategories(f.getValue());

                    if (filteredMappers.size() > 1) {
                        final var areBuildTimeMappers = filteredMappers.stream().anyMatch(PropertyMapper::isBuildTime);

                        // thrown in runtime, or in build time, when some mapper is marked as buildTime + not allowed to have duplicates for specific command
                        final var shouldBeThrown = !allowedForCommand && (!isBuildPhase || areBuildTimeMappers);
                        if (shouldBeThrown) {
                            throw new PropertyException(String.format("Duplicated mapper for key '%s'.", f.getKey()));
                        }
                    }
                });
            }
        }

        public Map<OptionCategory, List<PropertyMapper<?>>> getRuntimeMappers() {
            return runtimeTimeMappers;
        }

        public Map<OptionCategory, List<PropertyMapper<?>>> getBuildTimeMappers() {
            return buildTimeMappers;
        }

        public Map<String, PropertyMapper<?>> getDisabledBuildTimeMappers() {
            return disabledBuildTimeMappers;
        }

        public Map<String, PropertyMapper<?>> getDisabledRuntimeMappers() {
            return disabledRuntimeMappers;
        }

        private static void sanitizeMappers(Map<OptionCategory, List<PropertyMapper<?>>> mappers,
                                            Map<String, PropertyMapper<?>> disabledMappers) {
            mappers.forEach((category, propertyMappers) ->
                    propertyMappers.removeIf(pm -> {
                        final boolean shouldRemove = !pm.isEnabled();
                        if (shouldRemove) {
                            MAPPERS.removeMapper(pm);
                            handleMapper(pm, disabledMappers::put);
                        }
                        return shouldRemove;
                    }));
        }

        private static void handleMapper(PropertyMapper<?> mapper, BiConsumer<String, PropertyMapper<?>> operation) {
            operation.accept(mapper.getFrom(), mapper);
            if (!mapper.getFrom().equals(mapper.getTo())) {
                operation.accept(mapper.getTo(), mapper);
            }
        }
    }

}
