package de.nbscloud.files.controller;

import de.nbscloud.files.FileSystemService;
import de.nbscloud.files.LocationTracker;
import de.nbscloud.files.config.FilesConfig;
import de.nbscloud.files.exception.FileSystemServiceException;
import de.nbscloud.files.form.RenameForm;
import de.nbscloud.webcontainer.registry.AppRegistry;
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.UriUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Controller
public class FilesController implements InitializingBean {

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

    @Autowired
    private FilesConfig filesConfig;
    @Autowired
    private LocationTracker locationTracker;
    @Autowired
    private WebContainerSharedConfig webContainerSharedConfig;
    @Autowired
    private FileSystemService fileSystemService;
    @Autowired
    private AppRegistry appRegistry;

    // We have to temporarily store messages as we redirect: in the manipulation methods
    // so everything we add to the model will be gone
    private final List<String> errors = new ArrayList<>();
    private final List<String> shareInfo = new ArrayList<>();
    private final List<String> infoMessages = new ArrayList<>();

    @GetMapping("/files/browse/**")
    public String start(Model model, HttpServletRequest httpServletRequest, String sortOrder) {
        final FileSystemService.SortOrder order = FileSystemService.SortOrder.ofQueryParam(sortOrder);

        updateLocation(httpServletRequest);

        model.addAttribute("errors", getAndClear(this.errors));
        model.addAttribute("infoMessages", getAndClear(this.infoMessages));
        model.addAttribute("shareInfo", getAndClear(this.shareInfo));
        model.addAttribute("currentLocation", getCurrentLocationPrefixed());
        model.addAttribute("content", getContent(order));
        model.addAttribute("sortOrder", order);
        model.addAttribute("apps", this.appRegistry.getAll());
        this.webContainerSharedConfig.addDefaults(model);

        return "files/filesIndex";
    }

    @GetMapping("/files/delete")
    public String deleteFile(String filename) {
        if (this.filesConfig.isUseTrashBin() && !this.locationTracker.isTrashBin()) {
            try {
                // Soft delete
                this.fileSystemService.move(
                        this.locationTracker.getCurrentLocation().resolve(Path.of(filename)),
                        this.locationTracker.getTrashBin().resolve(filename));

                this.infoMessages.add("nbscloud.files.delete.success");
            } catch (RuntimeException e) {
                logger.error("Could not soft delete file", e);

                this.errors.add(e.getMessage());
            }
        } else {
            // Hard delete
            this.fileSystemService.delete(filename);

            this.infoMessages.add("nbscloud.files.delete.success");
        }

        return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
    }

    @GetMapping("/files/rename")
    public String rename(Model model, String filename) {
        model.addAttribute("currentLocation", getCurrentLocationPrefixed());
        model.addAttribute("form", new RenameForm(filename, getCurrentLocation(), filename));
        model.addAttribute("targetDirs", this.fileSystemService.collectDirs(filename));
        model.addAttribute("apps", this.appRegistry.getAll());
        this.webContainerSharedConfig.addDefaults(model);

        return "files/rename";
    }

    @PostMapping("/files/doRename")
    public String doRename(RenameForm form) {
        final Path sourcePath = this.locationTracker.getCurrentLocation().resolve(Path.of(form.getOriginalFilename()));

        // We navigate to the target dir, so we display the content of that dir after the move
        this.locationTracker.setCurrentLocation(form.getTargetDir().substring(1));

        final Path targetPath = this.locationTracker.getCurrentLocation().resolve(Path.of(form.getFilename()));

        try {
            this.fileSystemService.move(sourcePath, targetPath);

            this.infoMessages.add("nbscloud.files.rename.success");
        } catch (RuntimeException e) {
            logger.error("Could not rename file", e);

            this.errors.add(e.getMessage());
        }

        return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
    }

    @GetMapping("/files/download")
    public ResponseEntity<Resource> downloadFile(String filename) {
        // TODO download of directories via .zip? Also needs to be streaming

        try {
            return ResponseEntity.ok()
                                 .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
                                 .header(HttpHeaders.CONTENT_TYPE, this.fileSystemService.getMimeType(filename))
                                 .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(this.fileSystemService.getSize(filename)))
                                 .body(new InputStreamResource(this.fileSystemService.stream(filename)));
        } catch (RuntimeException e) {
            logger.error("Could not get file", e);

            return ResponseEntity.internalServerError().body(null);
        }
    }

    @PostMapping("/files/upload")
    public String uploadFiles(@RequestParam("files") MultipartFile[] files) {
        for (MultipartFile file : files) {
            try {
                this.fileSystemService.createFile(file.getOriginalFilename(), file.getBytes());

                this.infoMessages.add("nbscloud.files.created.file.success");
            } catch (IOException e) {
                logger.error("Could not upload file", e);

                this.errors.add(e.getMessage());
            }
        }

        return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
    }

    @PostMapping("/files/createDir")
    public String createDir(@RequestParam("dirName") String dirName) {
        try {
            this.fileSystemService.createDirectory(dirName);

            this.infoMessages.add("nbscloud.files.created.dir.success");
        } catch (RuntimeException e) {
            logger.error("Could not create dir", e);

            this.errors.add(e.getMessage());
        }

        return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
    }

    @GetMapping("/files/share")
    public String share(String filename) {
        final Path filePath = this.locationTracker.getRelativeToBaseDir(this.locationTracker.getCurrentLocation()
                                                                                            .resolve(filename));
        final String shareUuid = UUID.randomUUID().toString();

        try {
            this.fileSystemService.createFile(this.locationTracker.getBaseDirPath()
                                                                  .resolve(this.filesConfig.getSharesName())
                                                                  .resolve(shareUuid), filePath.toString()
                                                                                               .getBytes(StandardCharsets.UTF_8));

            this.shareInfo.add("/files/shares?shareUuid=" + shareUuid);
        } catch (RuntimeException e) {
            logger.error("Could not share file", e);

            this.errors.add(e.getMessage());
        }


        return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
    }

    @GetMapping("files/shares")
    public ResponseEntity shares(String shareUuid) {
        try {
            final String sharedFile = new String(this.fileSystemService.get(this.locationTracker.getBaseDirPath()
                                                                                                .resolve(this.filesConfig.getSharesName())
                                                                                                .resolve(shareUuid)
                                                                                                .toString()));
            final Path sharedFilePath = this.locationTracker.getBaseDirPath().resolve(sharedFile);
            final String filename = sharedFilePath.getFileName().toString();

            return ResponseEntity.ok()
                                 .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
                                 .header(HttpHeaders.CONTENT_TYPE, this.fileSystemService.getMimeType(filename))
                                 .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(this.fileSystemService.getSize(filename)))
                                 .body(new InputStreamResource(this.fileSystemService.stream(filename)));
        } catch (RuntimeException e) {
            logger.error("Could not get shared file", e);

            return ResponseEntity.internalServerError().body(null);
        }
    }

    @GetMapping("files/preview")
    public void preview(HttpServletResponse response, String filename) {
        try {
            response.setContentType(this.fileSystemService.getMimeType(filename));
            response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(this.fileSystemService.getSize(filename)));
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "filename=\"" + filename + "\"");

            this.fileSystemService.stream(filename, response.getOutputStream());
        } catch (FileSystemServiceException | IOException e) {
            logger.error("Could not preview file", e);
        }
    }

    @GetMapping("files/gallery")
    public String gallery(Model model) {
        try {
            final List<String> files = this.fileSystemService.getFilesWithMimeType(FileSystemService.MimeTypeFilter.IMAGES);

            model.addAttribute("files", files);
            model.addAttribute("apps", this.appRegistry.getAll());
            this.webContainerSharedConfig.addDefaults(model);

            return "files/gallery";
        } catch (FileSystemServiceException e) {
            logger.error("Could not generate gallery", e);

            this.errors.add(e.getMessage());

            return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
        }
    }

    private List<String> getAndClear(List<String> source) {
        final List<String> retList = new ArrayList<>(source);

        source.clear();

        return retList;
    }

    private List<FileSystemService.ContentContainer> getContent(FileSystemService.SortOrder order) {
        final List<FileSystemService.ContentContainer> contentList = this.fileSystemService.list(order);

        if (!this.locationTracker.isBasePath()) {
            contentList.add(0,
                    new FileSystemService.ContentContainer(true,
                            this.locationTracker.getParent(),
                            "..",
                            0L,
                            LocalDateTime.ofInstant(Instant.EPOCH, ZoneId.of("UTC"))));
        }

        return contentList;
    }

    private void updateLocation(HttpServletRequest httpServletRequest) {
        final String requestUrl = httpServletRequest.getRequestURL().toString();
        final String[] split = requestUrl.split("/files/browse");

        if (split.length > 1) {
            this.locationTracker.setCurrentLocation(UriUtils.decode(split[1].substring(1), StandardCharsets.UTF_8));
        } else {
            this.locationTracker.reset();
        }
    }

    private String getCurrentLocation() {
        if (this.locationTracker.getRelativeLocation().toString().isEmpty()) {
            return "/";
        }

        return this.locationTracker.getRelativeLocation().toString();
    }

    private String getCurrentLocationPrefixed() {
        if (this.locationTracker.getRelativeLocation().toString().isEmpty()) {
            return "/";
        }

        return "/" + this.locationTracker.getRelativeLocation().toString();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.locationTracker.init();

        if (this.filesConfig.isUseTrashBin()) {
            try {
                this.fileSystemService.createDirectory(this.filesConfig.getTrashBinName());
            } catch (FileSystemServiceException e) {
                if (!FileAlreadyExistsException.class.equals(e.getCause().getClass())) {
                    throw e;
                }
                // else: do nothing
            }
        }
        try {
            this.fileSystemService.createDirectory(this.filesConfig.getSharesName());
        } catch (FileSystemServiceException e) {
            if (!FileAlreadyExistsException.class.equals(e.getCause().getClass())) {
                throw e;
            }
            // else: do nothing
        }
    }
}
