MemberExportService.java

package no.nav.data.team.member;

import lombok.RequiredArgsConstructor;
import no.nav.data.common.export.ExcelBuilder;
import no.nav.data.common.utils.DateUtil;
import no.nav.data.common.utils.StreamUtils;
import no.nav.data.common.utils.StringUtils;
import no.nav.data.team.cluster.ClusterService;
import no.nav.data.team.cluster.domain.Cluster;
import no.nav.data.team.cluster.domain.ClusterMember;
import no.nav.data.team.member.MemberExportService.Member.Relation;
import no.nav.data.team.member.dto.MemberResponse;
import no.nav.data.team.po.ProductAreaService;
import no.nav.data.team.po.domain.AreaType;
import no.nav.data.team.po.domain.PaMember;
import no.nav.data.team.po.domain.ProductArea;
import no.nav.data.team.resource.NomGraphClient;
import no.nav.data.team.shared.Lang;
import no.nav.data.team.shared.domain.Membered;
import no.nav.data.team.team.TeamService;
import no.nav.data.team.team.domain.Team;
import no.nav.data.team.team.domain.TeamMember;
import no.nav.nom.graphql.model.RessursOrgTilknytningDto;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Comparator.comparing;
import static java.util.Objects.isNull;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static no.nav.data.common.utils.StreamUtils.convert;
import static no.nav.data.common.utils.StreamUtils.filter;
import static no.nav.data.common.utils.StreamUtils.tryFind;
import static no.nav.data.team.shared.Lang.NOM_ID_NOT_APPLICABLE;
import static org.apache.commons.lang3.StringUtils.EMPTY;

@Service
@RequiredArgsConstructor
public class MemberExportService {

    public enum SpreadsheetType {
        ALL,
        AREA,
        CLUSTER,
        TEAM,
        ROLE,
        LEADER
    }

    private final TeamService teamService;
    private final ProductAreaService productAreaService;
    private final ClusterService clusterService;
    private final NomGraphClient nomGraphClient;

    public byte[] generateSpreadsheet(SpreadsheetType type, String filter) {
        var pas = productAreaService.getAllActive();
        var clusters = clusterService.getAllActive();
        var members = switch (type) {
            case ALL -> getAll(pas, clusters);
            case AREA -> getForProductArea(StringUtils.toUUID(filter), pas, clusters);
            case CLUSTER -> getForCluster(StringUtils.toUUID(filter), pas, clusters);
            case TEAM -> mapTeamMembers(List.of(teamService.get(StringUtils.toUUID(filter))), pas, clusters).collect(toList());
            case ROLE -> filter(getAll(pas, clusters), m -> convert(m.member().getRoles(), Enum::name).contains(filter));
            case LEADER -> filter(getAll(pas, clusters), m -> {
                var leaderMembers = nomGraphClient.getLeaderMembersActiveOnly(filter);
                return leaderMembers.contains(m.member().getNavIdent());
            });
        };
        return generateFor(members);
    }

    private List<Member> getAll(List<ProductArea> pas, List<Cluster> clusters) {
        var allActiveTeams = teamService.getAllActive();
        return Stream.concat(
                Stream.concat(
                        mapTeamMembers(allActiveTeams, pas, clusters),
                        mapPaMembers(pas)
                ),
                mapClusterMembers(clusters, pas)
        ).collect(toList());
    }

    private List<Member> getForProductArea(UUID id, List<ProductArea> pas, List<Cluster> clusters) {
        ProductArea productArea = productAreaService.get(id);
        return Stream.concat(Stream.concat(
                mapPaMembers(List.of(productArea)),
                mapTeamMembers(teamService.findByProductArea(id).stream().filter(team -> team.getStatus().isActive()).toList(), pas, clusters))
                , mapClusterMembers(filter(clusters, cl -> productArea.getId().equals(cl.getProductAreaId())), pas)
        ).collect(toList());
    }

    private List<Member> getForCluster(UUID id, List<ProductArea> pas, List<Cluster> clusters) {
        return Stream.concat(
                mapClusterMembers(List.of(clusterService.get(id)), pas),
                mapTeamMembers(teamService.findByCluster(id), pas, clusters)
        ).collect(toList());
    }

    private Stream<Member> mapPaMembers(List<ProductArea> productAreas) {
        var members = productAreas.stream()
                .map(ProductArea::getMembers)
                .flatMap(List::stream)
                .map(PaMember::getNavIdent)
                .distinct().toList();
        var otMap = getOrgtilknytningMap(members);

        return productAreas.stream().flatMap(pa ->
                pa.getMembers().stream().map(m ->
                        new Member(Relation.PA, m.convertToResponse(), otMap.getOrDefault(m.getNavIdent(), new Member.Orgenhet()), null, pa, List.of())
                )
        );
    }

    private Stream<Member> mapClusterMembers(List<Cluster> clusters, List<ProductArea> productAreas) {
        var navidenter = clusters.stream()
                .flatMap(cluster -> cluster.getMembers().stream())
                .map(ClusterMember::getNavIdent)
                .distinct()
                .toList();

        var otMap = getOrgtilknytningMap(navidenter);

        return clusters.stream().flatMap(cluster ->
                cluster.getMembers().stream().map(m ->
                        new Member(Relation.CLUSTER, m.convertToResponse(), otMap.getOrDefault(m.getNavIdent(), new Member.Orgenhet()),null,
                                tryFind(productAreas, pa -> pa.getId().equals(cluster.getProductAreaId())).orElse(null),
                                List.of(cluster)
                        )
                )
        );
    }

    private Stream<Member> mapTeamMembers(List<Team> teams, List<ProductArea> pas, List<Cluster> clusters) {
        var navidenter = teams.stream()
                .flatMap(t -> t.getMembers().stream())
                .map(TeamMember::getNavIdent)
                .distinct()
                .toList();
        var otMap = getOrgtilknytningMap(navidenter);

        return teams.stream().flatMap(t -> t.getMembers().stream().map(m -> {
            ProductArea productArea = StreamUtils.tryFind(pas, pa -> pa.getId().equals(t.getProductAreaId())).orElse(null);
            List<Cluster> clustersForTeam = filter(clusters, cluster -> t.getClusterIds().contains(cluster.getId()));
            return new Member(Relation.TEAM, m.convertToResponse(), otMap.getOrDefault(m.getNavIdent(), new Member.Orgenhet()), t, productArea, clustersForTeam);
        }));
    }

    private Map<String, Member.Orgenhet> getOrgtilknytningMap(List<String> navidenter) {
        return nomGraphClient.getRessurser(navidenter).entrySet().stream().collect(
                Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getOrgTilknytning()
                        .stream().sorted(Comparator.comparing(RessursOrgTilknytningDto::getGyldigFom))
                        .filter(ot ->
                                ot.getErDagligOppfolging()
                                && (ot.getGyldigTom() == null || ot.getGyldigTom().isAfter(LocalDate.now())))
                        .findFirst()
                        .map(ot ->
                                new Member.Orgenhet(
                                        ot.getOrgEnhet().getId(),
                                        ot.getOrgEnhet().getNavn(),
                                        ot.getOrgEnhet().getLeder().stream().findFirst().map(leder -> leder.getRessurs().getVisningsnavn()).orElse(""),
                                        ot.getOrgEnhet().getLeder().stream().findFirst().map(leder -> leder.getRessurs().getNavident()).orElse(""))
                        ).orElse(new Member.Orgenhet())
                )
        );
    }

    private byte[] generateFor(List<Member> members) {
        var doc = new ExcelBuilder(Lang.MEMBERS);
        doc.addRow()
                .addCell(Lang.RELATION)
                .addCell(Lang.AREA_NOM_ID)
                .addCell(Lang.AREA)
                .addCell(Lang.CLUSTER)
                .addCell(Lang.TEAM)
                .addCell(Lang.IDENT)
                .addCell(Lang.GIVEN_NAME)
                .addCell(Lang.FAMILY_NAME)
                .addCell(Lang.RESOURCE_TYPE)
                .addCell(Lang.ROLES)
                .addCell(Lang.OTHER)
                .addCell(Lang.EMAIL)
                .addCell(Lang.START_DATE)
                .addCell(Lang.END_DATE)
                .addCell(Lang.ORGENHET_ID)
                .addCell(Lang.ORGENHET)
                .addCell(Lang.ORGENHET_LEDER)
                .addCell(Lang.ORGENHET_LEDER_NAVIDENT)
        ;

        Comparator<Member> c1 = comparing(m -> ofNullable(m.member.getResource().getFamilyName()).orElse(""));
        Comparator<Member> c2 = c1.thenComparing(m -> ofNullable(m.member.getResource().getGivenName()).orElse(""));
        members.sort(c2);
        members.forEach(m -> add(doc, m));

        return doc.build();
    }

    private void add(ExcelBuilder doc, Member member) {
        doc.addRow()
                .addCell(member.relationType())
                .addCell(member.productAreaNomId())
                .addCell(member.productAreaName())
                .addCell(member.clusterName())
                .addCell(member.teamName())
                .addCell(member.member.getNavIdent())
                .addCell(member.member.getResource().getGivenName())
                .addCell(member.member.getResource().getFamilyName())
                .addCell(member.memberType())
                .addCell(member.roles())
                .addCell(member.member.getDescription())
                .addCell(member.member.getResource().getEmail())
                .addCell(DateUtil.formatDate(member.member.getResource().getStartDate()))
                .addCell(shouldHideEndDateIfBeforeNow(member.member.getResource().getEndDate()))
                .addCell(member.orgenhet.id())
                .addCell(member.orgenhet.navn != null? String.format("%s", member.orgenhet.navn) : "")
                .addCell(member.orgenhet.leder)
                .addCell(member.orgenhet.lederNavident)
        ;
    }

    private String shouldHideEndDateIfBeforeNow(LocalDate endDate) {
        if (isNull(endDate) || endDate.isAfter(LocalDate.now())) return null;
        else return DateUtil.formatDate(endDate);
    }

    record Member(Relation relation, MemberResponse member, Orgenhet orgenhet, Team team, ProductArea pa, List<Cluster> clusters) {

        public String productAreaNomId() {
            if(pa != null){
                var nomId = pa.getNomId();
                if(nomId != null){
                    return pa.getNomId();
                }
                var isNomSeksjon = AreaType.PRODUCT_AREA.equals(pa.getAreaType());
                if(!isNomSeksjon){
                    return NOM_ID_NOT_APPLICABLE;
                }
            }
            return EMPTY;
        }

        enum Relation {
            TEAM(Team.class),
            PA(ProductArea.class),
            CLUSTER(Cluster.class);

            private final Class<? extends Membered> type;

            Relation(Class<? extends Membered> type) {
                this.type = type;
            }
        }

        public record Orgenhet(String id, String navn, String leder, String lederNavident) {

            public Orgenhet() {
                this(null, null , null, null);
            }
        }

        public String relationType() {
            return Lang.objectType(relation.type);
        }

        public String productAreaName() {
            return pa != null ? pa.getName() : EMPTY;
        }

        public String clusterName() {
            return clusters.isEmpty() ? EMPTY : String.join(", ", convert(clusters, Cluster::getName));
        }

        public String teamName() {
            return switch (relation) {
                case TEAM -> team.getName();
                case PA, CLUSTER -> EMPTY;
            };
        }

        public String memberType() {
            if (member.getResource().getResourceType() == null) {
                return EMPTY;
            }
            return Lang.memberType(member.getResource().getResourceType());
        }

        public String roles() {
            return String.join(", ", convert(member.getRoles(), Lang::roleName));
        }
    }
}