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

import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.ADD_UPDATE_DOSSIER;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.ARCHIVE_DOSSIER;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.DELETE_DOSSIER;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_DOSSIER;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.UNARCHIVE_DOSSIER;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
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.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
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.google.common.collect.Lists;
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.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.management.v1.processor.service.users.model.User;
import com.iqser.red.service.persistence.service.v1.api.external.resource.DossierResource;
import com.iqser.red.service.persistence.service.v1.api.shared.model.AuditCategory;
import com.iqser.red.service.persistence.service.v1.api.shared.model.DossierChangeEntry;
import com.iqser.red.service.persistence.service.v1.api.shared.model.DossierInformation;
import com.iqser.red.service.persistence.service.v1.api.shared.model.DossierRequest;
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.CreateOrUpdateDossierRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.Dossier;
import com.iqser.red.service.persistence.service.v1.api.shared.model.notification.NotificationType;
import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequiredArgsConstructor
public class DossierController implements DossierResource {

    private static final Set<String> VALID_MEMBER_ROLES = Set.of(ApplicationRoles.RED_USER_ROLE, ApplicationRoles.RED_MANAGER_ROLE);

    private final DossierManagementService dossierManagementService;
    private final UserService userService;
    private final FileStatusManagementService fileStatusManagementService;
    private final AuditPersistenceService auditPersistenceService;
    private final NotificationPersistenceService notificationPersistenceService;
    private final AccessControlService accessControlService;
    private final DossierACLService dossierACLService;


    @Override
    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    public DossierInformation getDossierInformation() {

        return dossierManagementService.getDossierInformation(dossierACLService.getDossierIdsWithViewPermission());
    }


    @Override
    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    @PostFilter("hasPermission(filterObject.dossierId, 'Dossier', 'VIEW_OBJECT')")
    public List<DossierChangeEntry> changesSince(@RequestBody JSONPrimitive<OffsetDateTime> since) {

        return dossierManagementService.changesSince(since)
                .stream()
                .map(d -> new DossierChangeEntry(d.getDossierId(), d.isDossierChanges(), d.isFileChanges()))
                .collect(Collectors.toList());
    }


    @Override
    @PreAuthorize("hasAuthority('" + ADD_UPDATE_DOSSIER + "') && (#dossierRequest.dossierId == null || hasPermission(#dossierRequest.dossierId, 'Dossier', 'ACCESS_OBJECT') )")
    public ResponseEntity<Dossier> createDossierOrUpdateDossier(@RequestBody DossierRequest dossierRequest) {

        if (dossierRequest.getDossierId() != null && dossierRequest.getOwnerId() == null) {
            throw new BadRequestException("Owner must be set for any update");
        }

        // this code always executes - members, approvers and owner must be valid
        String ownerId = getAndValidateOwnerId(dossierRequest.getOwnerId());
        Set<String> members = getAndValidateMembers(ownerId, dossierRequest.getMemberIds());
        Set<String> approvers = getAndValidateMembers(ownerId, dossierRequest.getApproverIds());
        members.addAll(approvers);

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

        // if dossierId is set - load dossier
        if (StringUtils.isNotEmpty(dossierRequest.getDossierId())) {

            Dossier existingDossier = dossierACLService.enhanceDossierWithACLData(dossierManagementService.getDossierById(dossierRequest.getDossierId(), true, false));

            if (existingDossier.getArchivedTime() != null) {
                checkValidityForArchivedDossierUpdateRequest(dossierRequest, existingDossier);
            }

            // update using data from request and computed owner/members/approvers
            Dossier updatedDossier = dossierManagementService.updateDossier(CreateOrUpdateDossierRequest.builder()
                    .dossierName(dossierRequest.getDossierName())
                    .description(dossierRequest.getDescription())
                    .dossierTemplateId(dossierRequest.getDossierTemplateId())
                    .downloadFileTypes(dossierRequest.getDownloadFileTypes())
                    .dueDate(dossierRequest.getDueDate())
                    .reportTemplateIds(new ArrayList<>(dossierRequest.getReportTemplateIds()))
                    .watermarkId(dossierRequest.getWatermarkId())
                    .previewWatermarkId(dossierRequest.getPreviewWatermarkId())
                    .dossierStatusId(dossierRequest.getDossierStatusId())
                    .build(), existingDossier.getId());

            dossierACLService.updateDossierACL(members, approvers, ownerId, updatedDossier.getId());
            dossierACLService.enhanceDossierWithACLData(updatedDossier);

            updateFileStatusForDossierFiles(updatedDossier.getId(), members);

            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(KeycloakSecurity.getUserId())
                    .objectId(updatedDossier.getId())
                    .category(AuditCategory.DOSSIER.name())
                    .message("Dossier has been updated.")
                    .build());

            if (existingDossier.getOwnerId() == null || !existingDossier.getOwnerId().equals(ownerId)) {
                if (ownerId != null && !ownerId.equals(KeycloakSecurity.getUserId())) {
                    notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                            .userId(ownerId)
                            .issuerId(KeycloakSecurity.getUserId())
                            .notificationType(NotificationType.DOSSIER_OWNER_SET.name())
                            .target(Map.of("dossierId", dossierRequest.getDossierId()))
                            .build());
                }
                if (existingDossier.getOwnerId() != null && !existingDossier.getOwnerId().equals(KeycloakSecurity.getUserId())) {
                    notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                            .userId(existingDossier.getOwnerId())
                            .issuerId(KeycloakSecurity.getUserId())
                            .notificationType(NotificationType.DOSSIER_OWNER_REMOVED.name())
                            .target(Map.of("dossierId", dossierRequest.getDossierId()))
                            .build());
                }
            }

            Set<String> uniqueMembers = new LinkedHashSet<>(members);
            uniqueMembers.addAll(approvers);

            uniqueMembers.stream()
                    .filter(member -> !member.equals(ownerId) && !member.equals(KeycloakSecurity.getUserId()) && (existingDossier.getMemberIds() == null || !existingDossier.getMemberIds()
                            .contains(member)))
                    .forEach(member -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                            .userId(member)
                            .issuerId(KeycloakSecurity.getUserId())
                            .notificationType(NotificationType.USER_BECOMES_DOSSIER_MEMBER.name())
                            .target(Map.of("dossierId", dossierRequest.getDossierId()))
                            .build()));

            if (existingDossier.getMemberIds() != null) {
                existingDossier.getMemberIds()
                        .stream()
                        .filter(member -> !members.contains(member) && !approvers.contains(member) && !member.equals(KeycloakSecurity.getUserId()))
                        .forEach(member -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                                .userId(member)
                                .issuerId(KeycloakSecurity.getUserId())
                                .notificationType(NotificationType.USER_REMOVED_AS_DOSSIER_MEMBER.name())
                                .target(Map.of("dossierId", dossierRequest.getDossierId()))
                                .build()));
            }

            approvers.stream()
                    .filter(approver -> !KeycloakSecurity.getUserId().equals(approver) && existingDossier.getMemberIds() != null && existingDossier.getMemberIds()
                            .contains(approver) && (existingDossier.getApproverIds() == null || !existingDossier.getApproverIds().contains(approver)))
                    .forEach(approver -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                            .userId(approver)
                            .issuerId(KeycloakSecurity.getUserId())
                            .notificationType(NotificationType.USER_PROMOTED_TO_APPROVER.name())
                            .target(Map.of("dossierId", dossierRequest.getDossierId()))
                            .build()));

            members.stream()
                    .filter(member -> !member.equals(KeycloakSecurity.getUserId()) && existingDossier.getApproverIds() != null && existingDossier.getApproverIds()
                            .contains(member) && !approvers.contains(member))
                    .forEach(member -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                            .userId(member)
                            .issuerId(KeycloakSecurity.getUserId())
                            .notificationType(NotificationType.USER_DEGRADED_TO_REVIEWER.name())
                            .target(Map.of("dossierId", dossierRequest.getDossierId()))
                            .build()));

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_JSON);
            return new ResponseEntity<>(updatedDossier, httpHeaders, HttpStatus.OK);

        } else {
            Dossier created = createNewDossier(dossierRequest, ownerId, members, approvers);

            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(KeycloakSecurity.getUserId())
                    .objectId(created.getId())
                    .category(AuditCategory.DOSSIER.name())
                    .message("Dossier has been created.")
                    .build());

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_JSON);
            return new ResponseEntity<>(created, httpHeaders, HttpStatus.CREATED);
        }

    }


    private String getAndValidateOwnerId(String ownerId) {

        String currentUserId = KeycloakSecurity.getUserId();
        // quick check if self, just proceed
        if (currentUserId.equals(ownerId)) {
            return currentUserId;
        }

        String actualOwnerId = ownerId == null ? currentUserId : ownerId;

        var user = userService.getUserById(actualOwnerId);
        if (user.isEmpty()) {
            userService.removeDeletedUsers(Collections.singleton(ownerId));
            actualOwnerId = null;
        }

        // check he has a manager role, thus he can be the owner
        if (user.isPresent() && user.get().getRoles().stream().noneMatch(ApplicationRoles.RED_MANAGER_ROLE::equals)) {
            throw new BadRequestException("Make sure provided user id has the manager role.");
        }

        return actualOwnerId;

    }


    private Set<String> getAndValidateMembers(String ownerId, Set<String> memberIds) {

        Set<String> actualMemberIds = memberIds == null ? new TreeSet<>() : memberIds;
        if (actualMemberIds.stream().anyMatch(Objects::isNull)) {
            throw new BadRequestException("Member IDs cannot be null");
        }

        // add owner automatically
        if (ownerId != null) {
            actualMemberIds.add(ownerId);
        }

        List<User> users = userService.getUsersByIds(actualMemberIds);
        if (users.size() != actualMemberIds.size()) {
            Set<String> deletedUserIds = userService.removeDeletedUsers(actualMemberIds);
            actualMemberIds.removeAll(deletedUserIds);
        }
        if (users.stream().anyMatch(u -> u.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.");
        }
        return actualMemberIds;
    }


    private void checkValidityForArchivedDossierUpdateRequest(DossierRequest updatedDossier, Dossier existingDossier) {
        //these must not be editted for archived dossiers
        checkEquality(updatedDossier.getDossierTemplateId(), existingDossier.getDossierTemplateId(), "dossier template id");
        checkEquality(updatedDossier.getDossierName(), existingDossier.getDossierName(), "dossier name");
        checkEquality(updatedDossier.getDescription(), existingDossier.getDescription(), "the description");
        checkEquality(updatedDossier.getDueDate(), existingDossier.getDueDate(), "the due date");
        checkEquality(updatedDossier.getDossierStatusId(), existingDossier.getDossierStatusId(), "dossier status id");
    }


    private void updateFileStatusForDossierFiles(String dossierId, Collection<String> members) {

        fileStatusManagementService.getDossierStatus(dossierId).stream().filter(fileStatus -> !fileStatus.isSoftOrHardDeleted()).forEach(f -> {

            if (f.getAssignee() != null && !members.contains(f.getAssignee())) {
                fileStatusManagementService.setCurrentFileAssignee(dossierId, f.getId(), null);
            }
        });

    }


    private Dossier createNewDossier(DossierRequest dossier, String ownerId, Set<String> members, Set<String> approvers) {

        Dossier newDossier = dossierManagementService.addDossier(CreateOrUpdateDossierRequest.builder()
                .dossierName(dossier.getDossierName())
                .description(dossier.getDescription())
                .dossierTemplateId(dossier.getDossierTemplateId())
                .downloadFileTypes(dossier.getDownloadFileTypes())
                .dueDate(dossier.getDueDate())
                .reportTemplateIds(dossier.getReportTemplateIds() != null ? new ArrayList<>(dossier.getReportTemplateIds()) : Lists.newArrayList())
                .watermarkId(dossier.getWatermarkId())
                .previewWatermarkId(dossier.getPreviewWatermarkId())
                .dossierStatusId(dossier.getDossierStatusId())
                .build());

        dossierACLService.updateDossierACL(members, approvers, ownerId, newDossier.getId());
        dossierACLService.enhanceDossierWithACLData(newDossier);

        return newDossier;

    }


    private <T> void checkEquality(T updatedValue, T existingValue, String message) {

        if (updatedValue == null && existingValue != null || updatedValue != null && existingValue == null) {
            throw new BadRequestException(String.format("You cannot edit %s in an archived dossier.", message));
        }
        if (updatedValue == null && existingValue == null) {
            return;
        }

        if (!updatedValue.equals(existingValue)) {
            throw new BadRequestException(String.format("You cannot edit %s in an archived dossier.", message));
        }
    }




    @PreAuthorize("hasAuthority('" + DELETE_DOSSIER + "') && hasPermission(#dossierId, 'Dossier', 'ACCESS_OBJECT')")
    public void deleteDossier(@PathVariable(DOSSIER_ID_PARAM) String dossierId) {

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

        dossierManagementService.delete(dossierId);

        auditPersistenceService.audit(AuditRequest.builder()
                .userId(KeycloakSecurity.getUserId())
                .objectId(dossierId)
                .category(AuditCategory.DOSSIER.name())
                .message("Dossier moved to trash.")
                .build());

        dossierToBeDeleted.getMemberIds()
                .stream()
                .filter(m -> !KeycloakSecurity.getUserId().equals(m))
                .forEach(member -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                        .userId(member)
                        .issuerId(KeycloakSecurity.getUserId())
                        .notificationType(NotificationType.DOSSIER_DELETED.name())
                        .target(Map.of("dossierId", dossierId, "dossierName", dossierToBeDeleted.getDossierName()))
                        .build()));

    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    @PostAuthorize("hasPermission(#dossierId, 'Dossier', 'VIEW_OBJECT')")
    public Dossier getDossier(@PathVariable(DOSSIER_ID_PARAM) String dossierId,
                              @RequestParam(name = INCLUDE_ARCHIVED_PARAM, defaultValue = "false", required = false) boolean includeArchived,
                              @RequestParam(name = INCLUDE_DELETED_PARAM, defaultValue = "false", required = false) boolean includeDeleted) {

        return dossierACLService.enhanceDossierWithACLData(dossierManagementService.getDossierById(dossierId, includeArchived, includeDeleted));
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    @PostFilter("hasPermission(filterObject.id, 'Dossier', 'VIEW_OBJECT')")
    public List<Dossier> getDossiers(@RequestParam(name = INCLUDE_ARCHIVED_PARAM, defaultValue = "false", required = false) boolean includeArchived,
                                     @RequestParam(name = INCLUDE_DELETED_PARAM, defaultValue = "false", required = false) boolean includeDeleted) {

        return dossierManagementService.getAllDossiers(includeArchived, includeDeleted).stream().map(dossierACLService::enhanceDossierWithACLData).collect(Collectors.toList());
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    @PostFilter("hasPermission(filterObject.id, 'Dossier', 'VIEW_OBJECT')")
    public List<Dossier> getDossiersForDossierTemplate(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId,
                                                       @RequestParam(name = INCLUDE_ARCHIVED_PARAM, defaultValue = "false", required = false) boolean includeArchived,
                                                       @RequestParam(name = INCLUDE_DELETED_PARAM, defaultValue = "false", required = false) boolean includeDeleted) {

        return dossierManagementService.getAllDossiersForDossierTemplateId(dossierTemplateId, includeArchived, includeDeleted)
                .stream()
                .map(dossierACLService::enhanceDossierWithACLData)
                .collect(Collectors.toList());
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    @PostFilter("hasPermission(filterObject.id, 'Dossier', 'VIEW_OBJECT')")
    public List<Dossier> getSoftDeletedDossiers() {

        return dossierManagementService.getSoftDeletedDossiers().stream().map(dossierACLService::enhanceDossierWithACLData).collect(Collectors.toList());

    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    @PostFilter("hasPermission(filterObject.id, 'Dossier', 'VIEW_OBJECT')")
    public List<Dossier> getArchivedDossiers() {

        return dossierManagementService.getArchivedDossiers().stream().map(dossierACLService::enhanceDossierWithACLData).collect(Collectors.toList());
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    @PostFilter("hasPermission(filterObject.id, 'Dossier', 'VIEW_OBJECT')")
    public List<Dossier> getArchivedDossiersForDossierTemplate(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {

        return dossierManagementService.getArchivedDossiersForDossierTemplateId(dossierTemplateId)
                .stream()
                .map(dossierACLService::enhanceDossierWithACLData)
                .collect(Collectors.toList());
    }


    @PreAuthorize("hasAuthority('" + ARCHIVE_DOSSIER + "')")
    @PreFilter("hasPermission(filterObject, 'Dossier', 'ACCESS_OBJECT')")
    public void archiveDossiers(@RequestBody Set<String> dossierIds) {

        for (String dossierId : dossierIds) {
            accessControlService.verifyUserIsDossierOwner(dossierId);
        }
        dossierManagementService.archiveDossiers(dossierIds);

        for (String dossierId : dossierIds) {
            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(KeycloakSecurity.getUserId())
                    .objectId(dossierId)
                    .category(AuditCategory.DOSSIER.name())
                    .message("Dossier archived.")
                    .build());
        }
    }


    @PreAuthorize("hasAuthority('" + UNARCHIVE_DOSSIER + "')")
    public void unarchiveDossiers(@RequestBody Set<String> dossierIds) {

        dossierManagementService.unarchiveDossiers(dossierIds);

        for (String dossierId : dossierIds) {
            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(KeycloakSecurity.getUserId())
                    .objectId(dossierId)
                    .category(AuditCategory.DOSSIER.name())
                    .message("Dossier restored from archive.")
                    .build());
        }
    }


    @PreAuthorize("hasAuthority('" + DELETE_DOSSIER + "')")
    @PreFilter("hasPermission(filterObject, 'Dossier', 'ACCESS_OBJECT')")
    public void hardDeleteDossiers(@RequestParam(DOSSIER_ID_PARAM) Set<String> dossierIds) {

        for (String dossierId : dossierIds) {
            accessControlService.verifyUserIsDossierOwner(dossierId);
        }
        dossierManagementService.hardDeleteDossiers(dossierIds);

        for (String dossierId : dossierIds) {

            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(KeycloakSecurity.getUserId())
                    .objectId(dossierId)
                    .category(AuditCategory.DOSSIER.name())
                    .message("Dossier permanently deleted.")
                    .build());

        }
    }


    @PreAuthorize("hasAuthority('" + DELETE_DOSSIER + "')")
    @PreFilter("hasPermission(filterObject, 'Dossier', 'ACCESS_OBJECT')")
    public void undeleteDossiers(@RequestBody Set<String> dossierIds) {

        dossierManagementService.undeleteDossiers(dossierIds);
        for (String dossierId : dossierIds) {

            auditPersistenceService.audit(AuditRequest.builder()
                    .userId(KeycloakSecurity.getUserId())
                    .objectId(dossierId)
                    .category(AuditCategory.DOSSIER.name())
                    .message("Dossier restored from trash.")
                    .build());

        }
    }

}
