GovernmentBodyChartDataManagerImpl.java

/*
 * Copyright 2010-2025 James Pether Sörling
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *	$Id$
 *  $HeadURL$
*/
package com.hack23.cia.web.impl.ui.application.views.common.chartfactory.impl;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import org.dussan.vaadin.dcharts.DCharts;
import org.dussan.vaadin.dcharts.base.elements.XYseries;
import org.dussan.vaadin.dcharts.data.DataSeries;
import org.dussan.vaadin.dcharts.options.Series;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.hack23.cia.service.external.esv.api.EsvApi;
import com.hack23.cia.service.external.esv.api.GovernmentBodyAnnualOutcomeSummary;
import com.hack23.cia.service.external.esv.api.GovernmentBodyAnnualSummary;
import com.hack23.cia.web.impl.ui.application.views.common.chartfactory.api.GovernmentBodyChartDataManager;
import com.vaadin.ui.AbstractOrderedLayout;
import com.vaadin.ui.VerticalLayout;

/**
 * The Class GovernmentBodyChartDataManagerImpl.
 */
@Service
public final class GovernmentBodyChartDataManagerImpl extends AbstractChartDataManagerImpl
        implements GovernmentBodyChartDataManager {

    /** The Constant ALL_GOVERNMENT_BODIES. */
    private static final String ALL_GOVERNMENT_BODIES = "All government bodies";

    /** The Constant ANNUAL_EXPENDITURE. */
    private static final String ANNUAL_EXPENDITURE = "Annual Expenditure";

    /** The Constant ANNUAL_HEADCOUNT. */
    private static final String ANNUAL_HEADCOUNT = "Annual headcount";

    /** The Constant ANNUAL_HEADCOUNT_ALL_MINISTRIES. */
    private static final String ANNUAL_HEADCOUNT_ALL_MINISTRIES = "Annual headcount, all ministries";

    /** The Constant ANNUAL_HEADCOUNT_SUMMARY_ALL_GOVERNMENT_BODIES. */
    private static final String ANNUAL_HEADCOUNT_SUMMARY_ALL_GOVERNMENT_BODIES =
            "Annual headcount summary, all government bodies";

    /** The Constant ANNUAL_HEADCOUNT_TOTAL_ALL_GOVERNMENT_BODIES. */
    private static final String ANNUAL_HEADCOUNT_TOTAL_ALL_GOVERNMENT_BODIES =
            "Annual headcount total all government bodies";

    /** The Constant ANNUAL_INCOME. */
    private static final String ANNUAL_INCOME = "Annual Income";

    /** The Constant EXPENDITURE_GROUP_NAME. */
    private static final String EXPENDITURE_GROUP_NAME = "Utgiftsområdesnamn";

    /** The Constant INKOMSTTITELGRUPPSNAMN. */
    private static final String INKOMSTTITELGRUPPSNAMN = "Inkomsttitelgruppsnamn";

    /** The Constant INKOMSTTITELSNAMN. */
    private static final String INKOMSTTITELSNAMN = "Inkomsttitelsnamn";

    /** The Constant ANSLAGSPOSTSNAMN. */
    private static final String ANSLAGSPOSTSNAMN = "Anslagspostsnamn";

    /** The esv api. */
    @Autowired
    private EsvApi esvApi;


    /**
     * Adds a data point to the DataSeries if the year and value are valid and value > 0.
     *
     * @param dataSeries the data series
     * @param year the year
     * @param value the value
     */
    private static void addDataPoint(final DataSeries dataSeries, final Integer year, final Number value) {
        if (dataSeries == null || year == null || value == null) {
            return;
        }
        final double doubleValue = value.doubleValue();
        if (doubleValue > 0) {
            // Ensure consistent date format: " 01-JAN-YYYY"
            final String formattedDate = String.format(Locale.ENGLISH," 01-JAN-%d", year);
            dataSeries.add(formattedDate, doubleValue);
        }
    }

    /**
     * Creates a chart using the provided data/series objects, then appends it to the given layout.
     *
     * @param layout the layout
     * @param label the label
     * @param dataSeries the data series
     * @param series the series
     */
    private void addChartToLayout(final AbstractOrderedLayout layout, final String label,
                                  final DataSeries dataSeries, final Series series) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        Objects.requireNonNull(label, "Label cannot be null");

        final DCharts chart = new DCharts()
                .setDataSeries(dataSeries)
                .setOptions(getChartOptions().createOptionsXYDateFloatLogYAxisLegendOutside(series))
                .show();

        ChartUtils.addChart(layout, label, chart, true);
    }

    /**
     * Consolidates logic to retrieve, group, and process data by a descriptive field
     * (e.g., 'Utgiftsområdesnamn' or 'Inkomsttitelgruppsnamn'), feeding the results into a DataSeries.
     *
     * @param dataSeries the data series
     * @param series the series
     * @param groupedData the grouped data
     */
    private void buildAnnualOutcomeDataSeriesByField(final DataSeries dataSeries, final Series series,
            final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> groupedData) {
        Optional.ofNullable(groupedData)
            .ifPresent(data -> data.entrySet().stream()
                .filter(entry -> entry.getValue() != null && !entry.getValue().isEmpty())
                .forEach(entry -> {
                    series.addSeries(new XYseries().setLabel(entry.getKey()));
                    dataSeries.newSeries();

                    entry.getValue().stream()
                        .filter(Objects::nonNull)
                        .forEach(summary -> {
                            // Only process if we have a valid year and value map
                            Optional.ofNullable(summary.getValueMap())
                                .ifPresent(valueMap -> {
                                    // Get the year directly from the summary
                                    final Integer year = summary.getYear();
                                    // Get the total value for this year
                                    final Double totalValue = valueMap.values().stream()
                                        .filter(Objects::nonNull)
                                        .mapToDouble(Number::doubleValue)
                                        .sum();

                                    // Only add if we have valid year and value
                                    if (year != null && totalValue > 0) {
                                        addDataPoint(dataSeries, year, totalValue);
                                    }
                                });
                        });
                }));
    }

    /**
     * Helper method to generate a headcount data series given a map of year -> list of summaries.
     *
     * @param dataSeries the data series
     * @param series the series
     * @param yearlyData the yearly data
     * @param label the label
     */
    private void buildHeadcountDataSeries(final DataSeries dataSeries, final Series series,
                                          final Map<Integer, List<GovernmentBodyAnnualSummary>> yearlyData,
                                          final String label) {
    	Optional.ofNullable(yearlyData)
        .ifPresent(data -> {
            series.addSeries(new XYseries().setLabel(label));
            dataSeries.newSeries();

            data.entrySet().stream()
                .filter(entry -> Objects.nonNull(entry.getValue()) && !entry.getValue().isEmpty())
                .forEach(entry -> {
                    final int totalHeadcount = entry.getValue().stream()
                        .filter(Objects::nonNull)
                        .mapToInt(GovernmentBodyAnnualSummary::getHeadCount)
                        .sum();
                    addDataPoint(dataSeries, entry.getKey(), totalHeadcount);
                });
        });
    }

    /**
     * Consolidates logic for creating a chart that is grouped by some string field in descriptionFields.
     *
     * @param layout the layout
     * @param field the field
     * @param chartLabel the chart label
     */
    private void createMinistryFieldSummary(final AbstractOrderedLayout layout, final String field, final String chartLabel) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        Objects.requireNonNull(field, "Field cannot be null");
        Objects.requireNonNull(chartLabel, "Chart label cannot be null");

        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();

        Optional.ofNullable(esvApi.getGovernmentBodyReportByMinistry())
            .ifPresent(reportByMinistry ->
                reportByMinistry.entrySet().stream()
                    .filter(entry -> entry.getValue() != null && !entry.getValue().isEmpty())
                    .forEach(entry -> {
                        final Map<Integer, Double> annualTotals = entry.getValue().stream()
                            .filter(Objects::nonNull)
                            .filter(summary -> Optional.ofNullable(summary.getDescriptionFields())
                                .map(fields -> fields.get(field))
                                .isPresent())
                            .collect(Collectors.groupingBy(
                                GovernmentBodyAnnualOutcomeSummary::getYear,
                                Collectors.summingDouble(GovernmentBodyAnnualOutcomeSummary::getYearTotal)
                            ));

                        if (!annualTotals.isEmpty()) {
                            series.addSeries(new XYseries().setLabel(entry.getKey()));
                            dataSeries.newSeries();
                            annualTotals.forEach((year, total) ->
                                addDataPoint(dataSeries, year + 1, total));
                        }
                    }));

        addChartToLayout(layout, chartLabel, dataSeries, series);
    }

    // ==================== Public Chart Methods ====================

    @Override
    public void createGovernmentBodyExpenditureSummaryChart(final VerticalLayout layout) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> report =
                esvApi.getGovernmentBodyReportByField(EXPENDITURE_GROUP_NAME);

        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();
        buildAnnualOutcomeDataSeriesByField(dataSeries, series, report);

        addChartToLayout(layout, ANNUAL_EXPENDITURE, dataSeries, series);
    }

    @Override
    public void createGovernmentBodyExpenditureSummaryChart(final VerticalLayout layout,
                                                            final String governmentBodyName) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

        final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> allSummaries = esvApi.getGovernmentBodyReport();

        final List<GovernmentBodyAnnualOutcomeSummary> summariesForBody = Optional.ofNullable(allSummaries)
                .map(summaries -> summaries.get(governmentBodyName))
                .orElse(Collections.emptyList());

        final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> grouped = summariesForBody.stream()
                .filter(Objects::nonNull)
                .filter(summary -> Optional.ofNullable(summary.getDescriptionFields())
                    .map(fields -> fields.get(ANSLAGSPOSTSNAMN))
                    .isPresent())
                .sorted(Comparator.comparing(GovernmentBodyAnnualOutcomeSummary::getYear))
                .collect(Collectors.groupingBy(
                    summary -> summary.getDescriptionFields().get(ANSLAGSPOSTSNAMN)));

        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();
        buildAnnualOutcomeDataSeriesByField(dataSeries, series, grouped);

        addChartToLayout(layout, governmentBodyName + " " + ANNUAL_EXPENDITURE, dataSeries, series);
    }

    @Override
    public void createGovernmentBodyHeadcountSummaryChart(final VerticalLayout layout) {
        Objects.requireNonNull(layout, "Layout cannot be null");

        final Map<Integer, List<GovernmentBodyAnnualSummary>> map = esvApi.getData();
        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();

        buildHeadcountDataSeries(dataSeries, series, map, ALL_GOVERNMENT_BODIES);
        addChartToLayout(layout, ANNUAL_HEADCOUNT_TOTAL_ALL_GOVERNMENT_BODIES, dataSeries, series);
    }

    @Override
    public void createGovernmentBodyHeadcountSummaryChart(final VerticalLayout layout,
                                                          final String governmentBodyName) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

        final Map<Integer, GovernmentBodyAnnualSummary> map = esvApi.getDataPerGovernmentBody(governmentBodyName);

        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();
        series.addSeries(new XYseries().setLabel(governmentBodyName));
        dataSeries.newSeries();

        Optional.ofNullable(map)
        .ifPresent(yearSummaryMap -> yearSummaryMap.entrySet().stream()
            .filter(entry -> Objects.nonNull(entry.getValue()))
            .forEach(entry -> addDataPoint(dataSeries, entry.getKey(), entry.getValue().getHeadCount())));

        addChartToLayout(layout, governmentBodyName + " " + ANNUAL_HEADCOUNT, dataSeries, series);
    }

    @Override
    public void createGovernmentBodyIncomeSummaryChart(final VerticalLayout layout) {
        Objects.requireNonNull(layout, "Layout cannot be null");

        final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> report =
                esvApi.getGovernmentBodyReportByField(INKOMSTTITELGRUPPSNAMN);

        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();
        buildAnnualOutcomeDataSeriesByField(dataSeries, series, report);

        addChartToLayout(layout, ANNUAL_INCOME, dataSeries, series);
    }

    @Override
    public void createGovernmentBodyIncomeSummaryChart(final VerticalLayout layout,
                                                       final String governmentBodyName) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

        final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> allSummaries = esvApi.getGovernmentBodyReport();

        final List<GovernmentBodyAnnualOutcomeSummary> summariesForBody = Optional.ofNullable(allSummaries)
                .map(summaries -> summaries.get(governmentBodyName))
                .orElse(Collections.emptyList());

        final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> grouped = summariesForBody.stream()
                .filter(Objects::nonNull)
                .filter(summary -> Optional.ofNullable(summary.getDescriptionFields())
                    .map(fields -> fields.get(INKOMSTTITELSNAMN))
                    .isPresent())
                .sorted(Comparator.comparing(GovernmentBodyAnnualOutcomeSummary::getYear))
                .collect(Collectors.groupingBy(
                    summary -> summary.getDescriptionFields().get(INKOMSTTITELSNAMN)));

        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();
        buildAnnualOutcomeDataSeriesByField(dataSeries, series, grouped);

        addChartToLayout(layout, governmentBodyName + " " + ANNUAL_INCOME, dataSeries, series);
    }

    @Override
    public void createMinistryGovernmentBodyExpenditureSummaryChart(final AbstractOrderedLayout layout) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        createMinistryFieldSummary(layout, EXPENDITURE_GROUP_NAME, "MinistryGovernmentBodySpendingSummaryChart");
    }

    @Override
    public void createMinistryGovernmentBodyExpenditureSummaryChart(final VerticalLayout layout,
                                                                    final String governmentBodyName) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

        final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> report =
                esvApi.getGovernmentBodyReportByFieldAndMinistry(EXPENDITURE_GROUP_NAME, governmentBodyName);

        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();
        buildAnnualOutcomeDataSeriesByField(dataSeries, series, report);

        addChartToLayout(layout, governmentBodyName + " " + ANNUAL_EXPENDITURE, dataSeries, series);
    }

    @Override
    public void createMinistryGovernmentBodyHeadcountSummaryChart(final AbstractOrderedLayout layout) {
        Objects.requireNonNull(layout, "Layout cannot be null");

        final Map<Integer, List<GovernmentBodyAnnualSummary>> map = esvApi.getData();
        final List<String> ministryNames = esvApi.getMinistryNames();

        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();

        Optional.ofNullable(ministryNames)
            .filter(names -> map != null)
            .ifPresent(names -> names.stream()
                .forEach(ministryName -> {
                    series.addSeries(new XYseries().setLabel(ministryName));
                    dataSeries.newSeries();

                    map.entrySet().stream()
                        .filter(entry -> entry.getValue() != null)
                        .forEach(entry -> {
                            final int headcount = entry.getValue().stream()
                                .filter(Objects::nonNull)
                                .filter(summary -> summary.getMinistry() != null)
                                .filter(summary -> summary.getMinistry().equalsIgnoreCase(ministryName))
                                .mapToInt(GovernmentBodyAnnualSummary::getHeadCount)
                                .sum();
                            addDataPoint(dataSeries, entry.getKey(), headcount);
                        });
                }));

        addChartToLayout(layout, ANNUAL_HEADCOUNT_ALL_MINISTRIES, dataSeries, series);
    }

    @Override
    public void createMinistryGovernmentBodyHeadcountSummaryChart(final AbstractOrderedLayout layout,
                                                                  final String governmentBodyName) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

        final Map<Integer, List<GovernmentBodyAnnualSummary>> map = esvApi.getDataPerMinistry(governmentBodyName);
        final List<String> governmentBodyNames = esvApi.getGovernmentBodyNames(governmentBodyName);

        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();

        Optional.ofNullable(governmentBodyNames)
            .filter(names -> map != null)
            .ifPresent(names -> names.stream()
                .forEach(govBodyName -> {
                    series.addSeries(new XYseries().setLabel(govBodyName));
                    dataSeries.newSeries();

                    map.entrySet().stream()
                        .filter(entry -> entry.getValue() != null)
                        .forEach(entry -> {
                            final int headcount = entry.getValue().stream()
                                .filter(Objects::nonNull)
                                .filter(summary -> govBodyName.equalsIgnoreCase(summary.getName()))
                                .mapToInt(GovernmentBodyAnnualSummary::getHeadCount)
                                .sum();
                            addDataPoint(dataSeries, entry.getKey(), headcount);
                        });
                }));

        if (map != null && governmentBodyNames != null) {
            addChartToLayout(layout,
                    governmentBodyName + " " + ANNUAL_HEADCOUNT_SUMMARY_ALL_GOVERNMENT_BODIES,
                    dataSeries,
                    series);
        }
    }

    @Override
    public void createMinistryGovernmentBodyIncomeSummaryChart(final AbstractOrderedLayout layout) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        createMinistryFieldSummary(layout, INKOMSTTITELGRUPPSNAMN, "MinistryGovernmentBodyIncomeSummaryChart");
    }

    @Override
    public void createMinistryGovernmentBodyIncomeSummaryChart(final VerticalLayout layout,
                                                               final String governmentBodyName) {
        Objects.requireNonNull(layout, "Layout cannot be null");
        Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

        final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> report =
                esvApi.getGovernmentBodyReportByFieldAndMinistry(INKOMSTTITELGRUPPSNAMN, governmentBodyName);

        final DataSeries dataSeries = new DataSeries();
        final Series series = new Series();
        buildAnnualOutcomeDataSeriesByField(dataSeries, series, report);

        addChartToLayout(layout, governmentBodyName + " " + ANNUAL_INCOME, dataSeries, series);
    }
}