package com.airlock.iam.admin.application.service.logs;

import com.airlock.iam.admin.domain.model.logs.LogFileInfo;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.domain.model.logs.Log;
import com.airlock.iam.common.domain.service.logs.LogParser;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;

public final class LogService {
   private static final ELogger LOG = ELoggerFactory.getLogger(LogService.class);
   private static final Pattern LEADING_AND_TRAILING_QUOTES_PATTERN = Pattern.compile("^\"|\"$");
   private final List<LogFileInfo> logFiles;
   private final LogParser parser;

   public Optional<LogFileInfo> findLogFile(@NonNull String logFileName) {
      if (logFileName == null) {
         throw new NullPointerException("logFileName is marked non-null but is null");
      } else {
         return this.logFiles.stream().filter(file -> file.key().equals(logFileName)).findFirst();
      }
   }

   public List<Log> getLogs(LogSearchProperties searchProperties) {
      LOG.debug(String.format("Starting the following log search: %s", LogUtil.logSafeQuote(searchProperties.toString())));
      if (StringUtils.isBlank(searchProperties.logFile().filePath())) {
         throw new IllegalArgumentException("log file name is null or empty");
      }

      if (searchProperties.numberOfMessages() <= 0) {
         return Collections.emptyList();
      }

      List<Log> logs = this.getLogs(searchProperties, searchProperties.logFile(), getFilterPatterns(searchProperties.filterPattern()));
      sortLogs(logs, searchProperties);
      return logs;
   }

   public List<String> getRawLogs(LogSearchProperties searchProperties) {
      return this.getLogs(searchProperties).stream().filter(log -> log.getRawMessageLines() != null).flatMap(log -> log.getRawMessageLines().stream()).toList();
   }

   public boolean areValidPatterns(LogSearchProperties search) {
      try {
         getFilterPatterns(search.filterPattern());
         return true;
      } catch (PatternSyntaxException e) {
         return false;
      }
   }

   private List<Log> getLogs(LogSearchProperties search, LogFileInfo log, List<Pattern> filterPatterns) {
      int fileSuffix = 0;
      int messagesFetched = 0;
      List<Log> result = new ArrayList<>();

      while (messagesFetched < search.numberOfMessages()) {
         String file = filename(log.filePath(), fileSuffix);
         if (!hasLogFile(file)) {
            LOG.debug(String.format("No log file found at %s", file));
            break;
         }

         List<String> logLines = getLogLines(file, log.charset(), filterPatterns);
         LOG.debug(String.format("Found %d log lines", logLines.size()));
         List<Log> parsedLines = this.parser.parse(logLines);
         LOG.debug(String.format("Parsed %d log lines", parsedLines.size()));
         result.addAll(0, parsedLines);
         messagesFetched += parsedLines.size();
         fileSuffix++;
      }

      Collections.reverse(result);
      return result.subList(0, Math.min(result.size(), search.numberOfMessages()));
   }

   private static List<String> getLogLines(String filePath, Charset charset, List<Pattern> filterPatterns) {
      Path file = Path.of(filePath);
      List<String> result = new ArrayList<>();

      try (BufferedReader br = Files.newBufferedReader(file, charset)) {
         for (String line = br.readLine(); line != null; line = br.readLine()) {
            if (matchesAllPatterns(filterPatterns, line)) {
               result.add(line);
            }
         }

         return result;
      } catch (IOException e) {
         throw new IllegalStateException("Error while reading file at: " + filePath, e);
      }
   }

   private static boolean matchesAllPatterns(List<Pattern> patterns, String line) {
      for (Pattern p : patterns) {
         if (!p.matcher(line).find()) {
            return false;
         }
      }

      return true;
   }

   private static boolean hasLogFile(String filePath) {
      File file = new File(filePath);
      if (!file.exists()) {
         return false;
      } else if (!file.canRead()) {
         throw new IllegalStateException("Cannot read file at: " + filePath);
      } else {
         return true;
      }
   }

   private static String filename(String path, int index) {
      return index == 0 ? path : path + "." + index;
   }

   private static List<Pattern> getFilterPatterns(String filterPattern) {
      if (!StringUtils.isNotEmpty(filterPattern)) {
         return Collections.emptyList();
      }

      List<Pattern> patterns = new ArrayList<>();
      Matcher matcher = Pattern.compile("\"[^\"]+\"|\\S+", 256).matcher(filterPattern);

      while (matcher.find()) {
         LOG.debug(String.format("Adding filter pattern: %s", LogUtil.logSafeQuote(filterPattern)));
         patterns.add(Pattern.compile(escapeUserSearch(removeSurroundingQuotes(matcher.group())), 2));
      }

      return patterns;
   }

   private static String escapeUserSearch(String pattern) {
      return pattern.startsWith("uid:") ? Pattern.quote(pattern + " - ") : pattern;
   }

   private static String removeSurroundingQuotes(String pattern) {
      return LEADING_AND_TRAILING_QUOTES_PATTERN.matcher(pattern).replaceAll("");
   }

   private static void sortLogs(List<Log> logs, LogSearchProperties search) {
      if (search.sortBy() != LogSort.NONE) {
         logs.sort(new LogComparator(search.sortBy(), search.sortAscending()));
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static LogService.LogServiceBuilder builder() {
      return new LogService.LogServiceBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public LogService(List<LogFileInfo> logFiles, LogParser parser) {
      this.logFiles = logFiles;
      this.parser = parser;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class LogServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private List<LogFileInfo> logFiles;
      @SuppressFBWarnings(justification = "generated code")
      private LogParser parser;

      @SuppressFBWarnings(justification = "generated code")
      LogServiceBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public LogService.LogServiceBuilder logFiles(List<LogFileInfo> logFiles) {
         this.logFiles = logFiles;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public LogService.LogServiceBuilder parser(LogParser parser) {
         this.parser = parser;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public LogService build() {
         return new LogService(this.logFiles, this.parser);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "LogService.LogServiceBuilder(logFiles=" + this.logFiles + ", parser=" + this.parser + ")";
      }
   }
}
