module transaction.data_impl_sqlite;

import handy_http_primitives : Optional;
import std.datetime;
import std.typecons;
import d2sqlite3;

import transaction.model;
import transaction.data;
import transaction.dto;
import util.sqlite;
import util.money;
import util.pagination;
import util.data;

class SqliteTransactionVendorRepository : TransactionVendorRepository {
    private Database db;
    this(Database db) {
        this.db = db;
    }
    
    Optional!TransactionVendor findById(ulong id) {
        return util.sqlite.findById(db, "transaction_vendor", &parseVendor, id);
    }

    TransactionVendor[] findAll() {
        return util.sqlite.findAll(
            db,
            "SELECT * FROM transaction_vendor ORDER BY name ASC",
            &parseVendor
        );
    }

    bool existsByName(string name) {
        return util.sqlite.exists(db, "SELECT id FROM transaction_vendor WHERE name = ?", name);
    }

    bool existsById(ulong id) {
        return util.sqlite.exists(db, "SELECT id FROM transaction_vendor WHERE id = ?", id);
    }

    TransactionVendor insert(string name, string description) {
        util.sqlite.update(
            db,
            "INSERT INTO transaction_vendor (name, description) VALUES (?, ?)",
            name, description
        );
        ulong id = db.lastInsertRowid();
        return findById(id).orElseThrow();
    }

    void deleteById(ulong id) {
        util.sqlite.deleteById(db, "transaction_vendor", id);
    }

    TransactionVendor updateById(ulong id, string name, string description) {
        util.sqlite.update(
            db,
            "UPDATE transaction_vendor SET name = ?, description = ? WHERE id = ?",
            name, description, id
        );
        return findById(id).orElseThrow();
    }

    private static TransactionVendor parseVendor(Row row) {
        return TransactionVendor(
            row.peek!ulong(0),
            row.peek!string(1),
            row.peek!string(2)
        );
    }
}

class SqliteTransactionCategoryRepository : TransactionCategoryRepository {
    private Database db;
    this(Database db) {
        this.db = db;
    }
    
    Optional!TransactionCategory findById(ulong id) {
        return util.sqlite.findById(db, "transaction_category", &parseCategory, id);
    }

    bool existsById(ulong id) {
        return util.sqlite.exists(db, "SELECT id FROM transaction_category WHERE id = ?", id);
    }

    TransactionCategory[] findAllByParentId(Optional!ulong parentId) {
        if (parentId) {
            return util.sqlite.findAll(
                db,
                "SELECT * FROM transaction_category WHERE parent_id = ? ORDER BY name ASC",
                &parseCategory,
                parentId.value
            );
        }
        return util.sqlite.findAll(
            db,
            "SELECT * FROM transaction_category WHERE parent_id IS NULL ORDER BY name ASC",
            &parseCategory
        );
    }

    TransactionCategory insert(Optional!ulong parentId, string name, string description, string color) {
        util.sqlite.update(
            db,
            "INSERT INTO transaction_category
            (parent_id, name, description, color)
            VALUES (?, ?, ?, ?)",
            toNullable(parentId), name, description, color
        );
        ulong id = db.lastInsertRowid();
        return findById(id).orElseThrow();
    }

    void deleteById(ulong id) {
        util.sqlite.deleteById(db, "transaction_category", id);
    }

    TransactionCategory updateById(ulong id, string name, string description, string color) {
        util.sqlite.update(
            db,
            "UPDATE transaction_category
            SET name = ?, description = ?, color = ?
            WHERE id = ?",
            name, description, color, id
        );
        return findById(id).orElseThrow();
    }

    private static TransactionCategory parseCategory(Row row) {
        import std.typecons;
        return TransactionCategory(
            row.peek!ulong(0),
            toOptional(row.peek!(Nullable!ulong)(1)),
            row.peek!string(2),
            row.peek!string(3),
            row.peek!string(4)
        );
    }
}

class SqliteTransactionTagRepository : TransactionTagRepository {
    private Database db;
    this(Database db) {
        this.db = db;
    }

    string[] findAllByTransactionId(ulong transactionId) {
        return util.sqlite.findAll(
            db,
            "SELECT tag FROM transaction_tag WHERE transaction_id = ? ORDER BY tag",
            r => r.peek!string(0),
            transactionId
        );
    }

    void updateTags(ulong transactionId, in string[] tags) {
        util.sqlite.update(
            db,
            "DELETE FROM transaction_tag WHERE transaction_id = ?",
            transactionId
        );
        foreach (tag; tags) {
            util.sqlite.update(
                db,
                "INSERT INTO transaction_tag (transaction_id, tag) VALUES (?, ?)",
                transactionId, tag
            );
        }
    }

    string[] findAll() {
        return util.sqlite.findAll(
            db,
            "SELECT DISTINCT tag FROM transaction_tag ORDER BY tag",
            r => r.peek!string(0)
        );
    }
}

class SqliteTransactionRepository : TransactionRepository {
    private const TABLE_NAME = "\"transaction\"";
    private Database db;
    this(Database db) {
        this.db = db;
    }

    Page!TransactionsListItem findAll(PageRequest pr) {
        const BASE_QUERY = import("sql/get_transactions.sql");
        // TODO: Implement filtering or something!
        import std.array;
        const string countQuery = "SELECT COUNT(ID) FROM " ~ TABLE_NAME;
        auto sqlBuilder = appender!string;
        sqlBuilder ~= BASE_QUERY;
        sqlBuilder ~= " ";
        sqlBuilder ~= pr.toSql();
        string query = sqlBuilder[];
        TransactionsListItem[] results = util.sqlite.findAll(db, query, (row) {
            TransactionsListItem item;
            item.id = row.peek!ulong(0);
            item.timestamp = row.peek!string(1);
            item.addedAt = row.peek!string(2);
            item.amount = row.peek!ulong(3);
            item.currency = Currency.ofCode(row.peek!(string, PeekMode.slice)(4));
            item.description = row.peek!string(5);

            Nullable!ulong vendorId = row.peek!(Nullable!ulong)(6);
            if (!vendorId.isNull) {
                string vendorName = row.peek!string(7);
                item.vendor = Optional!(TransactionsListItem.Vendor).of(
                    TransactionsListItem.Vendor(vendorId.get, vendorName));
            }
            Nullable!ulong categoryId = row.peek!(Nullable!ulong)(8);
            if (!categoryId.isNull) {
                string categoryName = row.peek!string(9);
                string categoryColor = row.peek!string(10);
                item.category = Optional!(TransactionsListItem.Category).of(
                    TransactionsListItem.Category(categoryId.get, categoryName, categoryColor));
            }
            Nullable!ulong creditedAccountId = row.peek!(Nullable!ulong)(11);
            if (!creditedAccountId.isNull) {
                ulong id = creditedAccountId.get;
                string name = row.peek!string(12);
                string type = row.peek!string(13);
                string suffix = row.peek!string(14);
                item.creditedAccount = Optional!(TransactionsListItem.Account).of(
                    TransactionsListItem.Account(id, name, type, suffix));
            }
            Nullable!ulong debitedAccountId = row.peek!(Nullable!ulong)(15);
            if (!debitedAccountId.isNull) {
                ulong id = debitedAccountId.get;
                string name = row.peek!string(16);
                string type = row.peek!string(17);
                string suffix = row.peek!string(18);
                item.debitedAccount = Optional!(TransactionsListItem.Account).of(
                    TransactionsListItem.Account(id, name, type, suffix));
            }
            string tagsStr = row.peek!string(19);
            if (tagsStr !is null && tagsStr.length > 0) {
                import std.string : split;
                item.tags = tagsStr.split(",");
            } else {
                item.tags = [];
            }
            return item;
        });
        ulong totalCount = util.sqlite.count(db, countQuery);
        return Page!(TransactionsListItem).of(results, pr, totalCount);
    }

    Optional!TransactionDetail findById(ulong id) {
        Optional!TransactionDetail item = util.sqlite.findOne(
            db,
            import("sql/get_transaction.sql"),
            (row) {
                TransactionDetail item;
                item.id = row.peek!ulong(0);
                item.timestamp = row.peek!string(1);
                item.addedAt = row.peek!string(2);
                item.amount = row.peek!ulong(3);
                item.currency = Currency.ofCode(row.peek!(string, PeekMode.slice)(4));
                item.description = row.peek!string(5);

                Nullable!ulong vendorId = row.peek!(Nullable!ulong)(6);
                if (!vendorId.isNull) {
                    item.vendor = Optional!(TransactionDetail.Vendor).of(
                        TransactionDetail.Vendor(
                            vendorId.get,
                            row.peek!string(7),
                            row.peek!string(8)
                        )).toNullable;
                }
                Nullable!ulong categoryId = row.peek!(Nullable!ulong)(9);
                if (!categoryId.isNull) {
                    item.category = Optional!(TransactionDetail.Category).of(
                        TransactionDetail.Category(
                            categoryId.get,
                            row.peek!(Nullable!ulong)(10),
                            row.peek!string(11),
                            row.peek!string(12),
                            row.peek!string(13)
                        )).toNullable;
                }
                Nullable!ulong creditedAccountId = row.peek!(Nullable!ulong)(14);
                if (!creditedAccountId.isNull) {
                    item.creditedAccount = Optional!(TransactionDetail.Account).of(
                        TransactionDetail.Account(
                            creditedAccountId.get,
                            row.peek!string(15),
                            row.peek!string(16),
                            row.peek!string(17)
                        )).toNullable;
                }
                Nullable!ulong debitedAccountId = row.peek!(Nullable!ulong)(18);
                if (!debitedAccountId.isNull) {
                    item.debitedAccount = Optional!(TransactionDetail.Account).of(
                        TransactionDetail.Account(
                            debitedAccountId.get,
                            row.peek!string(19),
                            row.peek!string(20),
                            row.peek!string(21)
                        )).toNullable;
                }
                string tagsStr = row.peek!string(22);
                if (tagsStr !is null && tagsStr.length > 0) {
                    import std.string : split;
                    item.tags = tagsStr.split(",");
                } else {
                    item.tags = [];
                }
                return item;
            },
            id
        );
        if (item.isNull) return item;
        item.value.lineItems = util.sqlite.findAll(
            db,
            import("sql/get_line_items.sql"),
            (row) {
                TransactionDetail.LineItem li;
                li.idx = row.peek!uint(0);
                li.valuePerItem = row.peek!long(1);
                li.quantity = row.peek!ulong(2);
                li.description = row.peek!string(3);
                Nullable!ulong categoryId = row.peek!(Nullable!ulong)(4);
                if (!categoryId.isNull) {
                    li.category = Optional!(TransactionDetail.Category).of(
                        TransactionDetail.Category(
                            categoryId.get,
                            row.peek!(Nullable!ulong)(5),
                            row.peek!string(6),
                            row.peek!string(7),
                            row.peek!string(8)
                        )).toNullable;
                }
                return li;
            },
            id
        );
        return item;
    }

    TransactionDetail insert(in AddTransactionPayload data) {
        util.sqlite.update(
            db,
            import("sql/insert_transaction.sql"),
            data.timestamp,
            Clock.currTime(UTC()),
            data.amount,
            data.currencyCode,
            data.description,
            data.vendorId,
            data.categoryId
        );
        ulong transactionId = db.lastInsertRowid();
        // Insert line items:
        foreach (size_t idx, lineItem; data.lineItems) {
            util.sqlite.update(
                db,
                import("sql/insert_line_item.sql"),
                transactionId,
                idx,
                lineItem.valuePerItem,
                lineItem.quantity,
                lineItem.description,
                lineItem.categoryId
            );
        }
        return findById(transactionId).orElseThrow();
    }

    void deleteById(ulong id) {
        util.sqlite.deleteById(db, TABLE_NAME, id);
    }

    static Transaction parseTransaction(Row row) {
        import std.typecons : Nullable;
        return Transaction(
            row.peek!ulong(0),
            SysTime.fromISOExtString(row.peek!string(1)),
            SysTime.fromISOExtString(row.peek!string(2)),
            row.peek!ulong(3),
            Currency.ofCode(row.peek!(string, PeekMode.slice)(4)),
            row.peek!string(5),
            toOptional(row.peek!(Nullable!ulong)(6)),
            toOptional(row.peek!(Nullable!ulong)(7))
        );
    }
}
