module analytics.modules.balances;

import handy_http_primitives : Optional, mapIfPresent;
import std.datetime;
import std.stdio;
import std.path;
import std.file;
import std.algorithm;
import std.array;
import std.conv;
import slf4d;

import profile.data;
import profile.model;
import account.data;
import account.model;
import account.service;
import analytics.util;
import transaction.model;
import transaction.dto;
import util.pagination;
import util.money;
import util.data;
import analytics.data;

struct AccountBalanceData {
    ulong accountId;
    TimeSeriesPoint[] data;
}

struct BalanceTimeSeriesAnalytics {
    AccountBalanceData[] accounts;
    TimeSeriesPoint[] totals;
}

/**
 * Computes a time series tracking the balance of each account (and total of
 * all accounts) over the given time range.
 * Params:
 *   ds = The profile data source.
 *   currency = The currency to report data for.
 *   timeRange = The time range to generate the time series for.
 * Returns: An analytics response containing a "totals" time series, as well
 * as a time series for each known account in the given time range.
 */
BalanceTimeSeriesAnalytics computeBalanceTimeSeries(
    ProfileDataSource ds,
    in Currency currency,
    in TimeRange timeRange
) {
    SysTime[] timestamps = generateTimeSeriesTimestamps(days(1), timeRange);
    AnalyticsRepository repo = ds.getAnalyticsRepository();
    JournalEntryStub[] journalEntries = repo.getJournalEntries(currency, timeRange);
    BalanceRecordStub[] balanceRecords = repo.getBalanceRecords(currency, timeRange);
    auto accountIds = balanceRecords.map!(br => br.accountId).uniq.array.sort;

    BalanceTimeSeriesAnalytics result;

    foreach (timestamp; timestamps) {
        long totalBalance = 0;
        foreach (accountId; accountIds) {
            Optional!long optionalBalance = deriveBalance(accountId, journalEntries, balanceRecords, timestamp);
            if (!optionalBalance.isNull) {
                TimeSeriesPoint p = TimeSeriesPoint(timestamp.toUnixMillis(), optionalBalance.value);
                bool isAccountDataPresent = false;
                foreach (ref accountData; result.accounts) {
                    if (accountData.accountId == accountId) {
                        accountData.data ~= p;
                        isAccountDataPresent = true;
                        break;
                    }
                }
                if (!isAccountDataPresent) {
                    result.accounts ~= AccountBalanceData(accountId, [p]);
                }
                totalBalance += optionalBalance.value;
            }
        }
        result.totals ~= TimeSeriesPoint(timestamp.toUnixMillis(), totalBalance);
    }

    return result;
}

private Optional!long deriveBalance(
    ulong accountId,
    in JournalEntryStub[] journalEntries,
    in BalanceRecordStub[] balanceRecords,
    in SysTime timestamp
) {
    import core.time : abs;
    Optional!BalanceRecordStub nearestBalanceRecord;
    foreach (br; balanceRecords) {
        if (br.accountId == accountId) {
            if (nearestBalanceRecord.isNull) {
                nearestBalanceRecord = Optional!(BalanceRecordStub).of(br);
            } else {
                Duration currentDiff = abs(nearestBalanceRecord.value.timestamp - timestamp);
                Duration newDiff = abs(br.timestamp - timestamp);
                if (newDiff < currentDiff) {
                    nearestBalanceRecord = Optional!(BalanceRecordStub).of(br);
                }
            }
        }
    }
    if (nearestBalanceRecord.isNull) {
        return Optional!(long).empty();
    }
    if (timestamp == nearestBalanceRecord.value.timestamp) {
        return Optional!(long).of(nearestBalanceRecord.value.value);
    }

    // Now that we have a balance record, work our way towards the desired
    // timestamp, applying journal entry changes.
    SysTime startTimestamp;
    SysTime endTimestamp;
    long balance = nearestBalanceRecord.value.value;
    if (timestamp > nearestBalanceRecord.value.timestamp) {
        startTimestamp = nearestBalanceRecord.value.timestamp;
        endTimestamp = timestamp;
    } else {
        startTimestamp = timestamp;
        endTimestamp = nearestBalanceRecord.value.timestamp;
    }
    auto relevantJournalEntries = journalEntries
        .filter!(je => (
            je.accountId == accountId &&
            je.timestamp >= startTimestamp &&
            je.timestamp <= endTimestamp
        ));
    foreach (je; relevantJournalEntries) {
        long entryValue = je.amount;
        if (je.type == AccountJournalEntryType.CREDIT) {
            entryValue *= -1;
        }
        if (!je.accountType.debitsPositive) {
            entryValue *= -1;
        }
        if (je.timestamp < nearestBalanceRecord.value.timestamp) {
            entryValue *= -1;
        }
        balance += entryValue;
    }
    return Optional!(long).of(balance);
}
