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

import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_FILE_STATUS;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.SET_REVIEWER;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.SET_STATUS_APPROVED;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.SET_STATUS_UNDER_APPROVAL;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

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.acl.custom.dossier.DossierACLService;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotAllowedException;
import com.iqser.red.service.persistence.management.v1.processor.roles.ApplicationRoles;
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.FileStatusManagementService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.AuditPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.NotificationPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.users.UserService;
import com.iqser.red.service.persistence.service.v1.api.external.resource.StatusResource;
import com.iqser.red.service.persistence.service.v1.api.shared.model.AuditCategory;
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttributes;
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileStatus;
import com.iqser.red.service.persistence.service.v1.api.shared.model.audit.AddNotificationRequest;
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.dossier.file.FileModel;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.ProcessingStatus;
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.notification.NotificationType;
import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequiredArgsConstructor
public class StatusController implements StatusResource {

    private static final Set<String> VALID_MEMBER_ROLES = Set.of(ApplicationRoles.RED_USER_ROLE, ApplicationRoles.RED_MANAGER_ROLE);
    private static final String DOSSIER_ID = "dossierId";
    private static final String FILE_ID = "fileId";
    private static final String FILE_NAME = "fileName";
    private final FileStatusManagementService fileStatusManagementService;
    private final UserService userService;
    private final DossierManagementService dossierManagementService;
    private final AuditPersistenceService auditPersistenceService;
    private final AccessControlService accessControlService;
    private final NotificationPersistenceService notificationPersistenceService;
    private final DossierACLService dossierACLService;


    @Override
    @PreAuthorize("hasAuthority('" + READ_FILE_STATUS + "')")
    public JSONPrimitive<Boolean> hasChangesSince(@PathVariable(DOSSIER_ID) String dossierId, @RequestBody JSONPrimitive<OffsetDateTime> since) {

        try {
            accessControlService.verifyUserHasViewPermissions(dossierId);
        } catch (AccessDeniedException e) {
            return JSONPrimitive.of(false);
        }
        return fileStatusManagementService.hasChangesSince(dossierId, since);
    }


    @Override
    @PreAuthorize("hasAuthority('" + READ_FILE_STATUS + "')")
    public Map<String, List<FileStatus>> getDossierStatus(@RequestBody List<String> dossierIds) {

        Map<String, List<FileStatus>> response = new HashMap<>();
        for (String dossierId : dossierIds) {
            try {
                accessControlService.verifyUserHasViewPermissions(dossierId);
                List<FileStatus> statusList = fileStatusManagementService.getDossierStatus(dossierId)
                        .stream()
                        .filter(fileStatus -> !fileStatus.isSoftOrHardDeleted())
                        .map(this::convert)
                        .collect(Collectors.toList());
                response.put(dossierId, statusList);
            } catch (AccessDeniedException e) {
                continue;
            }
        }

        return response;
    }


    @Override
    @PreAuthorize("hasAuthority('" + READ_FILE_STATUS + "')")
    @Transactional
    public Map<String, List<FileStatus>> getSoftDeletedFilesForDossiers(@RequestBody List<String> dossierIds) {

        List<FileStatus> statusList;
        List<String> dossiersWithViewPermissions = new ArrayList<>();
        for (var dossierId : dossierIds) {
            try {
                accessControlService.verifyUserHasViewPermissions(dossierId);
                dossiersWithViewPermissions.add(dossierId);
            } catch (AccessDeniedException e) {
                continue;
            }
        }
        if (dossiersWithViewPermissions.isEmpty()) {
            return new HashMap<>();
        }
        statusList = fileStatusManagementService.getSoftDeletedForDossierList(dossiersWithViewPermissions).stream().map(this::convert).collect(Collectors.toList());

        return statusList.stream().collect(Collectors.groupingBy(FileStatus::getDossierId, Collectors.toList()));
    }


    @PreAuthorize("hasAuthority('" + READ_FILE_STATUS + "')")
    public List<FileStatus> getDossierStatus(@PathVariable(DOSSIER_ID) String dossierId) {

        try {

            accessControlService.verifyUserHasViewPermissions(dossierId);
            return fileStatusManagementService.getDossierStatus(dossierId)
                    .stream()
                    .filter(fileStatus -> !fileStatus.isSoftOrHardDeleted())
                    .map(this::convert)
                    .collect(Collectors.toList());

        } catch (AccessDeniedException e) {
            return new ArrayList<>();
        }

    }


    @PreAuthorize("hasAuthority('" + READ_FILE_STATUS + "')")
    public FileStatus getFileStatus(@PathVariable(DOSSIER_ID) String dossierId, @PathVariable(FILE_ID) String fileId) {

        return convert(fileStatusManagementService.getFileStatus(fileId));
    }


    @Override
    @PreAuthorize("hasAuthority('" + SET_REVIEWER + "')")
    public void setCurrentFileAssignee(@PathVariable(DOSSIER_ID) String dossierId,
                                       @PathVariable(FILE_ID) String fileId,
                                       @RequestParam(name = ASSIGNEE_ID_REQUEST_PARAM, required = false) String assigneeId) {

        accessControlService.verifyUserIsMemberOrApprover(dossierId);

        log.debug("Requested [setFileReviewer] for dossier: {} / file: {} / reviewer: {}", dossierId, fileId, assigneeId);

        validateAssignee(dossierId, fileId, assigneeId);

        var fileStatus = fileStatusManagementService.getFileStatus(fileId);

        log.debug("Obtained status: {}", fileStatus);

        fileStatusManagementService.setCurrentFileAssignee(dossierId, fileId, assigneeId);
        if (assigneeId == null) {
            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(KeycloakSecurity.getUserId())
                    .objectId(fileId)
                    .category(AuditCategory.DOCUMENT.name())
                    .message("Reviewer was unassigned from document")
                    .details(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, FILE_NAME, fileStatus.getFilename()))
                    .build());
        } else {
            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(KeycloakSecurity.getUserId())
                    .objectId(fileId)
                    .category(AuditCategory.DOCUMENT.name())
                    .message("Reviewer was assigned to document")
                    .details(Map.of(DOSSIER_ID, dossierId, "reviewer", assigneeId))
                    .build());
        }

        if (assigneeId != null && !assigneeId.equals(KeycloakSecurity.getUserId())) {
            notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                    .userId(assigneeId)
                    .issuerId(KeycloakSecurity.getUserId())
                    .notificationType(NotificationType.ASSIGN_REVIEWER.name())
                    .target(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, FILE_NAME, fileStatus.getFilename()))
                    .build());
        }

        if (assigneeId == null || fileStatus.getAssignee() != null && !fileStatus.getAssignee().equals(assigneeId) && !KeycloakSecurity.getUserId()
                .equals(fileStatus.getAssignee())) {
            notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                    .userId(fileStatus.getAssignee())
                    .issuerId(KeycloakSecurity.getUserId())
                    .notificationType(NotificationType.UNASSIGNED_FROM_FILE.name())
                    .target(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, FILE_NAME, fileStatus.getFilename()))
                    .build());
        }

    }


    private void validateAssignee(String dossierId, String fileId, String assigneeId) {

        if (assigneeId != null) {
            var user = userService.getUserById(assigneeId);
            if (user.isEmpty()) {
                userService.removeDeletedUsers(Collections.singleton(assigneeId));
                throw new BadRequestException("Unknown user=" + assigneeId);
            }
            // check he has a manager role, thus he can be the owner
            if (user.get().getRoles().stream().noneMatch(VALID_MEMBER_ROLES::contains)) {
                throw new BadRequestException("Make sure each provided member id has the permission to be a member of a dossier.");
            }
            // check assignee is a member
            accessControlService.verifyUserIsMemberOrApprover(dossierId, assigneeId);
        } else {
            // check if file is already unassigned (cannot unassign an unassigned file)
            if (fileStatusManagementService.getFileStatus(fileId).getAssignee() == null) {
                throw new BadRequestException("File is already unassigned!");
            }
        }
    }


    @PreAuthorize("hasAuthority('" + SET_REVIEWER + "')")
    public void setStatusUnderReview(@PathVariable(DOSSIER_ID) String dossierId,
                                     @PathVariable(FILE_ID) String fileId,
                                     @RequestParam(value = ASSIGNEE_ID_REQUEST_PARAM, required = false) String assigneeId) {

        var fileStatus = fileStatusManagementService.getFileStatus(fileId);

        setStatusUnderReviewForFile(dossierId, fileId, assigneeId);
        auditPersistenceService.audit(AuditRequest.builder()
                .userId(KeycloakSecurity.getUserId())
                .objectId(fileId)
                .category(AuditCategory.DOCUMENT.name())
                .message("Document status was changed to Under Review")
                .details(Map.of(DOSSIER_ID, dossierId))
                .build());

        if (assigneeId != null && !assigneeId.equals(KeycloakSecurity.getUserId())) {
            notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                    .userId(assigneeId)
                    .issuerId(KeycloakSecurity.getUserId())
                    .notificationType(NotificationType.ASSIGN_REVIEWER.name())
                    .target(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, FILE_NAME, fileStatus.getFilename()))
                    .build());
        }

        generatePossibleUnassignedFromFileNotification(dossierId, fileId, fileStatus, assigneeId);
    }


    @PreAuthorize("hasAuthority('" + SET_STATUS_UNDER_APPROVAL + "')")
    public void setStatusUnderApproval(@PathVariable(DOSSIER_ID) String dossierId,
                                       @PathVariable(FILE_ID) String fileId,
                                       @RequestParam(name = ASSIGNEE_ID_REQUEST_PARAM, required = false) String assigneeId) {

        var fileStatus = fileStatusManagementService.getFileStatus(fileId);

        setStatusUnderApprovalForFile(dossierId, fileId, assigneeId);
        auditPersistenceService.audit(AuditRequest.builder()
                .userId(KeycloakSecurity.getUserId())
                .objectId(fileId)
                .category(AuditCategory.DOCUMENT.name())
                .message("Document status was changed to Under Approval")
                .details(Map.of(DOSSIER_ID, dossierId))
                .build());

        if (assigneeId != null && !assigneeId.equals(KeycloakSecurity.getUserId())) {

            notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                    .userId(assigneeId)
                    .issuerId(KeycloakSecurity.getUserId())
                    .notificationType(NotificationType.ASSIGN_APPROVER.name())
                    .target(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, FILE_NAME, fileStatus.getFilename()))
                    .build());
        }

        generatePossibleUnassignedFromFileNotification(dossierId, fileId, fileStatus, assigneeId);
    }


    @PreAuthorize("hasAuthority('" + SET_STATUS_APPROVED + "')")
    public void setStatusApproved(@PathVariable(DOSSIER_ID) String dossierId, @PathVariable(FILE_ID) String fileId) {

        accessControlService.verifyUserIsApprover(dossierId);
        setStatusApprovedForFile(dossierId, fileId);
        auditPersistenceService.audit(AuditRequest.builder()
                .userId(KeycloakSecurity.getUserId())
                .objectId(fileId)
                .category(AuditCategory.DOCUMENT.name())
                .message("Document status was changed to Approved")
                .details(Map.of(DOSSIER_ID, dossierId))
                .build());

        var dossier = dossierACLService.enhanceDossierWithACLData(dossierManagementService.getDossierById(dossierId, false, false));
        if (!dossier.getOwnerId().equals(KeycloakSecurity.getUserId())) {

            var fileStatus = fileStatusManagementService.getFileStatus(fileId);

            notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                    .userId(dossier.getOwnerId())
                    .issuerId(KeycloakSecurity.getUserId())
                    .notificationType(NotificationType.DOCUMENT_APPROVED.name())
                    .target(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, FILE_NAME, fileStatus.getFilename()))
                    .build());
        }
    }


    @Override
    @PreAuthorize("hasAuthority('" + SET_REVIEWER + "')")
    public void setAssigneeForList(@PathVariable(DOSSIER_ID) String dossierId,
                                   @RequestParam(value = ASSIGNEE_ID_REQUEST_PARAM, required = false) String assigneeId,
                                   @RequestBody List<String> fileIds) {

        fileIds.forEach(fileId -> setCurrentFileAssignee(dossierId, fileId, assigneeId));
    }


    @Override
    @PreAuthorize("hasAuthority('" + SET_REVIEWER + "')")
    public void setStatusUnderReviewForList(@PathVariable(DOSSIER_ID) String dossierId,
                                            @RequestBody List<String> fileIds,
                                            @RequestParam(value = ASSIGNEE_ID_REQUEST_PARAM, required = false) String assigneeId) {

        fileIds.forEach(fileId -> setStatusUnderReview(dossierId, fileId, assigneeId));

    }


    private void setStatusUnderReviewForFile(String dossierId, String fileId, String assigneeId) {

        validateAssignee(dossierId, fileId, assigneeId);
        fileStatusManagementService.setStatusUnderReview(dossierId, fileId, assigneeId);
    }


    private void generatePossibleUnassignedFromFileNotification(String dossierId, String fileId, FileModel oldFileStatus, String newAssigneeId) {

        if (oldFileStatus.getAssignee() != null && !oldFileStatus.getAssignee().equals(newAssigneeId) && !KeycloakSecurity.getUserId().equals(oldFileStatus.getAssignee())) {
            notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                    .userId(oldFileStatus.getAssignee())
                    .issuerId(KeycloakSecurity.getUserId())
                    .notificationType(NotificationType.UNASSIGNED_FROM_FILE.name())
                    .target(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, FILE_NAME, oldFileStatus.getFilename()))
                    .build());
        }
    }


    @Override
    @PreAuthorize("hasAuthority('" + SET_STATUS_UNDER_APPROVAL + "')")
    public void setStatusUnderApprovalForList(@PathVariable(DOSSIER_ID) String dossierId,
                                              @RequestBody List<String> fileIds,
                                              @RequestParam(value = ASSIGNEE_ID_REQUEST_PARAM, required = false) String assigneeId) {

        fileIds.forEach(fileId -> setStatusUnderApproval(dossierId, fileId, assigneeId));

    }


    private void setStatusUnderApprovalForFile(String dossierId, String fileId, String assigneeId) {

        accessControlService.verifyUserIsReviewerOrApprover(dossierId, fileId);

        var dossier = dossierACLService.enhanceDossierWithACLData(dossierManagementService.getDossierById(dossierId, false, false));

        if (assigneeId != null && !dossier.getApproverIds().contains(assigneeId)) {
            throw new BadRequestException("Approver is not valid");
        }

        fileStatusManagementService.setStatusUnderApproval(dossierId, fileId, assigneeId);
    }


    @Override
    @PreAuthorize("hasAuthority('" + SET_STATUS_APPROVED + "')")
    public void setStatusApprovedForList(String dossierId, List<String> fileIds) {

        accessControlService.verifyUserIsApprover(dossierId);

        dossierManagementService.getDossierById(dossierId, false, false);

        fileIds.forEach(fileId -> setStatusApproved(dossierId, fileId));
    }


    @PreAuthorize("hasAuthority('" + SET_REVIEWER + "')")
    public void setStatusNewForList(@PathVariable(DOSSIER_ID) String dossierId, @RequestBody List<String> fileIds) {

        for (var fileId : fileIds) {
            accessControlService.verifyUserIsReviewerOrApprover(dossierId, fileId);
            var fileStatus = fileStatusManagementService.getFileStatus(fileId);
            if (!WorkflowStatus.UNDER_REVIEW.equals(fileStatus.getWorkflowStatus())) {
                log.debug("Transition to NEW status is not possible from: " + fileStatus.getWorkflowStatus());
                break;
            } else {
                fileStatusManagementService.setCurrentFileAssignee(dossierId, fileId, null);
                fileStatusManagementService.setStatusNew(dossierId, fileId);
                generatePossibleUnassignedFromFileNotification(dossierId, fileId, fileStatus, null);
            }
        }

    }


    @PreAuthorize("hasAuthority('" + READ_FILE_STATUS + "')")
    public List<FileStatus> getSoftDeletedDossierStatus(@PathVariable(DOSSIER_ID) String dossierId) {

        try {
            accessControlService.verifyUserHasViewPermissions(dossierId);
            return fileStatusManagementService.getSoftDeletedDossierStatus(dossierId).stream().map(this::convert).collect(Collectors.toList());

        } catch (AccessDeniedException e) {
            return new ArrayList<>();
        }
    }


    private void setStatusApprovedForFile(String dossierId, String fileId) {

        var fileStatus = getFileStatus(dossierId, fileId);
        if (fileStatus.isHasSuggestions()) {
            throw new NotAllowedException("File contains unapproved requests.");
        }
        if (!fileStatus.getProcessingStatus().equals(ProcessingStatus.PROCESSED)) {
            throw new NotAllowedException("File is not processed.");
        }
        fileStatusManagementService.setStatusApproved(dossierId, fileId, KeycloakSecurity.getUserId());
    }


    private FileStatus convert(FileModel status) {

        return FileStatus.builder()
                .dossierId(status.getDossierId())
                .dossierArchived(status.isDossierArchived())
                .dossierStatusId(status.getDossierStatusId())
                .dossierTemplateId(status.getDossierTemplateId())
                .fileId(status.getId())
                .filename(status.getFilename())
                .processingStatus(ProcessingStatus.valueOf(status.getProcessingStatus().name()))
                .workflowStatus(WorkflowStatus.valueOf(status.getWorkflowStatus().name()))
                .numberOfPages(status.getNumberOfPages())
                .added(status.getAdded())
                .lastUpdated(status.getLastUpdated())
                .numberOfAnalyses(status.getNumberOfAnalyses())
                .assignee(status.getAssignee())
                .lastReviewer(status.getLastReviewer())
                .lastApprover(status.getLastApprover())
                .hasUpdates(status.isHasUpdates())
                .hasImages(status.isHasImages())
                .hasRequests(status.isHasSuggestions())
                .hasSuggestions(status.isHasSuggestions())
                .excludedFromAutomaticAnalysis(status.isExcludedFromAutomaticAnalysis())
                .hasHints(status.isHasHints())
                .hasRedactions(status.isHasRedactions())
                .ocrEndTime(status.getOcrEndTime())
                .ocrStartTime(status.getOcrStartTime())
                .numberOfOCRedPages(status.getNumberOfOCRedPages() != null ? status.getNumberOfOCRedPages() : 0)
                .numberOfPagesToOCR(status.getNumberOfPagesToOCR() != null ? status.getNumberOfPagesToOCR() : 0)
                .hasAnnotationComments(status.isHasAnnotationComments())
                .uploader(status.getUploader())
                .dictionaryVersion(status.getDictionaryVersion())
                .rulesVersion(status.getRulesVersion())
                .componentRulesVersion(status.getComponentRulesVersion())
                .legalBasisVersion(status.getLegalBasisVersion())
                .lastProcessed(status.getLastProcessed())
                .lastLayoutProcessed(status.getLastLayoutProcessed())
                .approvalDate(status.getApprovalDate())
                .lastUploaded(status.getLastUploaded())
                .analysisDuration(status.getAnalysisDuration())
                .fileAttributes(new FileAttributes(status.getFileAttributes()))
                .dossierDictionaryVersion(status.getDossierDictionaryVersion())
                .excluded(status.isExcluded())
                .excludedPages(status.getExcludedPages())
                .softDeletedTime(status.getDeleted())
                .analysisRequired(status.isAnalysisRequired())
                .lastFileAttributeChange(status.getLastFileAttributeChange())
                .redactionModificationDate(status.getRedactionModificationDate())
                .fileManipulationDate(status.getFileManipulationDate())
                .lastManualChangeDate(status.getLastManualChangeDate())
                .hasHighlights(status.isHasHighlights())
                .lastIndexed(status.getLastIndexed())
                .fileSize(status.getFileSize())
                .fileErrorInfo(status.getFileErrorInfo())
                .build();

    }

}
