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

import com.airlock.iam.application.parameters.domain.model.IamConfigRoot;
import com.airlock.iam.application.parameters.domain.model.Log4jConfig;
import com.airlock.iam.application.parameters.domain.model.LogFormat;
import com.airlock.iam.application.parameters.domain.model.ParameterSpec;
import com.airlock.iam.application.parameters.domain.model.ParameterVariableSpec;
import com.airlock.iam.application.parameters.domain.model.WebServerParameters;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.system.config.ConfigFormat;
import com.airlock.iam.system.module.IamModule;
import com.google.common.base.Strings;
import jakarta.annotation.Nullable;
import java.io.IOException;
import java.net.URI;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

final class ParameterParser {
   private static final ELogger LOG = ELoggerFactory.getLogger(ParameterParser.class);
   private static final Pattern COMMA_SEPARATOR = Pattern.compile(",");
   private static final String WEBSERVICE_MODULE = "webservice";
   private static final Set<String> SUPPORTED_TLS_PROTOCOLS = new LinkedHashSet<>(List.of("SSLv2Hello", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"));
   private static final Set<String> CLIENT_AUTH_OPTIONS = new LinkedHashSet<>(List.of("NONE", "OPTIONAL_NO_CA", "OPTIONAL", "REQUIRED"));
   private static final Set<String> VALID_WEBSERVER_LOG_LEVELS = new LinkedHashSet<>(List.of("ERROR", "WARN", "INFO", "DEBUG", "TRACE"));
   private static final Set<String> VALID_LOG_FORMAT_TYPES = new LinkedHashSet<>(
      List.of(LogFormat.SIMPLE.getIdentifier(), LogFormat.STRUCTURED.getIdentifier())
   );
   private static final Set<String> VALID_CONFIG_FORMAT_TYPES = new LinkedHashSet<>(
      List.of(ConfigFormat.XML_FORMAT.getIdentifier(), ConfigFormat.YAML_FORMAT.getIdentifier())
   );

   static int safeParseInteger(Map<ParameterSpec, String> parameters, ParameterSpec paramSpec) throws ParameterValidationException {
      try {
         return Optional.ofNullable(parameters.get(paramSpec)).map(Integer::parseInt).orElseThrow(NumberFormatException::new);
      } catch (NumberFormatException e) {
         throw new ParameterValidationException(paramSpec.propertyFileKey() + ": expected a valid integer, instead got: " + parameters.get(paramSpec));
      }
   }

   static int safeParsePositiveInteger(Map<ParameterSpec, String> parameters, ParameterSpec paramSpec) throws ParameterValidationException {
      int parsedInt = safeParseInteger(parameters, paramSpec);
      if (parsedInt < 0) {
         throw new ParameterValidationException(String.format("%s: integer argument must be non-negative.", paramSpec.propertyFileKey()));
      } else {
         return parsedInt;
      }
   }

   static boolean safeParseBoolean(Map<ParameterSpec, String> parameters, ParameterSpec paramSpec) throws ParameterValidationException {
      String boolString = parameters.get(paramSpec);
      if (boolString == null || boolString.isBlank()) {
         return false;
      } else if ("true".equalsIgnoreCase(boolString)) {
         return true;
      } else if ("false".equalsIgnoreCase(boolString)) {
         return false;
      } else {
         throw new ParameterValidationException(paramSpec.propertyFileKey() + ": expected boolean (true/false), instead got: " + parameters.get(paramSpec));
      }
   }

   static List<Path> requireValidClasspath(IamConfigRoot configRoot, String classpath) throws ParameterValidationException {
      if (Strings.isNullOrEmpty(classpath)) {
         return List.of();
      }

      try {
         return csvStream(classpath.replace(':', ',')).map(entry -> requireValidClasspathEntry(configRoot, entry)).flatMap(Collection::stream).toList();
      } catch (RuntimeException e) {
         throw new ParameterValidationException(e.getMessage(), e);
      }
   }

   private static List<Path> requireValidClasspathEntry(IamConfigRoot configRoot, String entry) throws ParameterValidationException {
      if (!entry.endsWith("/*")) {
         return List.of(resolvePath(configRoot, entry));
      }

      Path directory = resolvePath(configRoot, entry.substring(0, entry.lastIndexOf(47)));
      if (Files.isDirectory(directory)) {
         try (DirectoryStream<Path> entries = Files.newDirectoryStream(directory, "*.{jar,JAR}")) {
            return StreamSupport.stream(entries.spliterator(), false).sorted().toList();
         } catch (IOException e) {
            throw new ParameterValidationException(
               "Failed to resolve classpath entry: " + e.getMessage() + System.lineSeparator() + "Expected a directory containing .jar files", e
            );
         }
      } else {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "Classpath entry is not a directory: " + directory);
         return List.of();
      }
   }

   static Path resolveXMLPluginConfigPath(IamConfigRoot configRoot, String path) throws ParameterValidationException {
      Path pluginConfig = resolvePath(configRoot, path);
      if (!pluginConfig.endsWith("medusa-configuration.xml")) {
         throw new ParameterValidationException("Plugin configuration file must be named \"medusa-configuration.xml\": " + path);
      } else {
         return pluginConfig;
      }
   }

   static Path resolveYAMLPluginConfigPath(IamConfigRoot configRoot, String path) throws ParameterValidationException {
      Path xmlPath = resolveXMLPluginConfigPath(configRoot, path);
      return xmlPath.resolveSibling("iam-config.yaml");
   }

   static ConfigFormat parsePluginConfigurationFormat(String configFormat) throws ParameterValidationException {
      if (!VALID_CONFIG_FORMAT_TYPES.contains(configFormat)) {
         throw new ParameterValidationException(
            "Invalid format argument: "
               + configFormat
               + System.lineSeparator()
               + "Expected one of the following options: "
               + String.join(", ", VALID_CONFIG_FORMAT_TYPES)
         );
      } else {
         return ConfigFormat.parseConfigFormat(configFormat);
      }
   }

   static List<Log4jConfig> parseLog4jConfigs(IamConfigRoot configRoot, String value) {
      return Strings.isNullOrEmpty(value) ? List.of() : csvStream(value).map(v -> resolveLog4jConfig(configRoot, v)).toList();
   }

   private static Log4jConfig resolveLog4jConfig(IamConfigRoot configRoot, String value) {
      return !ParameterVariableSpec.LOG4J_BASE.expression().equals(value) && !ParameterVariableSpec.LOG4J_COMMON.expression().equals(value)
         ? Log4jConfig.resolvedWith(resolvePath(configRoot, value))
         : Log4jConfig.referencedBy(ParameterVariableSpec.LOG4J_BASE);
   }

   static Path resolvePath(IamConfigRoot configRoot, String path) {
      return configRoot.directory().resolve(path);
   }

   static Path resolvePath(IamConfigRoot configRoot, Path path) {
      return configRoot.directory().resolve(path);
   }

   @Nullable
   static Path resolveNullablePath(IamConfigRoot configRoot, @Nullable String path) {
      return Strings.isNullOrEmpty(path != null ? path.trim() : null) ? null : resolvePath(configRoot, path);
   }

   static URI parseJdbcUrl(String value) throws ParameterValidationException {
      return parseUrl(value, "jdbc");
   }

   static URI parseUrl(String value, String... allowedSchemes) throws ParameterValidationException {
      if (Strings.isNullOrEmpty(value)) {
         return null;
      } else {
         URI url = URI.create(value);
         if (!url.isAbsolute()) {
            throw new ParameterValidationException("URL must be absolute and start with a scheme followed by \"://\" " + url);
         } else if (Stream.of(allowedSchemes).noneMatch(url.getScheme()::equals)) {
            throw new ParameterValidationException(String.format("URL scheme must be %s: %s", String.join(" or ", allowedSchemes), value));
         } else {
            return url;
         }
      }
   }

   static String parseUrlPath(String urlPath) throws ParameterValidationException {
      if (!Objects.requireNonNull(urlPath).startsWith("/")) {
         throw new ParameterValidationException("URL path must start with \"/\": " + urlPath);
      } else {
         return urlPath;
      }
   }

   static List<String> parseTlsProtocols(String tlsProtocols) throws ParameterValidationException {
      List<String> protocols = csvStream(tlsProtocols).toList();
      if (!SUPPORTED_TLS_PROTOCOLS.containsAll(protocols)) {
         throw new ParameterValidationException(
            "Unsupported TLS protocol configuration: "
               + tlsProtocols
               + System.lineSeparator()
               + "Expected a comma separated list including these protocols: "
               + String.join(", ", SUPPORTED_TLS_PROTOCOLS)
         );
      } else {
         return protocols;
      }
   }

   static String parseClientAuth(String clientAuth) throws ParameterValidationException {
      if (!CLIENT_AUTH_OPTIONS.contains(clientAuth)) {
         throw new ParameterValidationException(
            "Unsupported client authentication configuration: "
               + clientAuth
               + System.lineSeparator()
               + "Expected one of the following options: "
               + String.join(", ", CLIENT_AUTH_OPTIONS)
         );
      } else {
         return clientAuth;
      }
   }

   static boolean csvContainsElement(@Nullable String csv, String element) {
      return csvStream(csv).anyMatch(it -> it.equals(element));
   }

   static Set<IamModule> parseModules(@Nullable String modulesCsv) throws ParameterValidationException {
      if (modulesCsv != null && modulesCsv.contains("webservice")) {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "Change needed: Please remove module \"webservice\" because it is no longer supported.");
      }

      try {
         return csvStream(modulesCsv)
            .filter(module -> !"webservice".equals(module))
            .map(
               id -> (IamModule)IamModule.fromIdentifier(id).orElseThrow(() -> new RuntimeException("Unknown module \"" + id + "\" in modules: " + modulesCsv))
            )
            .collect(Collectors.toSet());
      } catch (RuntimeException e) {
         throw new ParameterValidationException(e.getMessage());
      }
   }

   static List<Path> buildLayoutDirs(IamConfigRoot configRoot, @Nullable String layoutDirs) {
      return csvStream(layoutDirs).map(p -> resolvePath(configRoot, p)).filter(x$0 -> Files.exists(x$0)).toList();
   }

   static LogFormat parseWebServerLogFormat(String logFormat) throws ParameterValidationException {
      if (!VALID_LOG_FORMAT_TYPES.contains(logFormat)) {
         throw new ParameterValidationException(
            "Invalid format argument: "
               + logFormat
               + System.lineSeparator()
               + "Expected one of the following options: "
               + String.join(", ", VALID_LOG_FORMAT_TYPES)
         );
      } else {
         return LogFormat.parseLogFormat(logFormat);
      }
   }

   static void validateWebserverParameters(WebServerParameters webServer) throws ParameterValidationException {
      if (webServer.connectors().isEmpty()) {
         throw new ParameterValidationException("At least one web server connector must be defined.");
      }
   }

   static void validateWebserverLogFileName(boolean fileEnabled, String prefix, String suffix) throws ParameterValidationException {
      if (fileEnabled && prefix.isBlank() && suffix.isBlank()) {
         throw new ParameterValidationException("Log file prefix and suffix cannot both be empty.");
      }
   }

   static void validateWebserverLogDirectory(boolean fileEnabled, Path directory) throws ParameterValidationException {
      if (fileEnabled && directory == null) {
         throw new ParameterValidationException("File logging was enabled, but output directory was not specified.");
      }
   }

   static String parseWebServerLogPattern(String logPattern) throws ParameterValidationException {
      Optional<String> pattern = Optional.ofNullable(Strings.emptyToNull(logPattern));
      if (pattern.isEmpty()) {
         throw new ParameterValidationException("Webserver and access log pattern cannot be empty.");
      } else {
         return pattern.get();
      }
   }

   static String parseWebServerLogLevel(String logLevel) throws ParameterValidationException {
      try {
         if (!VALID_WEBSERVER_LOG_LEVELS.contains(logLevel)) {
            throw new ParameterValidationException("Specified string is not a valid webserver log level.");
         } else {
            return logLevel;
         }
      } catch (RuntimeException e) {
         throw new ParameterValidationException(
            "Invalid or empty webserver log level: "
               + logLevel
               + System.lineSeparator()
               + "Expected one of the following strings: "
               + String.join(", ", VALID_WEBSERVER_LOG_LEVELS),
            e
         );
      }
   }

   private static Stream<String> csvStream(@Nullable String csv) {
      return csv == null ? Stream.empty() : COMMA_SEPARATOR.splitAsStream(csv).map(String::trim).filter(part -> !Strings.isNullOrEmpty(part));
   }

   private ParameterParser() {
      throw new AssertionError();
   }
}
