package com.oguerreiro.resilient.service;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.oguerreiro.resilient.domain.Organization;
import com.oguerreiro.resilient.domain.User;
import com.oguerreiro.resilient.domain.enumeration.OrganizationNature;
import com.oguerreiro.resilient.repository.OrganizationRepository;
import com.oguerreiro.resilient.security.SecurityUtils;
import com.oguerreiro.resilient.service.dto.OrganizationDTO;
import com.oguerreiro.resilient.service.mapper.OrganizationMapper;

import jakarta.persistence.EntityManager;

/**
 * Service Implementation for managing {@link com.oguerreiro.resilient.domain.Organization}.
 */
@Service
@Transactional
public class OrganizationService extends AbstractResilientService<OrganizationDTO, Organization, Long> {

  private final OrganizationRepository organizationRepository;
  private final OrganizationMapper organizationMapper;
  private final EntityManager entityManager;

  public OrganizationService(OrganizationRepository organizationRepository, OrganizationMapper organizationMapper,
      EntityManager entityManager) {
    super(Organization.class, organizationRepository, organizationMapper);

    this.organizationRepository = organizationRepository;
    this.organizationMapper = organizationMapper;
    this.entityManager = entityManager;
  }

  /**
   * First, searches for the parent of the person, with provided nature.
   * Then, searches all children of parent, with the same nature.
   * 
   * @param  person
   * @param  nature
   * @return
   */
  public List<OrganizationDTO> findPersonParentAndChildrenOf(Organization person, OrganizationNature nature) {
    Organization myPerson = organizationRepository.getById(person.getId());

    List<Organization> all = new ArrayList<Organization>();
    Organization parent = this.findNearestParentOf(myPerson, nature);
    all.add(parent);

    all.addAll(this.getAllChildrenOf(parent, nature));
    List<OrganizationDTO> allFinal = all.stream().map(organizationMapper::toDto).collect(
        Collectors.toCollection(LinkedList::new));

    return allFinal;
  }

  public List<Organization> getOrganizationsForUserName(User user) {
    Organization person = organizationRepository.findOneByUser(user).orElseThrow();

    List<Organization> all = new ArrayList<Organization>();
    Organization parent = this.findNearestParentOf(person, OrganizationNature.ORGANIZATION);
    all.add(parent);

    all.addAll(this.getAllChildrenOf(parent, OrganizationNature.ORGANIZATION));

    return all;
  }

  public Organization findNearestParentOf(Organization child, OrganizationNature nature) {
    Organization parent = child.getParent();
    if (parent == null) {
      // Not found
      return null;
    } else if (parent.getOrganizationType().getNature().equals(nature)) {
      entityManager.refresh(parent); // Refresh parent to reload children collection
      return parent;
    } else {
      return findNearestParentOf(parent, nature);
    }
  }

  public List<Organization> getAllChildrenOf(Long parentId, OrganizationNature nature) {
    Organization parent = this.getRepository().getReferenceById(parentId);

    return this.getAllChildrenOf(parent, nature);
  }

  public List<Organization> getAllChildrenOf(Organization parent, OrganizationNature nature) {
    List<Organization> children = new LinkedList<Organization>();

    for (Organization child : parent.getChildren()) {
      if (child.getOrganizationType().getNature().equals(nature)) {
        children.add(child);
        children.addAll(this.getAllChildrenOf(child, nature));
      }
    }

    return children;
  }

  /**
   * 
   * @param  nature,    the {@link OrganizationNature}, if left {@code null} ALL {@link Organization Organization's}
   *                    will be retrieved
   * @param  forInput,  boolean value that indicates you only want {@link Organization Organization's} that allow
   *                    inventory input. If left null, this restriction is ignored
   * @param  forOutput, boolean value that indicates you only want {@link Organization Organization's} that allow
   *                    inventory output. If left null, this restriction is ignored
   * @return
   */
  @Transactional(readOnly = true)
  public List<OrganizationDTO> findAllByNature(OrganizationNature nature, Boolean forInput, Boolean forOutput) {
    log.debug("Request to findAllByNature");

    //@formatter:off
    return organizationRepository.findAllOrganizationByNature(nature, forInput, forOutput)
            .stream()
            .map(organizationMapper::toDto).collect(Collectors.toCollection(LinkedList::new));
    //@formatter:on
  }

  @Transactional(readOnly = true)
  public List<OrganizationDTO> findAllByNatureForUserHierarchy(OrganizationNature nature, Boolean forInput,
      Boolean forOutput) {
    log.debug("Request to findAllByNatureForUserHierarchy");

    Organization parentOrganization = SecurityUtils.getCurrentUserParentOrganization();
    parentOrganization = this.entityManager.merge(parentOrganization);

    List<OrganizationDTO> orgs = new ArrayList<OrganizationDTO>();

    // Add it self
    if ((forInput == null || parentOrganization.getInputInventory() == forInput)
        && (forOutput == null || parentOrganization.getOutputInventory() == forOutput)) {
      orgs.add(organizationMapper.toDto(parentOrganization));
    }

    // Add all his children
    if (parentOrganization != null) {
      //@formatter:off
      orgs.addAll(this.getAllChildrenOf(parentOrganization, nature).stream()
          .filter(org -> (forInput == null || org.getInputInventory()==forInput) && (forOutput == null || org.getOutputInventory()==forOutput)) 
          .map(organizationMapper::toDto).collect(Collectors.toCollection(LinkedList::new)));
      //@formatter:on
    }

    return orgs;
  }

}
