package de.nbscloud.files;

import de.nbscloud.files.config.FilesConfig;
import de.nbscloud.files.controller.FilesController;
import de.nbscloud.files.exception.FileSystemServiceException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.overviewproject.mime_types.GetBytesException;
import org.overviewproject.mime_types.MimeTypeDetector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

@Service
public class FileSystemService {

    private static final Logger logger = LoggerFactory.getLogger(FileSystemService.class);

    public static record ContentContainer(boolean directory, Path path, String name, long size,
                                          LocalDateTime lastModified) {
    }

    public enum SortOrder {
        /**
         * First by type (directories first), then by name ignoring case
         */
        NATURAL(Comparator.comparing(ContentContainer::directory)
                          .reversed()
                          .thenComparing(con -> con.path().getFileName().toString().toLowerCase())),

        NATURAL_REVERSE(NATURAL.comparator.reversed()),

        SIZE_DESC(Comparator.comparing(ContentContainer::size).reversed()),

        SIZE_ASC(Comparator.comparing(ContentContainer::size)),

        LAST_MOD_DESC(Comparator.comparing(ContentContainer::lastModified).reversed()),

        LAST_MOD_ASC(Comparator.comparing(ContentContainer::lastModified));

        private Comparator<ContentContainer> comparator;

        SortOrder(Comparator<ContentContainer> comparator) {
            this.comparator = comparator;
        }

        public static SortOrder ofQueryParam(String param) {
            return Arrays.stream(values()).filter(so -> so.name().equalsIgnoreCase(param)).findFirst()
                         .orElse(SortOrder.NATURAL);
        }
    }

    public enum MimeTypeFilter {
        IMAGES("image/jpeg", "image/png"),
        VIDEOS("video/mpeg", "video/mp4", "video/quicktime", "video/x-msvideo"),
        AUDIO("audio/mpeg", "audio/x-wav", "audio/x-flac", "audio/flac", "audio/MPA", "audio/mpa-robust");

        private List<String> mimeTypes;

        MimeTypeFilter(String... mimeTypes) {
            this.mimeTypes = Arrays.asList(mimeTypes);
        }

        public boolean match(String mimeType) {
            return this.mimeTypes.contains(mimeType);
        }
    }

    @Autowired
    private FilesConfig filesConfig;

    @Autowired
    private LocationTracker locationTracker;

    private MimeTypeDetector mimeTypeDetector = new MimeTypeDetector();

    public Path createDirectory(String name) {
        try {
            return Files.createDirectories(this.locationTracker.getCurrentLocation().resolve(name));
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not create directory", e);
        }
    }

    public void createFile(String name, byte[] content) {
        try {
            Files.write(this.locationTracker.getCurrentLocation()
                                            .resolve(name), content, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not create file", e);
        }
    }

    public void createFile(Path path, byte[] content) {
        try {
            Files.write(path, content, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not create file", e);
        }
    }

    public boolean delete(String name) {
        try {
            // TODO does only delete dirs if they are empty - but maybe we want that?
            return Files.deleteIfExists(this.locationTracker.getCurrentLocation().resolve(name));
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not delete file", e);
        }
    }

    public Path move(Path originalPath, Path newPath) {
        try {
            return Files.move(originalPath, newPath, StandardCopyOption.ATOMIC_MOVE);
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not move file", e);
        }
    }

    public byte[] get(String name) {
        try {
            return Files.readAllBytes(this.locationTracker.getCurrentLocation().resolve(name));
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not get file", e);
        }
    }

    public void stream(String name, OutputStream outputStream) {
        try {
            Files.copy(this.locationTracker.getCurrentLocation().resolve(name), outputStream);
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not get file", e);
        }
    }

    public InputStream stream(String name) {
        try {
            return Files.newInputStream(this.locationTracker.getCurrentLocation().resolve(name));
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not get file", e);
        }
    }

    public long getSize(String name) {
        try {
            return Files.size(this.locationTracker.getCurrentLocation().resolve(name));
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not get file", e);
        }
    }

    public String getMimeType(String name) {
        try {
            final String detectedMimeType = this.mimeTypeDetector.detectMimeType(this.locationTracker.getCurrentLocation()
                                                                                                     .resolve(name));

            logger.debug("Detected mime type {} for file {}", detectedMimeType, name);

            return detectedMimeType;
        } catch (GetBytesException e) {
            throw new FileSystemServiceException("Could not get mime type", e);
        }
    }

    public List<String> getFilesWithMimeType(MimeTypeFilter mimeTypeFilter) {
        try {
            final Map<String, CompletableFuture<String>> fileFutureMap = new HashMap<>();

            Files.list(this.locationTracker.getCurrentLocation())
                 .filter(Files::isRegularFile)
                 .forEach(path -> {
                     fileFutureMap.put(path.getFileName().toString(), this.mimeTypeDetector.detectMimeTypeAsync(path)
                                                                                           .toCompletableFuture());
                 });

            CompletableFuture.allOf(fileFutureMap.values().toArray(new CompletableFuture[0])).join();

            return fileFutureMap.entrySet().stream().filter(e -> {
                try {
                    return mimeTypeFilter.match(e.getValue().get());
                } catch (InterruptedException | ExecutionException ex) {
                    throw new FileSystemServiceException("Could not determine mime types", ex);
                }
            }).map(Map.Entry::getKey).collect(Collectors.toList());
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not get file", e);
        }
    }

    public List<ContentContainer> list(SortOrder sortOrder) {
        try {
            List<ContentContainer> contentList = Files.list(this.locationTracker.getCurrentLocation())
                                                      .filter(path -> {
                                                          boolean ok = Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.isDirectory(path);

                                                          try {
                                                              return ok && (this.filesConfig.isFilterHidden() ? !Files.isHidden(path) : true);
                                                          } catch (IOException e) {
                                                              throw new FileSystemServiceException("Could not filter files", e);
                                                          }
                                                      })
                                                      .map(path -> {
                                                          try {
                                                              final boolean isDir = Files.isDirectory(path);

                                                              return new ContentContainer(isDir,
                                                                      this.locationTracker.getRelativeToBaseDir(path),
                                                                      path.getFileName().toString(),
                                                                      isDir ? FileUtils.sizeOfDirectory(path.toFile()) : Files.size(path),
                                                                      LocalDateTime.ofInstant(Files.getLastModifiedTime(path)
                                                                                                   .toInstant(), ZoneId.systemDefault()));
                                                          } catch (IOException e) {
                                                              throw new FileSystemServiceException("Could not list files", e);
                                                          }
                                                      })
                                                      .collect(Collectors.toList());
            contentList.sort(sortOrder.comparator);

            return contentList;
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not list files", e);
        }
    }

    public List<String> collectDirs(String sourceFile) {
        try {
            final List<String> resultList = new ArrayList<>();
            final Path sourcePath = this.locationTracker.getCurrentLocation().resolve(sourceFile);
            final boolean sourceIsDir = Files.isDirectory(sourcePath);

            Files.walkFileTree(this.locationTracker.getBaseDirPath(), new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    super.visitFile(dir, attrs);

                    if (sourceIsDir && sourcePath.equals(dir)) {
                        return FileVisitResult.SKIP_SUBTREE;
                    }

                    if (filesConfig.isFilterHidden() ? Files.isHidden(dir) : false) {
                        return FileVisitResult.SKIP_SUBTREE;
                    }

                    if (dir.toString().contains(filesConfig.getTrashBinName())) {
                        return FileVisitResult.SKIP_SUBTREE;
                    }

                    if (dir.toString().contains(filesConfig.getSharesName())) {
                        return FileVisitResult.SKIP_SUBTREE;
                    }

                    final String relPath = locationTracker.getRelativeToBaseDir(dir).toString();

                    resultList.add("/" + relPath);

                    return FileVisitResult.CONTINUE;
                }

                public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
                    return FileVisitResult.SKIP_SUBTREE;
                }

                public FileVisitResult visitFileFailed(Path file, IOException exc) {
                    return FileVisitResult.SKIP_SUBTREE;
                }
            });

            Collections.sort(resultList);
            resultList.sort((s1, s2) -> {
                if (StringUtils.equals(s1, "/")) {
                    return 1;
                }
                if (StringUtils.equals(s2, "/")) {
                    return 1;
                }

                return 0;
            });

            return resultList;
        } catch (IOException e) {
            throw new FileSystemServiceException("Could not list files", e);
        }
    }
}
