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

import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.PROCESS_DOWNLOAD;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_DOWNLOAD_STATUS;

import java.io.BufferedInputStream;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
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.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 org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.iqser.red.persistence.service.v1.external.api.impl.service.OneTimeTokenService;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
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.DossierManagementService;
import com.iqser.red.service.persistence.management.v1.processor.service.DownloadService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileManagementStorageService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.AuditPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.utils.StringEncodingUtils;
import com.iqser.red.service.persistence.service.v1.api.external.resource.DownloadResource;
import com.iqser.red.service.persistence.service.v1.api.shared.model.AuditCategory;
import com.iqser.red.service.persistence.service.v1.api.shared.model.DownloadResponse;
import com.iqser.red.service.persistence.service.v1.api.shared.model.DownloadStatusResponse;
import com.iqser.red.service.persistence.service.v1.api.shared.model.PrepareDownloadRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.PrepareDownloadWithOptionRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.RemoveDownloadRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.audit.AuditRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSONPrimitive;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.DownloadFileType;
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.WorkflowStatus;
import com.iqser.red.service.persistence.service.v1.api.shared.model.download.DownloadRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.download.DownloadStatus;
import com.iqser.red.service.persistence.service.v1.api.shared.model.download.DownloadWithOptionRequest;
import com.iqser.red.storage.commons.service.StorageService;
import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity;
import com.knecon.fforesight.tenantcommons.TenantContext;

import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequiredArgsConstructor
public class DownloadController implements DownloadResource {

    private static final Pattern COLOR_PATTERN = Pattern.compile("^#[\\da-f]{6,6}$");

    private final DossierManagementService dossierService;
    private final FileStatusService fileStatusService;
    private final DownloadService downloadService;
    private final StorageService storageService;
    private final AuditPersistenceService auditPersistenceService;
    private final OneTimeTokenService oneTimeTokenDownloadService;
    private final AccessControlService accessControlService;
    private final FileManagementStorageService fileManagementStorageService;

    private final String REPORT_INFO = "/REPORT_INFO.json";

    @Value("${storage.backend}")
    private String storageBackend;


    @PreAuthorize("hasAuthority('" + PROCESS_DOWNLOAD + "')")
    public DownloadResponse prepareDownload(@RequestBody PrepareDownloadRequest request) {

        // check the user is non-member or reviewer
        accessControlService.verifyUserIsDossierOwnerOrApprover(request.getDossierId());
        var response = downloadService.prepareDownload(convert(request));
        auditPersistenceService.insertRecord(AuditRequest.builder()
                .userId(KeycloakSecurity.getUserId())
                .objectId(response.getValue())
                .category(AuditCategory.DOWNLOAD.name())
                .message("Download was prepared")
                .details(Map.of("dossierId", request.getDossierId()))
                .build());
        return new DownloadResponse(response.getValue());
    }


    @PreAuthorize("hasAuthority('" + PROCESS_DOWNLOAD + "')")
    public DownloadResponse prepareDownload(@RequestBody PrepareDownloadWithOptionRequest request) {

        validateDossierId(request.getDossierId());

        validateAndFilterFileIds(request);

        if ((request.getDownloadFileTypes() == null || request.getDownloadFileTypes().isEmpty()) && (request.getReportTemplateIds() == null || request.getReportTemplateIds()
                .isEmpty())) {
            throw new BadRequestException("Download and report types cannot both be empty");
        }

        if (request.getRedactionPreviewColor() != null && !COLOR_PATTERN.matcher(request.getRedactionPreviewColor()).matches()) {
            throw new BadRequestException("The specified redaction-preview-color is malformed.");
        }

        var response = downloadService.prepareDownload(convert(request));
        auditPersistenceService.audit(AuditRequest.builder()
                .userId(KeycloakSecurity.getUserId())
                .objectId(response.getValue())
                .category(AuditCategory.DOWNLOAD.name())
                .message("Download was prepared")
                .details(Map.of("dossierId", request.getDossierId()))
                .build());
        return new DownloadResponse(response.getValue());
    }


    private void validateDossierId(String dossierId) {

        if (StringUtils.isBlank(dossierId)) {
            throw new BadRequestException("Empty dossier id");
        }
        dossierService.getDossierById(dossierId, true, true);
        accessControlService.verifyUserIsDossierOwnerOrApprover(dossierId);
    }


    private void validateAndFilterFileIds(PrepareDownloadWithOptionRequest request) {

        List<FileModel> validFiles = fileStatusService.getDossierStatus(request.getDossierId());
        var fileIds = request.getFileIds();
        if (fileIds != null && !fileIds.isEmpty()) { // validate the ids provided
            validFiles = validFiles.stream().filter(f -> fileIds.contains(f.getId())).collect(Collectors.toList());
            if (validFiles.isEmpty()) {
                throw new NotFoundException("No file id provided is found");
            }
        } // otherwise consider the files from dossier

        var validFilesAndNotProcessed = validFiles.stream().filter(f -> !(f.getAnalysisVersion() > 0 && f.getNumberOfAnalyses() > 0)).collect(Collectors.toList());
        if (!validFilesAndNotProcessed.isEmpty()) {
            throw new BadRequestException("At least a file is in its initial analysis process");
        }

        request.setFileIds(validFiles.stream().map(FileModel::getId).collect(Collectors.toList()));
        var approvedFiles = validFiles.stream().filter(f -> f.getWorkflowStatus().equals(WorkflowStatus.APPROVED)).toList();
        // special corner case: unapproved files, no reports and only REDACTED type selected
        if (approvedFiles.isEmpty() && (request.getReportTemplateIds() == null || request.getReportTemplateIds()
                .isEmpty()) && request.getDownloadFileTypes() != null && request.getDownloadFileTypes().size() == 1 && request.getDownloadFileTypes()
                    .contains(DownloadFileType.REDACTED)) {
            throw new BadRequestException("Unapproved files in redacted state with no reports cannot be included");
        }
    }


    private DownloadWithOptionRequest convert(PrepareDownloadWithOptionRequest request) {

        return DownloadWithOptionRequest.builder()
                .dossierId(request.getDossierId())
                .userId(KeycloakSecurity.getUserId())
                .fileIds(request.getFileIds())
                .downloadFileTypes(request.getDownloadFileTypes())
                .reportTemplateIds(request.getReportTemplateIds())
                .redactionPreviewColor(request.getRedactionPreviewColor())
                .build();
    }


    @Override
    @PreAuthorize("hasAuthority('" + PROCESS_DOWNLOAD + "')")
    public void deleteDownloadStatus(@RequestBody RemoveDownloadRequest removeDownloadRequest) {

        removeDownloadRequest.getStorageIds().forEach(storageId -> {
            downloadService.deleteDownloadStatus(JSONPrimitive.of(storageId));

            fileManagementStorageService.deleteObject(storageId);

            if (storageBackend.equals("s3")) {
                var storageIdForS3 = generateReportJsonStorageIdForS3(storageId);
                log.info("Deleting Report Json from S3 Storage {}", storageIdForS3);
                fileManagementStorageService.deleteObject(storageIdForS3);
            } else {
                var storageIdForAzure = generateReportJsonStorageIdForAzure(storageId);
                log.info("Deleting Report Json from Azure Storage {}", storageIdForAzure);
                fileManagementStorageService.deleteObject(storageIdForAzure);
            }

            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(KeycloakSecurity.getUserId())
                    .objectId(storageId)
                    .category(AuditCategory.DOWNLOAD.name())
                    .message("Remove Prepared Download")
                    .build());
        });

    }


    @PreAuthorize("hasAuthority('" + READ_DOWNLOAD_STATUS + "')")
    public DownloadStatusResponse getDownloadStatus() {

        var resp = downloadService.getDownloadStatus(KeycloakSecurity.getUserId());
        return new DownloadStatusResponse(resp);
    }


    @SneakyThrows
    @PreAuthorize("hasAuthority('" + PROCESS_DOWNLOAD + "')")
    public void downloadFile(@RequestParam(STORAGE_ID) String storageId) {

        var requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = ((ServletRequestAttributes) requestAttributes).getResponse();

        var userId = KeycloakSecurity.getUserId();
        var downloadStatus = getDownloadStatus(storageId, userId);
        var fileDownloadStream = getFileForDownload(storageId, userId);

        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", "attachment" + "; filename*=utf-8''" + StringEncodingUtils.urlEncode(downloadStatus.getFilename()));
        response.setHeader("Content-Length", String.valueOf(downloadStatus.getFileSize()));

        org.apache.commons.io.IOUtils.copyLarge(fileDownloadStream.getInputStream(), response.getOutputStream());
        response.flushBuffer();
    }


    private DownloadStatus getDownloadStatus(String storageId, String userId) {
        // TODO Add endpoint to get single download status for userId and storageId.
        var downloadStatusResponse = downloadService.getDownloadStatus(userId);
        Optional<DownloadStatus> downloadStatusOptional = downloadStatusResponse.stream().filter(ds -> ds.getStorageId().equals(storageId)).findFirst();

        return downloadStatusOptional.orElseThrow(() -> new NotFoundException("Download status not found for this user"));
    }


    private InputStreamResource getFileForDownload(String storageId, String userId) {

        try {
            var response = storageService.getObject(TenantContext.getTenantId(), storageId);

            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(userId)
                    .objectId(storageId)
                    .category(AuditCategory.DOWNLOAD.name())
                    .message("File was downloaded.")
                    .build());
            downloadService.setDownloaded(JSONPrimitive.of(storageId));

            return new InputStreamResource(new BufferedInputStream(response.getInputStream()));
        } catch (Exception e) {
            throw new NotFoundException(e.getMessage(), e);
        }
    }


    @Override
    @PreAuthorize("hasAuthority('" + PROCESS_DOWNLOAD + "')")
    public JSONPrimitive<String> generateOneTimeToken(@RequestBody JSONPrimitive<String> storageIdWrapper) {

        log.debug("Generate one time token");
        return JSONPrimitive.of(oneTimeTokenDownloadService.createToken(storageIdWrapper.getValue(), KeycloakSecurity.getUserId()).getTokenId());
    }


    @Override
    @SneakyThrows
    public void downloadFileUsingOTT(@PathVariable(OTT) String oneTimeToken,
                                     @RequestParam(value = "tenantId") String tenantId) {

        TenantContext.setTenantId(tenantId);
        var token = oneTimeTokenDownloadService.getToken(oneTimeToken);
        var requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = ((ServletRequestAttributes) requestAttributes).getResponse();

        var downloadStatus = getDownloadStatus(token.getStorageId(), token.getUserId());
        var fileDownloadStream = getFileForDownload(token.getStorageId(), token.getUserId());

        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", "attachment" + "; filename*=utf-8''" + StringEncodingUtils.urlEncode(downloadStatus.getFilename()));
        response.setHeader("Content-Length", String.valueOf(downloadStatus.getFileSize()));

        org.apache.commons.io.IOUtils.copyLarge(fileDownloadStream.getInputStream(), response.getOutputStream());
        response.flushBuffer();
    }


    private DownloadRequest convert(PrepareDownloadRequest request) {

        return DownloadRequest.builder().dossierId(request.getDossierId()).userId(KeycloakSecurity.getUserId()).fileIds(request.getFileIds()).build();
    }


    private String generateReportJsonStorageIdForS3(String storageId) {

        return storageId.substring(0, storageId.length() - 3) + REPORT_INFO;
    }


    private String generateReportJsonStorageIdForAzure(String storageId) {

        return storageId.substring(0, storageId.length() - 4) + REPORT_INFO;
    }

}
