package com.airlock.iam.application.parameters.infrastructure;

import com.airlock.iam.application.parameters.domain.model.IamProfile;
import com.airlock.iam.application.parameters.domain.model.IamTarget;
import com.airlock.iam.application.parameters.domain.model.ParameterSpec;
import com.airlock.iam.application.parameters.domain.model.ParameterVariableSpec;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public class ParametersLoader {
   private static final ELogger LOG = ELoggerFactory.getLogger(ParametersLoader.class);
   private final Properties systemProperties;
   private final Map<String, String> environment;

   public ParametersLoader(Properties systemProperties, Map<String, String> environment) {
      this.systemProperties = systemProperties;
      this.environment = environment;
   }

   public Map<ParameterSpec, String> loadApplicationParameters(IamTarget target, List<Path> parametersFiles) {
      List<Properties> parametersProperties = this.loadApplicationParametersFiles(parametersFiles);
      return this.loadApplicationParametersProperties(target, parametersProperties);
   }

   private List<Properties> loadApplicationParametersFiles(List<Path> parametersFiles) {
      return parametersFiles.stream().map(ParametersLoader::loadProperties).toList();
   }

   @VisibleForTesting
   Map<ParameterSpec, String> loadApplicationParametersProperties(IamTarget target, List<Properties> parametersProperties) {
      Map<ParameterSpec, String> parameters = this.mergeApplicationParameters(parametersProperties);
      return patchValues(replaceVariables(target, parameters));
   }

   private Map<ParameterSpec, String> mergeApplicationParameters(List<Properties> parametersProperties) {
      Map<ParameterSpec, String> parameters = new HashMap<>(loadDefaultParameters());
      parametersProperties.forEach(properties -> parameters.putAll(parametersMap(ParameterSpec::propertyFileKey, properties::getProperty)));
      parameters.putAll(parametersMap(ParameterSpec::environmentKey, this.environment::get));
      parameters.putAll(parametersMap(ParameterSpec::systemPropertyKey, this.systemProperties::getProperty));
      return parameters;
   }

   private static Map<ParameterSpec, String> replaceVariables(IamTarget target, Map<ParameterSpec, String> parameters) {
      Map<ParameterVariableSpec, Supplier<String>> replacements = Map.ofEntries(
         Map.entry(ParameterVariableSpec.INSTANCE_NAME, () -> target.instance().name()),
         Map.entry(ParameterVariableSpec.WORK_DIR, () -> parameters.get(ParameterSpec.WORK_DIR)),
         Map.entry(ParameterVariableSpec.PROFILE_NAME, () -> target.profile().map(IamProfile::name).orElse(""))
      );
      Map<ParameterSpec, String> result = new HashMap<>(parameters);
      replacements.forEach((variable, replacement) -> result.replaceAll((parameter, value) -> replace(value, variable.expression(), replacement)));
      return result;
   }

   private static String replace(String input, String pattern, Supplier<String> replacement) {
      return input.contains(pattern) ? input.replace(pattern, replacement.get()) : input;
   }

   private static Map<ParameterSpec, String> patchValues(Map<ParameterSpec, String> parameters) {
      String sensitiveValuesConfig = parameters.get(ParameterSpec.SENSITIVE_VALUES_CONFIG);
      if (!Strings.isNullOrEmpty(sensitiveValuesConfig) && !sensitiveValuesConfig.contains(":")) {
         LOG.warn(
            EDefaultLogType.GENERIC_LOG_TYPE,
            "Parameter " + ParameterSpec.SENSITIVE_VALUES_CONFIG + " is missing scheme (\"provider:\" or \"secrets:\" prefix): " + sensitiveValuesConfig
         );
         parameters.put(ParameterSpec.SENSITIVE_VALUES_CONFIG, "provider:" + sensitiveValuesConfig);
      }

      return parameters;
   }

   static Map<ParameterSpec, String> loadDefaultParameters() {
      return parametersMap(ParameterSpec::propertyFileKey, loadDefaultProperties()::getProperty);
   }

   private static Map<ParameterSpec, String> parametersMap(Function<ParameterSpec, String> keyMapper, Function<String, String> valueMapper) {
      return parametersMap(valueMapper.compose(keyMapper));
   }

   private static Map<ParameterSpec, String> parametersMap(Function<ParameterSpec, String> valueMapper) {
      return Arrays.stream(ParameterSpec.values())
         .filter(param -> filterValue(param, valueMapper.apply(param)))
         .collect(Collectors.toMap(Function.identity(), parameter -> valueMapper.apply(parameter).strip()));
   }

   private static boolean filterValue(ParameterSpec parameter, String value) {
      if (value == null) {
         return false;
      } else {
         return parameter.isEmptyValueAllowed() && value.isBlank() ? true : !value.isBlank();
      }
   }

   private static Properties loadDefaultProperties() {
      Properties properties = new Properties();

      try (InputStream input = ParametersLoader.class.getResourceAsStream("parameter-defaults.properties")) {
         properties.load(input);
         return properties;
      } catch (IOException e) {
         throw new IllegalStateException(e);
      }
   }

   private static Properties loadProperties(Path propertiesFile) {
      if (propertiesFile == null) {
         return new Properties();
      }

      Properties properties = new Properties();

      try (InputStream input = Files.newInputStream(propertiesFile)) {
         properties.load(input);
         return properties;
      } catch (IOException e) {
         throw new IllegalStateException(e);
      }
   }
}
