package com.iqser.red.persistence.service.v1.external.api.impl.controller;

import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.DELETE_FILE;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.DOWNLOAD_ORIGINAL_FILE;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.ROTATE_PAGE;
import static com.iqser.red.service.persistence.management.v1.processor.service.FeignExceptionHandler.processFeignException;
import static com.iqser.red.service.persistence.management.v1.processor.utils.DownloadBufferUtils.fileProxyStreamForDownload;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.iqser.red.service.persistence.management.v1.processor.client.pdftronredactionservice.PDFTronClient;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.exception.InternalServerErrorException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotAllowedException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.AccessControlService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileManagementStorageService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusManagementService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusService;
import com.iqser.red.service.persistence.management.v1.processor.service.ReanalysisService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.AuditPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.utils.StorageIdUtils;
import com.iqser.red.service.persistence.management.v1.processor.utils.StringEncodingUtils;
import com.iqser.red.service.persistence.service.v1.api.external.resource.FileManagementResource;
import com.iqser.red.service.persistence.service.v1.api.shared.model.AuditCategory;
import com.iqser.red.service.persistence.service.v1.api.shared.model.RotatePagesRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.audit.AuditRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileModel;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
import com.iqser.red.storage.commons.exception.StorageException;
import com.iqser.red.storage.commons.exception.StorageObjectDoesNotExist;
import com.iqser.red.storage.commons.service.StorageService;
import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity;
import com.knecon.fforesight.tenantcommons.TenantContext;

import feign.FeignException;
import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequiredArgsConstructor
public class FileManagementController implements FileManagementResource {

    private static final String DOWNLOAD_HEADER_NAME = "Content-Disposition";

    private final FileService fileService;
    private final AuditPersistenceService auditPersistenceService;
    private final AccessControlService accessControlService;
    private final PDFTronClient pdfTronClient;
    private final ReanalysisService reanalysisService;
    private final StorageService storageService;
    private final FileStatusManagementService fileStatusManagementService;
    private final FileStatusService fileStatusService;
    private final FileManagementStorageService fileManagementStorageService;


    @Timed
    @Override
    @PreAuthorize("hasAuthority('" + DELETE_FILE + "')")
    public void deleteFile(@PathVariable(DOSSIER_ID) String dossierId, @PathVariable(FILE_ID) String fileId) {

        fileService.deleteFile(dossierId, fileId);
        auditPersistenceService.audit(AuditRequest.builder()
                .userId(KeycloakSecurity.getUserId())
                .objectId(dossierId)
                .category(AuditCategory.DOSSIER.name())
                .message("File has been deleted.")
                .details(Map.of("fileId", fileId))
                .build());
    }


    @Timed
    @Override
    @PreAuthorize("hasAuthority('" + DELETE_FILE + "')")
    public void deleteFiles(@PathVariable(DOSSIER_ID) String dossierId, @RequestBody List<String> fileIds) {

        List<String> errorIds = new ArrayList<>();
        for (String fileId : fileIds) {
            try {
                fileService.deleteFile(dossierId, fileId);
                auditPersistenceService.audit(AuditRequest.builder()
                        .userId(KeycloakSecurity.getUserId())
                        .objectId(dossierId)
                        .category(AuditCategory.DOSSIER.name())
                        .message("Files have been deleted.")
                        .details(Map.of("Size", fileIds.size()))
                        .build());
            } catch (Exception e) {
                errorIds.add(fileId);
            }
        }
        if (!errorIds.isEmpty()) {
            throw new BadRequestException("Failed to delete files with ids: " + errorIds);
        }
    }


    @Timed
    @Override
    @PreAuthorize("hasAuthority('" + DOWNLOAD_ORIGINAL_FILE + "')")
    public ResponseEntity<?> downloadOriginal(@PathVariable(DOSSIER_ID) String dossierId,
                                              @PathVariable(FILE_ID) String fileId,
                                              @RequestParam(value = "inline", required = false, defaultValue = FALSE) boolean inline) {

        return getResponseEntityForPDFDocument(fileId, dossierId, FileType.ORIGIN, inline);
    }


    @Timed
    @Override
    @PreAuthorize("hasAuthority('" + DOWNLOAD_ORIGINAL_FILE + "')")
    public ResponseEntity<?> downloadViewerDocument(@PathVariable(DOSSIER_ID) String dossierId,
                                                    @PathVariable(FILE_ID) String fileId,
                                                    @RequestParam(value = "inline", required = false, defaultValue = FALSE) boolean inline) {

        // Viewer Document Returns
        if (storageService.objectExists(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.VIEWER_DOCUMENT))) {
            return getResponseEntityForPDFDocument(fileId, dossierId, FileType.VIEWER_DOCUMENT, inline);
        } else {
            return getResponseEntityForPDFDocument(fileId, dossierId, FileType.ORIGIN, inline);
        }
    }


    private ResponseEntity<?> getResponseEntityForPDFDocument(String fileId, String dossierId, FileType fileType, boolean inline) {

        String storageId = StorageIdUtils.getStorageId(dossierId, fileId, fileType);
        try {
            var file = fileStatusManagementService.getFileStatus(fileId);
            var pdfFileStream = fileManagementStorageService.getObject(TenantContext.getTenantId(), storageId);
            return getResponseEntity(inline, pdfFileStream, file.getFilename());
        } catch (StorageObjectDoesNotExist e) {
            throw new NotFoundException(String.format("File \"%s\" not found!, the dossier with ID \"%s\" probably does not exist.", storageId, dossierId));
        } catch (StorageException e) {
            log.debug(e.getMessage(), e);
            throw new InternalServerErrorException(e.getMessage());
        }
    }


    @SneakyThrows
    private ResponseEntity<?> getResponseEntity(boolean inline, InputStream resource, String filename) {

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_PDF);

        if (filename != null) {
            httpHeaders.add(DOWNLOAD_HEADER_NAME, inline ? "inline" : "attachment" + "; filename*=utf-8''" + StringEncodingUtils.urlEncode(filename));
        }

        return new ResponseEntity<>(fileProxyStreamForDownload(resource), httpHeaders, HttpStatus.OK);

    }


    @Override
    @PreAuthorize("hasAuthority('" + DELETE_FILE + "')")
    public void hardDeleteFiles(@PathVariable(DOSSIER_ID) String dossierId, @RequestParam(FILE_IDS) Set<String> fileIds) {

        for (String fileId : fileIds) {
            if (fileStatusManagementService.getFileStatus(fileId).getAssignee() != null) {
                accessControlService.verifyUserIsReviewerOrApprover(dossierId, fileId);
            }
        }
        fileService.hardDeleteFiles(dossierId, fileIds);
        auditPersistenceService.audit(AuditRequest.builder()
                .userId(KeycloakSecurity.getUserId())
                .objectId(dossierId)
                .category(AuditCategory.DOSSIER.name())
                .message("Files has been hard deleted.")
                .details(Map.of("FileIds", fileIds))
                .build());
    }


    @Override
    @PreAuthorize("hasAuthority('" + DELETE_FILE + "')")
    public void restoreFiles(@PathVariable(DOSSIER_ID) String dossierId, @RequestBody Set<String> fileIds) {

        verifyUserIsDossierOwnerOrApproverOrAssignedReviewer(dossierId, fileIds);
        fileService.undeleteFiles(dossierId, fileIds);
        auditPersistenceService.audit(AuditRequest.builder()
                .userId(KeycloakSecurity.getUserId())
                .objectId(dossierId)
                .category(AuditCategory.DOSSIER.name())
                .message("Files has been restored.")
                .details(Map.of("FileIds", fileIds))
                .build());
    }


    @Override
    @PreAuthorize("hasAuthority('" + ROTATE_PAGE + "')")
    public void rotatePages(@PathVariable(DOSSIER_ID) String dossierId, @PathVariable(FILE_ID) String fileId, @RequestBody RotatePagesRequest rotatePagesRequest) {

        accessControlService.verifyFileIsNotApproved(dossierId, fileId);
        accessControlService.verifyUserIsReviewer(dossierId, fileId);

        try {
            pdfTronClient.rotate(com.iqser.red.service.pdftron.redaction.v1.api.model.RotatePagesRequest.builder()
                    .dossierId(dossierId)
                    .fileId(fileId)
                    .pages(rotatePagesRequest.getPages())
                    .build());

            fileStatusManagementService.updateFileModificationDate(fileId);

            FileModel fileModel = fileStatusManagementService.getFileStatus(fileId);

            if (!fileModel.isExcludedFromAutomaticAnalysis()) {
                if (fileModel.getOcrStartTime() != null || fileModel.getOcrEndTime() != null) {
                    reanalysisService.ocrFile(dossierId, fileId, true);
                } else {
                    fileStatusService.setStatusFullReprocess(dossierId, fileId, true, true);
                }
            }

            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(KeycloakSecurity.getUserId())
                    .objectId(dossierId)
                    .category(AuditCategory.DOCUMENT.name())
                    .message("Pages have been rotated.")
                    .details(Map.of("Pages", rotatePagesRequest.getPages().keySet()))
                    .build());

        } catch (FeignException e) {
            throw processFeignException(e);
        }
    }


    private void verifyUserIsDossierOwnerOrApproverOrAssignedReviewer(String dossierId, Set<String> fileIds) {

        try {
            accessControlService.verifyUserIsDossierOwnerOrApprover(dossierId);
        } catch (AccessDeniedException e1) {
            try {
                for (String fileId : fileIds) {
                    accessControlService.verifyUserIsReviewer(dossierId, fileId);
                }
            } catch (NotAllowedException e2) {
                throw new NotAllowedException("User must be dossier owner, approver or assigned reviewer of the file.");
            }
        }
    }

}
