package com.airlock.iam.application.webserver.infrastructure.deploy;

import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.webresources.DirResourceSet;

public final class SpaResourceSets extends DirResourceSet {
   private static final ELogger LOG = ELoggerFactory.getLogger(SpaResourceSets.class);
   private final Optional<Duration> cacheReloadDelay;
   private final SpaCacheReloadManager spaCacheReloadManager;
   private final DirectoryWatcher directoryWatcher;
   private final ConcurrentMap<File, SpaResourceSet> fileToResourceSet = new ConcurrentHashMap<>();

   public SpaResourceSets(WebResourceRoot root, String webAppMount, String base, String internalPath) {
      super(root, webAppMount, base, internalPath);
      this.cacheReloadDelay = Optional.empty();
      this.spaCacheReloadManager = new SpaCacheReloadManager();
      File baseDir = new File(base);
      this.directoryWatcher = new DirectoryWatcher(baseDir.toPath(), this.collectResourceSets(baseDir, SpaResourceSets::logErrorMessage));
      this.collectResourceSets(baseDir, SpaResourceSets::throwIllegalStateException).run();
   }

   SpaResourceSets(WebResourceRoot root, String webAppMount, String base, String internalPath, Duration cacheReloadDelay) {
      super(root, webAppMount, base, internalPath);
      this.cacheReloadDelay = Optional.of(cacheReloadDelay);
      this.spaCacheReloadManager = new SpaCacheReloadManager(cacheReloadDelay);
      File baseDir = new File(base);
      this.directoryWatcher = new DirectoryWatcher(baseDir.toPath(), this.collectResourceSets(baseDir, SpaResourceSets::logErrorMessage));
      this.collectResourceSets(baseDir, SpaResourceSets::throwIllegalStateException).run();
   }

   private Runnable collectResourceSets(File baseDir, Consumer<String> failureHandler) {
      return () -> {
         Map<File, SpaResourceSet> newResourceSets = this.findNewResourceSets(baseDir, failureHandler);
         if (!newResourceSets.isEmpty()) {
            this.fileToResourceSet.putAll(newResourceSets);
            this.spaCacheReloadManager.setCacheReloadAttributeWithDelay(this.getRoot().getContext().getServletContext());
         }
      };
   }

   private Map<File, SpaResourceSet> findNewResourceSets(File baseDirectory, Consumer<String> failureHandler) {
      List<File> resourceSetFiles = new ArrayList<>();
      this.traverseFolderRecursively(baseDirectory, resourceSetFiles);
      Set<String> duplicateFileNames = getDuplicateFileNames(resourceSetFiles);
      if (!duplicateFileNames.isEmpty()) {
         failureHandler.accept(
            String.format(
               "UI resource set directory '%s' contains duplicate file names: '%s'. Make sure that the resource set files have unique names.",
               this.getBase(),
               String.join("', '", duplicateFileNames)
            )
         );
         return Collections.emptyMap();
      } else {
         List<File> newResourceSets = resourceSetFiles.stream().filter(file -> !this.fileToResourceSet.containsKey(file)).toList();
         return this.createResourceSets(newResourceSets);
      }
   }

   private void traverseFolderRecursively(File currentDirectory, List<File> resourceSetFiles) {
      File[] filesInDir = currentDirectory.listFiles();
      if (filesInDir != null && filesInDir.length != 0) {
         List<File> zipResourceSetFiles = Arrays.stream(filesInDir).filter(SpaResourceSets::isValidZipResourceSet).toList();
         resourceSetFiles.addAll(zipResourceSetFiles);
         List<File> dirResourceSetFiles = Arrays.stream(filesInDir).filter(SpaResourceSets::isValidDirResourceSet).toList();
         resourceSetFiles.addAll(dirResourceSetFiles);
         Arrays.stream(filesInDir).filter(SpaResourceSets::isTraversableFolder).forEach(file -> this.traverseFolderRecursively(file, resourceSetFiles));
      }
   }

   private static boolean isValidZipResourceSet(File file) {
      return file.getAbsolutePath().endsWith(".zip");
   }

   private static boolean isValidDirResourceSet(File file) {
      File[] folderContent = file.listFiles();
      return folderContent != null && folderContent.length > 0 && folderHasValidDirResourceSetFolderStructure(folderContent);
   }

   private static boolean folderHasValidDirResourceSetFolderStructure(File[] folderContent) {
      Optional<File> assetsFolder = Arrays.stream(folderContent).filter(file -> file.getName().equals("assets")).findFirst();
      if (assetsFolder.isEmpty()) {
         return false;
      }

      File[] assetsFolderContent = assetsFolder.get().listFiles();
      return assetsFolderContent != null && assetsFolderContent.length != 0
         ? Arrays.stream(assetsFolderContent).anyMatch(file -> file.getName().equals("custom"))
         : false;
   }

   private static boolean isTraversableFolder(File file) {
      return !isValidZipResourceSet(file) && !isValidDirResourceSet(file);
   }

   private static Set<String> getDuplicateFileNames(List<File> resourceSetFiles) {
      Set<String> uniqueFileNames = new HashSet<>();
      return resourceSetFiles.stream().map(SpaResourceSets::getNameWithoutFileExtension).filter(n -> !uniqueFileNames.add(n)).collect(Collectors.toSet());
   }

   private static String getNameWithoutFileExtension(File file) {
      return file.getName().replace(".zip", "");
   }

   private ConcurrentMap<File, SpaResourceSet> createResourceSets(List<File> resourceSetFiles) {
      ConcurrentMap<File, SpaResourceSet> resourceSetEntries = this.createFileToSpaResourceSetEntries(
         resourceSetFiles, File::isDirectory, this::createNewDirResourceSet
      );
      resourceSetEntries.putAll(this.createFileToSpaResourceSetEntries(resourceSetFiles, File::isFile, this::createNewZipResourceSet));
      return resourceSetEntries;
   }

   private ConcurrentMap<File, SpaResourceSet> createFileToSpaResourceSetEntries(
      List<File> resourceSetFiles, Predicate<File> spaResourceSetDistinguisher, Function<File, SpaResourceSet> spaResourceSetBuilder
   ) {
      return resourceSetFiles.stream().filter(spaResourceSetDistinguisher).collect(Collectors.toConcurrentMap(UnaryOperator.identity(), spaResourceSetBuilder));
   }

   private SpaResourceSet createNewDirResourceSet(File file) {
      return SpaResourceSet.spaDirResourceSet(
         this.getRoot(), appendResourceSetNameToWebAppMount(this.getWebAppMount(), getNameWithoutFileExtension(file)), file.getAbsolutePath()
      );
   }

   private static String appendResourceSetNameToWebAppMount(String webAppMount, String resourceSetName) {
      return webAppMount + "/" + resourceSetName;
   }

   private SpaResourceSet createNewZipResourceSet(File zipFile) {
      Duration reloadDelay = this.cacheReloadDelay.orElse(SpaCacheReloadManager.DEFAULT_CACHE_RELOAD_DELAY);
      return SpaResourceSet.spaZipResourceSet(
         this.getRoot(), appendResourceSetNameToWebAppMount(this.getWebAppMount(), getNameWithoutFileExtension(zipFile)), zipFile.toPath(), reloadDelay
      );
   }

   private static void throwIllegalStateException(String errorMessage) {
      throw new IllegalStateException(errorMessage);
   }

   private static void logErrorMessage(String errorMessage) {
      LOG.error(EDefaultLogType.GENERIC_LOG_TYPE, String.format("%s ERROR - SpaResourceSets: %s", Instant.now().toString(), errorMessage));
   }
}
