DashCacheProvider.java

package no.nav.data.team.dashboard;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import no.nav.data.common.utils.StreamUtils;
import no.nav.data.team.cluster.ClusterService;
import no.nav.data.team.cluster.domain.Cluster;
import no.nav.data.team.dashboard.dto.DashResponse;
import no.nav.data.team.location.LocationRepository;
import no.nav.data.team.member.dto.MemberResponse;
import no.nav.data.team.po.ProductAreaService;
import no.nav.data.team.po.domain.ProductArea;
import no.nav.data.team.resource.NomClient;
import no.nav.data.team.resource.domain.ResourceType;
import no.nav.data.team.shared.domain.DomainObjectStatus;
import no.nav.data.team.shared.domain.Member;
import no.nav.data.team.team.TeamService;
import no.nav.data.team.team.domain.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static no.nav.data.common.utils.StreamUtils.*;

@Configuration
@RequiredArgsConstructor
@Slf4j
public class DashCacheProvider {
    private final ProductAreaService productAreaService;
    private final TeamService teamService;
    private final ClusterService clusterService;
    private final NomClient nomClient;
    private final LocationRepository locationRepository;

    private static final List<Team> E = List.of();
    private static final TreeSet<Integer> groups = new TreeSet<>(Set.of(0, 5, 10, 20, Integer.MAX_VALUE));
    private static final TreeSet<Integer> extPercentGroups = new TreeSet<>(Set.of(0, 25, 50, 75, 100));
    private static final BiFunction<Object, Integer, Integer> counter = (k, v) -> v == null ? 1 : v + 1;

    @Bean(name="dashCache")
    public LoadingCache<String, DashResponse> getDashCache() {
        return Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofMinutes(1))
                .maximumSize(1).build(k -> calcDash());
    }

    private DashResponse calcDash() {
        List<Team> teamsActive = teamService.getAllActive();
        List<ProductArea> productAreasActive = productAreaService.getAllActive();
        List<Cluster> clustersActive = clusterService.getAllActive();

        List<Team> teamsAll = teamService.getAll();
        List<ProductArea> productAreasAll = productAreaService.getAll();
        List<Cluster> clustersAll = clusterService.getAll();

        return DashResponse.builder()
                .teamsCount(teamsActive.size())
                .productAreasCount(productAreasActive.size())
                .clusterCount(clustersActive.size())
                .resources(nomClient.count())
                .resourcesDb(nomClient.countDb())

                .teamsCountPlanned(teamsAll.stream().filter(team -> team.getStatus().equals(DomainObjectStatus.PLANNED)).count())
                .teamsCountInactive(teamsAll.stream().filter(team -> team.getStatus().equals(DomainObjectStatus.INACTIVE)).count())

                .productAreasCountPlanned(productAreasAll.stream().filter(po -> po.getStatus().equals(DomainObjectStatus.PLANNED)).count())
                .productAreasCountInactive(productAreasAll.stream().filter(po -> po.getStatus().equals(DomainObjectStatus.INACTIVE)).count())

                .clusterCountPlanned(clustersAll.stream().filter(cluster -> cluster.getStatus().equals(DomainObjectStatus.PLANNED)).count())
                .clusterCountInactive(clustersAll.stream().filter(cluster -> cluster.getStatus().equals(DomainObjectStatus.INACTIVE)).count())

                .total(calcForTotal(teamsActive, productAreasActive, clustersActive))
                .productAreas(convert(productAreasActive, pa -> calcForArea(filter(teamsActive, t -> pa.getId().equals(t.getProductAreaId())), pa, clustersActive)))
                .clusters(convert(clustersActive, cluster -> calcForCluster(filter(teamsActive, t -> copyOf(t.getClusterIds()).contains(cluster.getId())), cluster, clustersActive)))

                .areaSummaryMap(createAreaSummaryMap(teamsActive, productAreasActive, clustersActive))
                .clusterSummaryMap(createClusterSummaryMap(teamsActive, clustersActive))
                .teamSummaryMap(createTeamSummaryMap(teamsActive, productAreasActive, clustersActive))

                .locationSummaryMap(createLocationSummaryMap(teamsActive))

                .build();
    }


    private <T> void accumulateSubList(HashMap<String, ArrayList<T>> targetMap, String mapKey, List<T> subList ){
        val prev = targetMap.get(mapKey);
        if(prev == null){
            targetMap.put(mapKey,new ArrayList<>(subList));
        }else{
            prev.addAll(subList);
        }
    }

    private <T> long countUnique(List<T> listWithPossibleDuplicates){
        val acc = new ArrayList<T>();
        for(val item : listWithPossibleDuplicates){
            if(!acc.contains(item)) {
                acc.add(item);
            }
        }
        return acc.size();
    }


    private Map<String, DashResponse.LocationSummary> createLocationSummaryMap(List<Team> teams) {

        val out = new HashMap<String, DashResponse.LocationSummary>();

        val locationToNavIdentList = new HashMap<String, ArrayList<String>>();
        val locationToTeamIdList = new HashMap<String,ArrayList<UUID>>();

        val locationDayToNavIdentList = new HashMap<String,ArrayList<String>>();
        val locationDayToTeamIdList = new HashMap<String,ArrayList<UUID>>();

        for(var team : teams){
            val officeHours = team.getOfficeHours();
            if(officeHours == null) {
                continue;
            }
            val teamLocCode = officeHours.getLocationCode();

            @SuppressWarnings("OptionalGetWithoutIsPresent")
            val teamLoc = locationRepository.getLocationByCode(teamLocCode).get();
            val teamMemberList = team.getMembers().stream().map(TeamMember::getNavIdent).toList();

            accumulateSubList(locationToNavIdentList,teamLoc.getCode(),teamMemberList);
            accumulateSubList(locationToTeamIdList,teamLoc.getCode(),List.of(team.getId()));
            for(val day : officeHours.getDays()){
                val mapKeyStr = teamLoc.getCode() + "/" + day.name();
                accumulateSubList(locationDayToNavIdentList, mapKeyStr, teamMemberList);
                accumulateSubList(locationDayToTeamIdList, mapKeyStr, List.of(team.getId()));
            }

            var parentLoc = teamLoc.getParent();
            while (parentLoc != null) {
                val parentLocCode = parentLoc.getCode();
                accumulateSubList(locationToNavIdentList, parentLocCode, teamMemberList);
                accumulateSubList(locationToTeamIdList, parentLocCode, List.of(team.getId()));

                for (val day : officeHours.getDays()) {
                    val mapKeyStr = parentLocCode + "/" + day.name();
                    accumulateSubList(locationDayToNavIdentList, mapKeyStr, teamMemberList);
                    accumulateSubList(locationDayToTeamIdList, mapKeyStr, List.of(team.getId()));
                }

                parentLoc = parentLoc.getParent();
            }
        }

        val allLocations = locationRepository.getAll();
        for(val loc : allLocations){

            val locNavIdList = locationToNavIdentList.get(loc.getCode());
            val resCount = locNavIdList != null ? countUnique(locNavIdList) : 0;

            val locTeamIdList = locationToTeamIdList.get(loc.getCode());
            val teamCount = locTeamIdList != null ? countUnique(locTeamIdList) : 0;

            val locSumBuilder = DashResponse.LocationSummary.builder()
                    .resourceCount(resCount)
                    .teamCount( teamCount );

            val weekDays = List.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY);
            for(val day : weekDays){
                val mapKeyStr = loc.getCode() + "/" + day.name();
                val locDayNavIdList = locationDayToNavIdentList.get(mapKeyStr);
                val resCountDay = locDayNavIdList != null ? countUnique(locDayNavIdList) : 0;

                val locDayTeamIdList = locationDayToTeamIdList.get(mapKeyStr);
                val teamCountDay = locDayTeamIdList != null ? countUnique(locDayTeamIdList) : 0;

                switch(day){
                    case MONDAY -> locSumBuilder.monday(new DashResponse.LocationDaySummary(teamCountDay,resCountDay));
                    case TUESDAY -> locSumBuilder.tuesday(new DashResponse.LocationDaySummary(teamCountDay,resCountDay));
                    case WEDNESDAY -> locSumBuilder.wednesday(new DashResponse.LocationDaySummary(teamCountDay,resCountDay));
                    case THURSDAY -> locSumBuilder.thursday(new DashResponse.LocationDaySummary(teamCountDay,resCountDay));
                    case FRIDAY -> locSumBuilder.friday(new DashResponse.LocationDaySummary(teamCountDay,resCountDay));
                }
            }

            out.put(loc.getCode(),locSumBuilder.build());
        }

        return out;
    }

    private Map<UUID, DashResponse.ClusterSummary> createClusterSummaryMap(List<Team> teams, List<Cluster> clusters) {
        val map = new HashMap<UUID, DashResponse.ClusterSummary>();

        for (val cluster: clusters){

            val relatedTeams = teams.stream()
                    .filter(team -> team.getClusterIds().contains(cluster.getId())
                    ).toList();

            val clusterSubteamMembers = relatedTeams.stream()
                    .flatMap(team -> team.getMembers().stream()).toList();

            val totalMembershipCount = (long) cluster.getMembers().size() + (long) clusterSubteamMembers.size();


            val totaluniqueResources = StreamUtils.distinctByKey(
                    List.of(
                            cluster.getMembers().stream().map(it -> it.getNavIdent()),
                            clusterSubteamMembers.stream().map(it -> it.getNavIdent())

                    ).stream().reduce((a,b) -> Stream.concat(a,b)).get().toList(), it -> it
            );

            val uniqueResourcesExternal = totaluniqueResources.stream()
                    .map(ident -> nomClient.getByNavIdent(ident).orElse(null))
                    .filter(Objects::nonNull)
                    .filter(ressource -> ressource.getResourceType().equals(ResourceType.EXTERNAL))
                    .count();

            map.put(cluster.getId(), DashResponse.ClusterSummary.builder()
                    .totalMembershipCount(totalMembershipCount)
                    .totalUniqueResourcesCount(totaluniqueResources.stream().count())
                    .uniqueResourcesExternal(uniqueResourcesExternal)
                    .teamCount(relatedTeams.stream().count())

                    .build());

        }


        return map;
    }

    private Map<UUID, DashResponse.TeamSummary2> createTeamSummaryMap(List<Team> teams, List<ProductArea> productAreas, List<Cluster> clusters) {
        val map = new HashMap<UUID, DashResponse.TeamSummary2>();

        for(val team : teams){

            val uniqueResourcesExternal = team.getMembers().stream()
                    .map(teamMember -> nomClient.getByNavIdent(teamMember.getNavIdent()).orElse(null))
                    .filter(Objects::nonNull)
                    .filter(resource -> resource.getResourceType().equals(ResourceType.EXTERNAL))
                    .count();


            map.put(team.getId(), DashResponse.TeamSummary2.builder()
                    .membershipCount(team.getMembers().stream().count())
                    .ResourcesExternal(uniqueResourcesExternal).build());



        }

        return map;
    }

    private Map<UUID, DashResponse.AreaSummary> createAreaSummaryMap(List<Team> teams, List<ProductArea> productAreas, List<Cluster> clusters) {
        val map = new HashMap<UUID, DashResponse.AreaSummary>();

        for (val pa: productAreas){

            val relatedClusters = clusters.stream().filter(cl -> pa.getId().equals(cl.getProductAreaId())).toList();



            val relatedTeams = teams.stream().filter(team ->
                    pa.getId().equals(team.getProductAreaId())
            ).toList();
            long clusterCount = relatedClusters.size();

            val relatedClusterMembers = relatedClusters.stream().flatMap(cluster -> {return cluster.getMembers().stream();}).toList();
            val subteamMembers = relatedTeams.stream().flatMap(team -> {return team.getMembers().stream();}).toList();
            val relatedClusterSubteams = relatedClusters.stream()
                    .flatMap(cluster -> teams.stream()
                            .filter(team -> team.getClusterIds().contains(cluster.getId()))
                    ).toList();

            val allSubteams = relatedClusterSubteams.stream().map(it -> it.getId()).collect(Collectors.toSet());
            allSubteams.addAll(relatedTeams.stream().map(it -> it.getId()).collect(Collectors.toSet()));



//            val clusterSubTeamMembers = teams.stream()
//                    .filter(team -> {
//                        val teamBelongsToAreaByCluster = relatedClusters.stream()
//                                .map(cl -> cl.getId())
//                                .anyMatch(clId -> team.getClusterIds().contains(clId));
//                        return teamBelongsToAreaByCluster;
//                    })
//                    .filter(team -> {return (relatedClusterSubteams.stream()
//                        .map(it -> it.getId())
//                        .toList()).contains(team.getId());
//                    }
//                    )
//                    .flatMap(subteam -> {return subteam.getMembers().stream();}).toList();


//            long membershipCount = pa.getMembers().size() + relatedClusterMembers.size() + subteamMembers.size()  + clusterSubTeamMembers.size();
            long membershipCount = pa.getMembers().size() + relatedClusterMembers.size() + subteamMembers.size();

            val uniqueResources = StreamUtils.distinctByKey(
                    List.of(
                            pa.getMembers().stream().map(it -> it.getNavIdent()),
                            relatedClusterMembers.stream().map(it -> it.getNavIdent()),
                            subteamMembers.stream().map(it ->  it.getNavIdent())
//                            clusterSubTeamMembers.stream().map(it -> it.getNavIdent())

                    ).stream().reduce((a,b) -> Stream.concat(a,b)).get().toList(), it -> it
            );

            val uniqueResourcesExternal = uniqueResources.stream()
                    .map(ident -> nomClient.getByNavIdent(ident).orElse(null))
                    .filter(Objects::nonNull)
                    .filter(ressource -> ressource.getResourceType().equals(ResourceType.EXTERNAL))
                    .count();


            map.put(pa.getId(), DashResponse.AreaSummary.builder()
                    .clusterCount(clusterCount)
                    .membershipCount(membershipCount)
                    .uniqueResourcesCount(uniqueResources.stream().count())
                    .totalTeamCount(allSubteams.stream().count())
                    .uniqueResourcesExternal(uniqueResourcesExternal)


                    .build());
        }


        return map;
    }


    private DashResponse.TeamSummary calcForTotal(List<Team> teams, List<ProductArea> productAreas, List<Cluster> clusters) {
        return calcForTeams(teams, null, productAreas, null, clusters);
    }

    private DashResponse.TeamSummary calcForArea(List<Team> teams, ProductArea productArea, List<Cluster> clusters) {
        return calcForTeams(teams, productArea, List.of(), null, clusters);
    }

    private DashResponse.TeamSummary calcForCluster(List<Team> teams, Cluster cluster, List<Cluster> clusters) {
        return calcForTeams(teams, null, List.of(), cluster, clusters);
    }

    private DashResponse.TeamSummary calcForTeams(List<Team> teams, ProductArea productArea, List<ProductArea> productAreas, Cluster cluster, List<Cluster> clusters) {
        Map<Role, Integer> roles = new EnumMap<>(Role.class);
        Map<TeamType, Integer> teamTypes = new EnumMap<>(TeamType.class);

        Map<Integer, List<Team>> teamsBuckets = teams.stream().collect(Collectors.groupingBy(t -> groups.ceiling(t.getMembers().size())));
        Map<Integer, List<Team>> extPercentBuckets = teams.stream().collect(Collectors.groupingBy(t -> extPercentGroups.ceiling(percentExternalMembers(t))));

        teams.stream().flatMap(t -> t.getMembers().stream()).flatMap(m -> m.getRoles().stream()).forEach(r -> roles.compute(r, counter));
        teams.forEach(t -> teamTypes.compute(t.getTeamType() == null ? TeamType.UNKNOWN : t.getTeamType(), counter));

        List<Member> productAreaMembers;
        if (cluster != null) {
            productAreaMembers = List.of();
        } else {
            if (productArea != null) {
                productAreaMembers = productArea.getMembersAsSuper();
            } else {
                productAreaMembers = productAreas.stream().flatMap(pa -> pa.getMembers().stream()).collect(Collectors.toList());
            }
        }
        productAreaMembers.stream().flatMap(m -> m.getRoles().stream()).forEach(r -> roles.compute(r, counter));

        List<Member> clusterMembers;
        List<Cluster> paClusters = null;
        if (productArea != null) {
            paClusters = filter(clusters, cl -> productArea.getId().equals(cl.getProductAreaId()));
            clusterMembers = paClusters.stream().flatMap(cl -> cl.getMembers().stream()).collect(Collectors.toList());
        } else {
            if (cluster != null) {
                clusterMembers = cluster.getMembersAsSuper();
            } else {
                clusterMembers = clusters.stream().flatMap(cl -> cl.getMembers().stream()).collect(Collectors.toList());
            }
        }
        clusterMembers.stream().flatMap(m -> m.getRoles().stream()).forEach(r -> roles.compute(r, counter));

        return DashResponse.TeamSummary.builder()
                .productAreaId(productArea != null ? productArea.getId() : cluster != null ? cluster.getProductAreaId() : null)
                .clusterId(cluster != null ? cluster.getId() : null)
                .clusters(paClusters != null ? (long) paClusters.size() : null)
                .teams(teams.size())
                .teamsEditedLastWeek(filter(teams, t -> t.getChangeStamp().getLastModifiedDate().isAfter(LocalDateTime.now().minusDays(7))).size())

                .teamEmpty(teamsBuckets.getOrDefault(0, E).size())
                .teamUpTo5(teamsBuckets.getOrDefault(5, E).size())
                .teamUpTo10(teamsBuckets.getOrDefault(10, E).size())
                .teamUpTo20(teamsBuckets.getOrDefault(20, E).size())
                .teamOver20(teamsBuckets.getOrDefault(Integer.MAX_VALUE, E).size())

                .teamExternal0p(extPercentBuckets.getOrDefault(0, E).size())
                .teamExternalUpto25p(extPercentBuckets.getOrDefault(25, E).size())
                .teamExternalUpto50p(extPercentBuckets.getOrDefault(50, E).size())
                .teamExternalUpto75p(extPercentBuckets.getOrDefault(75, E).size())
                .teamExternalUpto100p(extPercentBuckets.getOrDefault(100, E).size())

                .uniqueResources(countUniqueResources(teams, productAreaMembers, clusterMembers))
                .uniqueResourcesExternal(countUniqueResourcesExternal(teams, productAreaMembers, clusterMembers))
                .totalResources(countResources(teams, productAreaMembers, clusterMembers))

                .roles(roles.entrySet().stream()
                        .map(e -> new DashResponse.RoleCount(e.getKey(), e.getValue())).collect(Collectors.toList()))
                .teamTypes(teamTypes.entrySet().stream()
                        .map(e -> new DashResponse.TeamTypeCount(e.getKey(), e.getValue()))
                        .sorted(Comparator.comparing(DashResponse.TeamTypeCount::getCount))
                        .collect(Collectors.toList()))
                .build();
    }

    private long countUniqueResourcesExternal(List<Team> teams, List<Member> productAreaMembers, List<Member> clusterMembers) {
        return Stream.concat(
                        Stream.concat(
                                productAreaMembers.stream().map(Member::convertToResponse),
                                teams.stream().flatMap(team -> team.getMembers().stream()).map(TeamMember::convertToResponse)
                        ),
                        clusterMembers.stream().map(Member::convertToResponse)
                )
                .filter(m -> ResourceType.EXTERNAL == m.getResource().getResourceType())
                .map(MemberResponse::getNavIdent).distinct()
                .count();
    }

    private long countUniqueResources(List<Team> teams, List<Member> productAreaMembers, List<Member> clusterMembers) {
        return Stream.concat(
                Stream.concat(
                        productAreaMembers.stream().map(Member::getNavIdent),
                        teams.stream().flatMap(team -> team.getMembers().stream().map(TeamMember::getNavIdent))
                ), clusterMembers.stream().map(Member::getNavIdent)
        ).distinct().count();

    }

    private long countResources(List<Team> teams, List<Member> productAreaMembers, List<Member> clusterMembers) {
        return teams.stream().mapToLong(team -> team.getMembers().size()).sum() +
                productAreaMembers.size() + clusterMembers.size();
    }

    private int percentExternalMembers(Team t) {
        if (t.getMembers().isEmpty()) {
            return 0;
        }
        long externalMembers = t.getMembers().stream().map(TeamMember::convertToResponse).filter(m -> ResourceType.EXTERNAL == m.getResource().getResourceType()).count();
        return ((int) externalMembers * 100) / t.getMembers().size();
    }

}