/**
 * This module provides helper logic for applying filters when searching over
 * transactions.
 */
module transaction.search_filters;

import handy_http_primitives;
import std.algorithm;
import std.conv;
import std.array;
import std.range;
import std.string;
import std.uri;
import std.uni;

import util.sqlite;

/**
 * Applies a set of filters to a query builder for searching over transactions.
 * Params:
 *   qb = The query builder to add WHERE clauses and argument bindings to.
 *   request = The request to get filter options from.
 */
void applyFilters(ref QueryBuilder qb, in ServerHttpRequest request) {
    applyPropertyInFilter!string(qb, request, "tags.tag", "tag");
    applyPropertyInFilter!ulong(qb, request, "vendor.id", "vendor");
    applyPropertyInFilter!ulong(qb, request, "category.id", "category");
    applyPropertyInFilter!string(qb, request, "txn.currency", "currency");
    applyPropertyInFilter!ulong(qb, request, "account_credit.id", "credited-account");
    applyPropertyInFilter!ulong(qb, request, "account_debit.id", "debited-account");

    // Separate filter that combines both credit and debit accounts.
    if (request.hasParam("account")) {
        ulong[] accountIds = request.getParamValues!ulong("account");
        string inStr = "(" ~ "?".repeat(accountIds.length).join(",") ~ ")";
        qb.where("(account_credit.id IN " ~ inStr ~ " OR account_debit.id IN " ~ inStr ~ ")");
        qb.withArgBinding((ref stmt, ref idx) {
            foreach (value; accountIds) {
                stmt.bind(idx++, value);
            }
            // Again for the second IN clause.
            foreach (value; accountIds) {
                stmt.bind(idx++, value);
            }
        });
    }

    if (request.hasParam("min-amount")) {
        ulong[] values = request.getParamValues!ulong("min-amount");
        if (values.length > 0) {
            ulong minAmount = values[0];
            qb.where("txn.amount >= ?");
            qb.withArgBinding((ref stmt, ref idx) {
                stmt.bind(idx++, minAmount);
            });
        }
    }

    if (request.hasParam("max-amount")) {
        ulong[] values = request.getParamValues!ulong("max-amount");
        if (values.length > 0) {
            ulong minAmount = values[0];
            qb.where("txn.amount <= ?");
            qb.withArgBinding((ref stmt, ref idx) {
                stmt.bind(idx++, minAmount);
            });
        }
    }
    
    // Boolean filter for internal transfer.
    if (request.hasParam("internal-transfer")) {
        string value = request.getParamValues("internal-transfer")[0]
            .strip()
            .toUpper();
        Optional!bool internalTransferFilter = Optional!bool.empty();
        if (value == "Y" || value == "YES" || value == "TRUE" || value == "1") {
            internalTransferFilter = Optional!bool.of(true);
        } else if (value == "N" || value == "NO" || value == "FALSE" || value == "0") {
            internalTransferFilter = Optional!bool.of(false);
        }
        if (!internalTransferFilter.isNull) {
            qb.where("txn.internal_transfer = ?");
            qb.withArgBinding((ref stmt, ref idx) {
                stmt.bind(idx++, internalTransferFilter.value);
            });
        }
    }

    // Textual search query:
    if (request.hasParam("q")) {
        string searchQuery = request.getParamValues!string("q")[0];
        string likeStr = "%" ~ toUpper(strip(searchQuery)) ~ "%";
        const string[] conditions = [
            "UPPER(txn.description) LIKE ?",
            "UPPER(vendor.name) LIKE ?",
            "UPPER(category.name) LIKE ?",
            "UPPER(account_credit.name) LIKE ?",
            "UPPER(account_debit.name) LIKE ?",
            "UPPER(tags.tag) LIKE ?"
        ];
        qb.where("(" ~ conditions.join(" OR ") ~ ")");
        qb.withArgBinding((ref stmt, ref idx) {
            for (int i = 0; i < conditions.length; i++) {
                stmt.bind(idx++, likeStr);
            }
        });
    }
}

private void applyPropertyInFilter(T)(
    ref QueryBuilder qb,
    in ServerHttpRequest request,
    string property,
    string key
) {
    if (request.hasParam(key)) {
        T[] values = request.getParamValues!T(key);
        qb.where(property ~ " IN (" ~ "?".repeat(values.length).join(",") ~ ")");
        qb.withArgBinding((ref stmt, ref idx) {
            foreach (value; values) {
                stmt.bind(idx++, value);
            }
        });
        return;
    }
}

private bool hasParam(in ServerHttpRequest request, string key) {
    foreach (param; request.queryParams) {
        if (param.key == key && param.values.length > 0) return true;
    }
    return false;
}

private T[] getParamValues(T = string)(in ServerHttpRequest request, string key) {
    foreach (param; request.queryParams) {
        if (param.key == key) {
            return param.values.map!(s => s.to!T).array;
        }
    }
    return [];
}
