/*
 * Copyright (C) 2015 University of Pittsburgh.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package edu.pitt.dbmi.ccd.anno.annotation;

import edu.pitt.dbmi.ccd.anno.annotation.data.AnnotationDataForm;
import edu.pitt.dbmi.ccd.anno.annotation.data.AnnotationDataPagedResourcesAssembler;
import edu.pitt.dbmi.ccd.anno.annotation.data.AnnotationDataResource;
import edu.pitt.dbmi.ccd.anno.annotation.data.AnnotationDataResourceAssembler;
import edu.pitt.dbmi.ccd.anno.error.AccessNotFoundException;
import edu.pitt.dbmi.ccd.anno.error.AccessUpdateException;
import edu.pitt.dbmi.ccd.anno.error.AnnotationDataNotFoundException;
import edu.pitt.dbmi.ccd.anno.error.AnnotationNotFoundException;
import edu.pitt.dbmi.ccd.anno.error.AnnotationTargetNotFoundException;
import edu.pitt.dbmi.ccd.anno.error.AttributeNotFoundException;
import edu.pitt.dbmi.ccd.anno.error.ForbiddenException;
import edu.pitt.dbmi.ccd.anno.error.GroupNotFoundException;
import edu.pitt.dbmi.ccd.anno.error.NotFoundException;
import edu.pitt.dbmi.ccd.anno.error.VocabularyNotFoundException;
import edu.pitt.dbmi.ccd.db.entity.Access;
import edu.pitt.dbmi.ccd.db.entity.Annotation;
import edu.pitt.dbmi.ccd.db.entity.AnnotationData;
import edu.pitt.dbmi.ccd.db.entity.AnnotationTarget;
import edu.pitt.dbmi.ccd.db.entity.Attribute;
import edu.pitt.dbmi.ccd.db.entity.Group;
import edu.pitt.dbmi.ccd.db.entity.UserAccount;
import edu.pitt.dbmi.ccd.db.entity.Vocabulary;
import edu.pitt.dbmi.ccd.db.service.AccessService;
import edu.pitt.dbmi.ccd.db.service.AnnotationDataService;
import edu.pitt.dbmi.ccd.db.service.AnnotationService;
import edu.pitt.dbmi.ccd.db.service.AnnotationTargetService;
import edu.pitt.dbmi.ccd.db.service.AttributeService;
import edu.pitt.dbmi.ccd.db.service.GroupService;
import edu.pitt.dbmi.ccd.db.service.VocabularyService;
import edu.pitt.dbmi.ccd.security.userDetails.UserAccountDetails;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.IntStream;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.hateoas.ExposesResourceFor;
import org.springframework.hateoas.PagedResources;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import static org.springframework.util.StringUtils.isEmpty;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

// logging
/**
 * @author Mark Silvis (marksilvis@pitt.edu)
 */
@RestController
@ExposesResourceFor(AnnotationResource.class)
@RequestMapping(AnnotationLinks.INDEX)
public class AnnotationController {

    // loggers
    private static final Logger LOGGER = LoggerFactory.getLogger(AnnotationController.class);

    // servlet
    private final HttpServletRequest request;

    // services and components
    private final AnnotationLinks annotationLinks;
    private final AnnotationService annotationService;
    private final AnnotationDataService annotationDataService;
    private final AnnotationTargetService annotationTargetService;
    private final AccessService accessService;
    private final GroupService groupService;
    private final VocabularyService vocabularyService;
    private final AttributeService attributeService;
    private final AnnotationResourceAssembler assembler;
    private final AnnotationPagedResourcesAssembler pageAssembler;
    private final AnnotationDataResourceAssembler dataAssembler;
    private final AnnotationDataPagedResourcesAssembler dataPageAssembler;

    @Autowired(required = true)
    public AnnotationController(
            HttpServletRequest request,
            AnnotationLinks annotationLinks,
            AnnotationService annotationService,
            AnnotationDataService annotationDataService,
            AnnotationTargetService annotationTargetService,
            AccessService accessService,
            GroupService groupService,
            VocabularyService vocabularyService,
            AttributeService attributeService,
            AnnotationResourceAssembler assembler,
            AnnotationPagedResourcesAssembler pageAssembler,
            AnnotationDataResourceAssembler dataAssembler,
            AnnotationDataPagedResourcesAssembler dataPageAssembler) {
        this.request = request;
        this.annotationLinks = annotationLinks;
        this.annotationService = annotationService;
        this.annotationDataService = annotationDataService;
        this.annotationTargetService = annotationTargetService;
        this.accessService = accessService;
        this.groupService = groupService;
        this.vocabularyService = vocabularyService;
        this.attributeService = attributeService;
        this.assembler = assembler;
        this.pageAssembler = pageAssembler;
        this.dataAssembler = dataAssembler;
        this.dataPageAssembler = dataPageAssembler;
    }

    /* GET requests */
    /**
     * Get all annotations
     *
     * @param principal authenticated user
     * @param user username (optional)
     * @param group group name (nullable)
     * @param target target id (nullable)
     * @param vocab vocabulary name (nullable)
     * @param attributeLevel attribute level (nullable)
     * @param attributeName attribute name (nullable)
     * @param attributeRequirementLevel attribute requirement level (nullable)
     * @param pageable page request
     * @return page of annotations
     */
    @RequestMapping(method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public PagedResources<AnnotationResource> annotations(
            @AuthenticationPrincipal UserAccountDetails principal,
            @RequestParam(value = "user", required = false) String user,
            @RequestParam(value = "group", required = false) String group,
            @RequestParam(value = "target", required = false) Long target,
            @RequestParam(value = "vocab", required = false) String vocab,
            @RequestParam(value = "level", required = false) String attributeLevel,
            @RequestParam(value = "name", required = false) String attributeName,
            @RequestParam(value = "requirement", required = false) String attributeRequirementLevel,
            @RequestParam(value = "showRedacted", required = false, defaultValue = "false") Boolean showRedacted,
            @RequestParam(value = "parentless", required = false, defaultValue = "false") Boolean parentless,
            @RequestParam(value = "createdBefore", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date createdBefore,
            @RequestParam(value = "createdAfter", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date createdAfter,
            @RequestParam(value = "modifiedBefore", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date modifiedBefore,
            @RequestParam(value = "modifiedAfter", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date modifiedAfter,
            Pageable pageable) {
        final UserAccount requester = principal.getUserAccount();
        final Page<Annotation> page = annotationService.filter(requester, user, group, target, vocab, attributeLevel, attributeName, attributeRequirementLevel, showRedacted, parentless, createdBefore, createdAfter, modifiedBefore, modifiedAfter, pageable);
        final PagedResources<AnnotationResource> pagedResources = pageAssembler.toResource(page, assembler, request);
        pagedResources.add(annotationLinks.search());
        return pagedResources;
    }

    /**
     * Get annotation by id
     *
     * @param principal authenticated user
     * @param id annotation id
     * @return annotation
     */
    @RequestMapping(value = AnnotationLinks.ANNOTATION, method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public AnnotationResource annotation(@AuthenticationPrincipal UserAccountDetails principal, @PathVariable Long id) throws NotFoundException {
        final UserAccount requester = principal.getUserAccount();
        final Annotation annotation = annotationService.findById(requester, id);
        if (annotation == null) {
            throw new AnnotationNotFoundException(id);
        }
        final AnnotationResource resource = assembler.toResource(annotation);
        return resource;
    }

    /**
     * Get annotation data
     *
     * @param principal authenticated user
     * @param id annotation id
     * @return page of annotation data
     */
    @RequestMapping(value = AnnotationLinks.ANNOTATION_DATA, method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public PagedResources<AnnotationDataResource> annotationDataCollection(
            @AuthenticationPrincipal UserAccountDetails principal,
            @PathVariable Long id,
            @RequestParam(value = "attribute", required = false) Long attributeId,
            @PageableDefault(size = 20, sort = {"id"}) Pageable pageable) throws NotFoundException {
        final UserAccount requester = principal.getUserAccount();
        final Annotation annotation = annotationService.findById(requester, id);
        if (annotation == null) {
            throw new AnnotationNotFoundException(id);
        }
        final Page<AnnotationData> page = annotationDataService.findByAnnotation(annotation, pageable);
        final PagedResources<AnnotationDataResource> pagedResources = dataPageAssembler.toResource(page, dataAssembler, request);
        return pagedResources;
    }

    /**
     * Get annotation data by id
     *
     * @param principal authenticated user
     * @param id annotation id
     * @param dataId annotation data id
     * @return annotation data
     */
    @RequestMapping(value = AnnotationLinks.ANNOTATION_DATA_ID, method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public AnnotationDataResource annotationData(@AuthenticationPrincipal UserAccountDetails principal, @PathVariable Long id, @PathVariable Long dataId) throws NotFoundException {
        final UserAccount requester = principal.getUserAccount();
        final Annotation annotation = annotationService.findById(requester, id);
        if (annotation == null) {
            throw new AnnotationNotFoundException(id);
        }
        final AnnotationData data = annotation.getData()
                .stream()
                .filter(d -> d.getId().equals(dataId))
                .findFirst()
                .orElseThrow(() -> new AnnotationDataNotFoundException(dataId));
        final AnnotationDataResource resource = dataAssembler.toResource(data);
        return resource;
    }

    /**
     * Get child annotations by parent
     *
     * @param principal authenticated user
     * @param id parent annotation id
     * @return page of annotations
     */
    @RequestMapping(value = AnnotationLinks.CHILDREN, method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public PagedResources<AnnotationResource> children(@AuthenticationPrincipal UserAccountDetails principal, @PathVariable Long id, @RequestParam(name = "showRedacted", required = false) boolean showRedacted, Pageable pageable) throws NotFoundException {
        final UserAccount requester = principal.getUserAccount();
        final Annotation annotation = annotationService.findById(requester, id);
        if (annotation == null) {
            throw new AnnotationNotFoundException(id);
        }
        final Page<Annotation> page = annotationService.findByParent(requester, annotation, showRedacted, pageable);
        final PagedResources<AnnotationResource> pagedResources = pageAssembler.toResource(page, assembler, request);
        return pagedResources;
    }

    /**
     * Search for annotations
     *
     * @param principal authenticated user (required)
     * @param user username (nullable)
     * @param group group name (nullable)
     * @param target target id (nullable)
     * @param vocab vocabulary name (nnullable)
     * @param attributeLevel attribute level (nullable)
     * @param attributeName attribute name (nullable)
     * @param attributeRequirementLevel attribute requirement level (nullable)
     * @param query search terms (nullable)
     * @param not negated search terms (nullable)
     * @param pageable page request
     * @return page of annotations matching parameters
     */
    @RequestMapping(value = AnnotationLinks.SEARCH, method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public PagedResources<AnnotationResource> search(
            @AuthenticationPrincipal UserAccountDetails principal,
            @RequestParam(value = "user", required = false) String user,
            @RequestParam(value = "group", required = false) String group,
            @RequestParam(value = "dataset", required = false) Long target,
            @RequestParam(value = "vocab", required = false) String vocab,
            @RequestParam(value = "level", required = false) String attributeLevel,
            @RequestParam(value = "name", required = false) String attributeName,
            @RequestParam(value = "requirement", required = false) String attributeRequirementLevel,
            @RequestParam(value = "showRedacted", required = false, defaultValue = "false") Boolean showRedacted,
            @RequestParam(value = "parentless", required = false, defaultValue = "false") Boolean parentless,
            @RequestParam(value = "createdBefore", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date createdBefore,
            @RequestParam(value = "createdAfter", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date createdAfter,
            @RequestParam(value = "modifiedBefore", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date modifiedBefore,
            @RequestParam(value = "modifiedAfter", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date modifiedAfter,
            @RequestParam(value = "terms", required = false) String query,
            @RequestParam(value = "not", required = false) String not,
            Pageable pageable) {
        final UserAccount requester = principal.getUserAccount();
        final Set<String> matches = (query != null) ? new HashSet<>(Arrays.asList(query.trim().split("\\s+")))
                : null;
        final Set<String> nots = (not != null) ? new HashSet<>(Arrays.asList(not.trim().split("\\s+")))
                : null;
        final Page<Annotation> page = annotationService.search(requester, user, group, target, vocab, attributeLevel, attributeName, attributeRequirementLevel, showRedacted, parentless, createdBefore, createdAfter, modifiedBefore, modifiedAfter, matches, nots, pageable);
        final PagedResources<AnnotationResource> pagedResources = pageAssembler.toResource(page, assembler, request);
        return pagedResources;
    }

    /* POST requests */
    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    @ResponseBody
    public AnnotationResource newAnnotation(@AuthenticationPrincipal UserAccountDetails principal, @RequestBody @Valid AnnotationForm form) throws NotFoundException {
        final UserAccount requester = principal.getUserAccount();
        // get target (nullable)
        final Long targetId = form.getTarget();
        AnnotationTarget annotationTarget = null;
        if (targetId != null) {
            annotationTarget = annotationTargetService.findById(targetId);
            if (annotationTarget == null) {
                throw new AnnotationTargetNotFoundException(targetId);
            }
        }

        // get parent (nullable)
        final Long parentId = form.getParent();
        Annotation parent = null;

        if (parentId != null) {
            parent = annotationService.findById(requester, parentId);
            if (parent == null) {
                throw new AnnotationNotFoundException(parentId);
            }
        }

        // get access
        final Access access = accessService.findByName(form.getAccess());
        if (access == null) {
            throw new AccessNotFoundException(form.getAccess());
        }

        // get group (nullable)
        final String groupName = form.getGroup();
        Group group = null;
        if (groupName != null) {
            group = groupService.findByName(groupName);
            if (group == null) {
                throw new GroupNotFoundException(groupName);
            }
        }

        // get vocabulary
        final Vocabulary vocabulary = vocabularyService.findByName(form.getVocabulary());
        if (vocabulary == null) {
            throw new VocabularyNotFoundException(form.getVocabulary());
        }
        Annotation annotation = new Annotation(requester, annotationTarget, parent, access, group, vocabulary);
        annotation = annotationService.save(annotation);
        annotation = newAnnotationData(annotation, form.getData());
        annotation = annotationService.saveAndFlush(annotation);
        final AnnotationResource resource = assembler.toResource(annotation);
        return resource;
    }

    private Annotation newAnnotationData(Annotation annotation, @Valid List<AnnotationDataForm> data) {
        IntStream.range(0, data.size())
                .forEach(i -> {
                    final Long attributeId = data.get(i).getAttribute();
                    final Attribute attribute = attributeService.findById(attributeId);
                    if (attribute == null) {
                        throw new AttributeNotFoundException(attributeId);
                    }
                    final String value = data.get(i).getValue();
                    final List<AnnotationDataForm> subData = data.get(i).getChildren();
                    AnnotationData annoData = new AnnotationData(annotation, attribute, value);
                    annoData = annotationDataService.save(annoData);
                    if (subData.size() > 0) {
                        newAnnotationDataSubData(annotation, annoData, subData);
                    }
                });
        return annotation;
    }

    private void newAnnotationDataSubData(Annotation annotation, AnnotationData parent, @Valid List<AnnotationDataForm> children) {
        IntStream.range(0, children.size())
                .forEach(i -> {
                    final Long attributeId = children.get(i).getAttribute();
                    final Attribute attribute = attributeService.findById(attributeId);
                    if (attribute == null) {
                        throw new AttributeNotFoundException(attributeId);
                    }
                    final String value = children.get(i).getValue();
                    final List<AnnotationDataForm> subData = children.get(i).getChildren();
                    AnnotationData annotationData = new AnnotationData(annotation, attribute, value);
                    annotationData = annotationDataService.save(annotationData);
                    if (subData.size() > 0) {
                        newAnnotationDataSubData(annotation, annotationData, subData);
                    }
                });
    }

    /**
     * Redact an annotation
     *
     * @param principal authenticated user
     * @param id annotation id
     */
    @RequestMapping(value = AnnotationLinks.ANNOTATION_REDACT, method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    @ResponseBody
    public void redactAnnotation(@AuthenticationPrincipal UserAccountDetails principal, @PathVariable Long id) throws NotFoundException, ForbiddenException {
        final UserAccount requester = principal.getUserAccount();
        final Annotation annotation = annotationService.findById(requester, id);
        if (annotation == null) {
            throw new AnnotationNotFoundException(id);
        }
        if (annotation.getUser().getId().equals(requester.getId())) {
            annotation.redact();
            annotationService.save(annotation);
        } else {
            throw new ForbiddenException(requester, request);
        }
    }

    /* PUT requests */
//    public AnnotationResource newAnnotationPUT(@AuthenticationPrincipal UserAccount principal, @RequestBody @Valid AnnotationForm form) {
//        return newAnnotation(principal, form);
//    }

    /* PATCH requests */
    @RequestMapping(value = AnnotationLinks.ANNOTATION, method = RequestMethod.PATCH)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public AnnotationResource editAnnotation(@AuthenticationPrincipal UserAccountDetails principal, @PathVariable Long id, @RequestBody AnnotationForm form) throws NotFoundException, ForbiddenException, AccessUpdateException {
        final UserAccount requester = principal.getUserAccount();
        Annotation annotation = annotationService.findById(requester, id);
        if (annotation == null) {
            throw new AnnotationNotFoundException(id);
        }
        if (annotation.getUser().getId().equals(requester.getId())) {
            final String accessName = form.getAccess();
            Access access = null;
            if (accessName != null) {
                access = accessService.findByName(accessName);
                if (access == null) {
                    throw new AccessNotFoundException(accessName);
                }
            }
            final String groupName = form.getGroup();
            Group group = null;
            if (groupName != null) {
                group = groupService.findByName(groupName);
                if (group == null) {
                    throw new GroupNotFoundException(groupName);
                }
            }
            annotation = updateAnnotation(annotation, access, group);
            annotation = annotationService.save(annotation);
            final AnnotationResource resource = assembler.toResource(annotation);
            return resource;
        } else {
            throw new ForbiddenException(requester, request);
        }
    }

    private Annotation updateAnnotation(Annotation annotation, Access access, Group group) {
        // no changes, don't update
        if (annotation.getAccess().getId().equals(access.getId()) && annotation.getGroup().getId().equals(group.getId())) {
            return annotation;
            // update just group
        } else if (annotation.getAccess().getId().equals(access.getId()) && access.getName().equalsIgnoreCase("GROUP") && !annotation.getGroup().getId().equals(group.getId())) {
            annotation.setGroup(group);
            return annotationService.save(annotation);
            // update access from private to group and update group
        } else if (annotation.getAccess().getName().equalsIgnoreCase("PRIVATE") && access.getName().equalsIgnoreCase("GROUP")) {
            if (group == null) {
                throw new AccessUpdateException(true);
            } else {
                annotation.setAccess(access);
                annotation.setGroup(group);
                return annotationService.save(annotation);
            }
            // update access from private to public
        } else if (annotation.getAccess().getName().equalsIgnoreCase("PRIVATE") && access.getName().equalsIgnoreCase("PUBLIC")) {
            annotation.setAccess(access);
            return annotationService.save(annotation);
            // update access from group to public and remove group
        } else if (annotation.getAccess().getName().equalsIgnoreCase("GROUP") && access.getName().equalsIgnoreCase("PUBLIC")) {
            annotation.setAccess(access);
            annotation.setGroup(null);
            return annotationService.save(annotation);
        } else {
            throw new AccessUpdateException(annotation.getAccess(), access);
        }
    }

    @RequestMapping(value = AnnotationLinks.ANNOTATION_DATA, method = RequestMethod.PATCH)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public AnnotationDataResource editAnnotationData(@AuthenticationPrincipal UserAccountDetails principal, @PathVariable Long id, @PathVariable Long dataId, @RequestBody AnnotationDataForm form) throws NotFoundException, ForbiddenException {
        final UserAccount requester = principal.getUserAccount();
        final Annotation annotation = annotationService.findById(requester, id);
        if (annotation == null) {
            throw new AnnotationNotFoundException(id);
        }
        if (annotation.getUser().getId().equals(requester.getId())) {
            AnnotationData data = annotation.getData()
                    .stream()
                    .filter(d -> d.getId().equals(dataId))
                    .findFirst()
                    .orElseThrow(() -> new AnnotationDataNotFoundException(dataId));
            final Long attributeId = form.getAttribute();
            if (!isEmpty(attributeId) && !data.getAttribute().getId().equals(attributeId)) {
                final Attribute attribute = attributeService.findById(attributeId);
                if (attribute == null) {
                    throw new AttributeNotFoundException(attributeId);
                }
                data.setAttribute(attribute);
            }
            final String value = form.getValue();
            if (!isEmpty(value) && !data.getValue().equals(value)) {
                data.setValue(value);
            }
            data = annotationDataService.save(data);
            final AnnotationDataResource resource = dataAssembler.toResource(data);
            return resource;
        } else {
            throw new ForbiddenException(requester, request);
        }
    }
}
