package com.andrewlalis.perfin.view.component.module;

import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.TimestampRange;
import com.andrewlalis.perfin.data.TransactionRepository;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.model.Transaction;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.*;
import javafx.scene.control.ChoiceBox;
import javafx.scene.layout.Pane;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;

/**
 * A module for visualizing the total asset value in the user's profile over
 * a configurable period of time.
 */
public class TotalAssetsGraphModule extends DashboardModule {
    private final ObservableList<XYChart.Data<String, Number>> totalAssetDataPoints = FXCollections.observableArrayList();
    private final ChoiceBox<Currency> currencyChoiceBox = new ChoiceBox<>();
    private final ChoiceBox<String> timeRangeChoiceBox = new ChoiceBox<>();

    private static final String PREFERRED_CURRENCY_SETTING = "charts.total-assets.default-currency";
    private static final String PREFERRED_TIME_RANGE_SETTING = "charts.total-assets.default-time-range";

    private record TimeRangeOption(String name, Supplier<TimestampRange> rangeSupplier) {}

    private static final TimeRangeOption[] TIME_RANGE_OPTIONS = {
            new TimeRangeOption("Last 90 Days", () -> TimestampRange.lastNDays(90)),
            new TimeRangeOption("Last 6 Months", () -> TimestampRange.lastNMonths(6)),
            new TimeRangeOption("Last 12 Months", () -> TimestampRange.lastNMonths(12)),
            new TimeRangeOption("This Year", TimestampRange::thisYear),
            new TimeRangeOption("Last 5 Years", () -> TimestampRange.lastNMonths(60)),
            new TimeRangeOption("All Time", TimestampRange::unbounded)
    };

    public TotalAssetsGraphModule(Pane parent) {
        super(parent);
        Axis<String> xAxis = new CategoryAxis();
        Axis<Number> yAxis = new NumberAxis();
        xAxis.setAnimated(false);
        yAxis.setAnimated(false);

        LineChart<String, Number> chart = new LineChart<>(xAxis, yAxis, FXCollections.observableArrayList(
                new XYChart.Series<>("Total Assets", totalAssetDataPoints)
        ));
        chart.setLegendVisible(false);
        chart.setAnimated(false);
        // Init currency choice box.
        currencyChoiceBox.managedProperty().bind(currencyChoiceBox.visibleProperty());
        currencyChoiceBox.valueProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue != null) {
                renderChart();
            } else {
                totalAssetDataPoints.clear();
            }
        });
        // Init time range choice box.
        timeRangeChoiceBox.getItems().addAll(
                Arrays.stream(TIME_RANGE_OPTIONS).map(TimeRangeOption::name).toList()
        );
        timeRangeChoiceBox.getSelectionModel().select(TIME_RANGE_OPTIONS[0].name());
        timeRangeChoiceBox.valueProperty().addListener((observable, oldValue, newValue) -> renderChart());
        // Add all the elements to the module VBox.
        this.getChildren().add(new ModuleHeader(
                "Total Assets over Time",
                timeRangeChoiceBox,
                currencyChoiceBox
        ));
        this.getChildren().add(chart);
    }

    @Override
    public void refreshContents() {
        refreshCurrencies();
        String savedTimeRangeLabel = Profile.getCurrent().getSetting(PREFERRED_TIME_RANGE_SETTING).orElse(null);
        if (savedTimeRangeLabel != null && Arrays.stream(TIME_RANGE_OPTIONS).anyMatch(o -> o.name().equals(savedTimeRangeLabel))) {
            timeRangeChoiceBox.getSelectionModel().select(savedTimeRangeLabel);
        }
    }

    private void renderChart() {
        totalAssetDataPoints.clear();
        final Currency currency = currencyChoiceBox.getValue();
        String timeRangeLabel = timeRangeChoiceBox.getValue();
        if (currency == null || timeRangeLabel == null) {
            return;
        }

        getSelectedTimestampRange().thenAccept(range -> {
            Duration rangeDuration = Duration.between(range.start(), range.end());
            boolean useMonths = rangeDuration.dividedBy(Duration.ofDays(1)) > 365;


            List<CompletableFuture<Number>> dataFutures = new ArrayList<>();
            List<String> labels = new ArrayList<>();
            LocalDateTime currentInterval = range.start();
            while (currentInterval.isBefore(range.end())) {
                Instant currentInstant = currentInterval.toInstant(ZoneOffset.UTC);
                final String label;
                if (useMonths) {
                    label = currentInterval.format(DateTimeFormatter.ofPattern("MMM yyyy"));
                } else {
                    label = currentInterval.format(DateTimeFormatter.ofPattern("dd MMM yyyy"));
                }
                labels.add(label);
                dataFutures.add(Profile.getCurrent().dataSource().getCombinedAccountBalances(currentInstant)
                        .thenApply(moneyValues -> moneyValues.stream()
                                .filter(m -> m.currency().equals(currency))
                                .map(m -> m.amount().doubleValue())
                                .findFirst().orElse(0.0)
                        ));
                if (useMonths) {
                    currentInterval = currentInterval.plusMonths(1);
                } else {
                    currentInterval = currentInterval.plusDays(1);
                }
            }

            // Once all futures are complete, we build the final list of data points
            // for the chart, and send them off.
            CompletableFuture.allOf(dataFutures.toArray(new CompletableFuture[0])).thenRun(() -> {
                List<XYChart.Data<String, Number>> dataPoints = new ArrayList<>(dataFutures.size());
                for (int i = 0; i < dataFutures.size(); i++) {
                    dataPoints.add(new XYChart.Data<>(labels.get(i), dataFutures.get(i).join()));
                }
                Platform.runLater(() -> totalAssetDataPoints.setAll(dataPoints));
            });
        });

        Profile.getCurrent().setSettingAndSave(PREFERRED_CURRENCY_SETTING, currency.getCurrencyCode());
        Profile.getCurrent().setSettingAndSave(PREFERRED_TIME_RANGE_SETTING, timeRangeLabel);
    }

    private CompletableFuture<TimestampRange> getSelectedTimestampRange() {
        String selectedLabel = timeRangeChoiceBox.getValue();
        if (selectedLabel == null || Arrays.stream(TIME_RANGE_OPTIONS).noneMatch(o -> o.name().equals(selectedLabel))) {
            return CompletableFuture.completedFuture(TimestampRange.thisYear());
        }
        if (selectedLabel.equals("All Time")) {
            return Profile.getCurrent().dataSource().mapRepoAsync(
                    TransactionRepository.class,
                    repo -> repo.findEarliest().map(Transaction::getTimestamp)
                            .orElse(LocalDateTime.now(ZoneOffset.UTC).minusYears(1))
            ).thenApply(ts -> new TimestampRange(ts, LocalDateTime.now(ZoneOffset.UTC)));
        }
        return CompletableFuture.completedFuture(Arrays.stream(TIME_RANGE_OPTIONS)
                .filter(o -> o.name().equals(selectedLabel))
                .findFirst()
                .map(o -> o.rangeSupplier().get())
                .orElseThrow());
    }

    private void refreshCurrencies() {
        Profile.getCurrent().dataSource().mapRepoAsync(
            AccountRepository.class,
            AccountRepository::findAllUsedCurrencies
        )
            .thenAccept(currencies -> {
                final List<Currency> orderedCurrencies = currencies.isEmpty()
                        ? List.of(Currency.getInstance("USD"))
                        : currencies.stream()
                        .sorted(Comparator.comparing(Currency::getCurrencyCode))
                        .toList();
                final Currency preferredCurrency = Profile.getCurrent().getSetting(PREFERRED_CURRENCY_SETTING)
                        .map(Currency::getInstance).orElse(null);
                Platform.runLater(() -> {
                    currencyChoiceBox.getItems().setAll(orderedCurrencies);
                    if (preferredCurrency != null && currencies.contains(preferredCurrency)) {
                        currencyChoiceBox.getSelectionModel().select(preferredCurrency);
                    } else {
                        currencyChoiceBox.getSelectionModel().selectFirst();
                    }
                    currencyChoiceBox.setVisible(orderedCurrencies.size() > 1);
                });
            });
    }
}
