module util.sample_data;

import slf4d;
import handy_http_primitives : Optional, mapIfPresent, toOptional;

import auth;
import profile;
import account;
import transaction;
import transaction.dto;
import util.money;
import util.data;

import std.random;
import std.conv;
import std.array;
import std.datetime;
import std.typecons;
import std.file;
import std.algorithm;
import std.csv;

void generateSampleData() {
    UserRepository userRepo = new FileSystemUserRepository;
    // Remove all existing user data.
    foreach (User user; userRepo.findAll()) {
        userRepo.deleteByUsername(user.username);
    }

    const int userCount = uniform(5, 10);
    for (int i = 0; i < userCount; i++) {
        try {
            generateRandomUser(i, userRepo);
        } catch (Throwable t) {
            import std.stdio;
            stderr.writeln(t);
            throw t;
        }
    }
    info("Random sample data generation complete.");
}

void generateRandomUser(int idx, UserRepository userRepo) {
    string username = "testuser" ~ idx.to!string;
    string password = "testpass";
    infoF!"Generating random user %s, password: %s."(username, password);
    User user = createNewUser(userRepo, username, password);
    ProfileRepository profileRepo = new FileSystemProfileRepository(username);
    const int profileCount = uniform(1, 3);
    for (int i = 0; i < profileCount; i++) {
        generateRandomProfile(i, profileRepo);
    }
}

void generateRandomProfile(int idx, ProfileRepository profileRepo) {
    string profileName = "test-profile-" ~ idx.to!string;
    infoF!"  Generating random profile %s."(profileName);
    Profile profile = profileRepo.createProfile(profileName);
    ProfileDataSource ds = profileRepo.getDataSource(profile);

    ds.getPropertiesRepository().setProperty("sample-data-idx", idx.to!string);
    Currency preferredCurrency = Currencies.USD;

    const int accountCount = uniform(3, 8);
    for (int i = 0; i < accountCount; i++) {
        generateRandomAccount(i, ds, preferredCurrency);
    }

    ds.doTransaction(() {
        generateVendors(ds);
        generateCategories(ds);
    });
    generateRandomTransactions(ds);
}

void generateVendors(ProfileDataSource ds) {
    auto vendorRepo = ds.getTransactionVendorRepository();
    string vendorsCsv = readText("sample-data/vendors.csv");
    uint vendorCount = 0;
    foreach (record; csvReader!(Tuple!(string, string))(vendorsCsv)) {
        vendorRepo.insert(record[0], record[1]);
        vendorCount++;
    }
    infoF!"    Generated %d vendors."(vendorCount);
}

void generateCategories(ProfileDataSource ds) {
    auto categoryRepo = ds.getTransactionCategoryRepository();
    string categoriesCsv = readText("sample-data/categories.csv");
    uint categoryCount = 0;
    foreach (record; csvReader!(Tuple!(string, string, string, string))(categoriesCsv)) {
        string parentName = record[3];
        Optional!ulong parentId = Optional!ulong.empty;
        if (parentName !is null && parentName.length > 0) {
            parentId = categoryRepo.findByName(parentName).mapIfPresent!(c => c.id);
        }
        categoryRepo.insert(parentId, record[0], record[2], record[1][1..$]);
    }
    infoF!"    Generated %d categories."(categoryCount);
}

void generateRandomAccount(int idx, ProfileDataSource ds, Currency preferredCurrency) {
    AccountRepository accountRepo = ds.getAccountRepository();
    AccountValueRecordRepository valueRecordRepo = ds.getAccountValueRecordRepository();
    string idxStr = idx.to!string;
    string numberSuffix = "0".replicate(4 - idxStr.length) ~ idxStr;
    string name = "Test Account " ~ idxStr;
    AccountType type = choice(ALL_ACCOUNT_TYPES);
    Currency currency = preferredCurrency;
    if (uniform01() < 0.1) {
        currency = choice(ALL_CURRENCIES);
    }
    string description = "This is a testing account generated by util.sample_data.generateRandomAccount().";
    Account account = accountRepo.insert(
        type,
        numberSuffix,
        name,
        currency,
        description
    );
    long balance = uniform(-1_000_000, 1_000_000);
    ulong accountAge = uniform(5, 365*10);
    SysTime accountOpenedAt = Clock.currTime(UTC()) - days(accountAge);
    valueRecordRepo.insert(
        accountOpenedAt,
        account.id,
        AccountValueRecordType.BALANCE,
        balance,
        account.currency
    );
    infoF!"    Generated random account: %s, #%s, %s"(name, numberSuffix, currency.code);
}

void generateRandomTransactions(ProfileDataSource ds) {
    const TransactionVendor[] vendors = ds.getTransactionVendorRepository.findAll();
    const TransactionCategory[] categories = ds.getTransactionCategoryRepository()
        .findAllByParentId(Optional!ulong.empty);
    const Account[] accounts = ds.getAccountRepository().findAll();

    SysTime timestamp = Clock.currTime(UTC()) - seconds(1);

    for (int i = 0; i < 100; i++) {
        AddTransactionPayload data;
        data.timestamp = timestamp.toISOExtString();
        if (uniform01() < 0.7) {
            data.vendorId = Optional!ulong.of(choice(vendors).id);
        }
        if (uniform01() < 0.8) {
            data.categoryId = Optional!ulong.of(choice(categories).id);
        }

        // Randomly choose an account to credit / debit the transaction to.
        Account primaryAccount = choice(accounts);
        data.currencyCode = primaryAccount.currency.code.idup;
        Optional!ulong secondaryAccountId;
        if (uniform01() < 0.25) {
            foreach (acc; accounts) {
                if (acc.id != primaryAccount.id && acc.currency == primaryAccount.currency) {
                    secondaryAccountId = Optional!ulong.of(acc.id);
                    break;
                }
            }
        }
        if (uniform01() < 0.5) {
            data.creditedAccountId = Optional!ulong.of(primaryAccount.id);
            if (secondaryAccountId) data.debitedAccountId = secondaryAccountId;
        } else {
            data.debitedAccountId = Optional!ulong.of(primaryAccount.id);
            if (secondaryAccountId) data.creditedAccountId = secondaryAccountId;
        }

        // Randomly choose some tags to add.
        string[] tags;
        foreach (n; 1..10) {
            if (uniform01 < 0.25) {
                tags ~= "tag-" ~ n.to!string;
            }
        }
        data.tags = tags;

        data.amount = uniform(0, 1_000_000);
        data.description = "This is a sample transaction which was generated as part of sample data.";

        // Generate random line items:
        if (uniform01 < 0.5) {
            long lineItemTotal = 0;
            foreach (n; 1..uniform(1, 20)) {
                AddTransactionPayload.LineItemPayload item;
                item.valuePerItem = uniform(1, 10_000);
                item.quantity = uniform(1, 5);
                lineItemTotal += item.quantity * item.valuePerItem;
                item.description = "Sample item " ~ n.to!string;
                if (uniform01 < 0.5) {
                    TransactionCategory category = choice(categories);
                    item.categoryId = Optional!ulong.of(category.id);
                }
                data.lineItems ~= item;
            }
            long diff = data.amount - lineItemTotal;
            // Add one final line item that adds up to the transaction total.
            if (diff != 0) {
                data.lineItems ~= AddTransactionPayload.LineItemPayload(
                    diff,
                    1,
                    "Last item which reconciles line items total with transaction amount.",
                    Optional!ulong.empty()
                );
            }
        }

        // Add attachments:
        MultipartFile[] attachments;
        if (uniform01 < 0.5) {
            for (int k = 0; k < uniform(1, 3); k++) {
                if (k == 0) {
                    attachments ~= MultipartFile(
                        "1.txt",
                        "text/plain",
                        cast(ubyte[]) std.file.read("sample-data/attachments/1.txt")
                    );
                } else if (k == 1) {
                    attachments ~= MultipartFile(
                        "2.jpg",
                        "image/jpg",
                        cast(ubyte[]) std.file.read("sample-data/attachments/2.jpg")
                    );
                } else if (k == 2) {
                    attachments ~= MultipartFile(
                        "3.png",
                        "image/png",
                        cast(ubyte[]) std.file.read("sample-data/attachments/3.png")
                    );
                }
            }
        }

        auto txn = addTransaction(ds, data, attachments);
        infoF!"    Generated transaction %d"(txn.id);
        timestamp -= seconds(uniform(10, 1_000_000));
    }
}
