module account.data_impl_sqlite;

import std.datetime;

import d2sqlite3;
import handy_http_primitives : Optional;

import account.data;
import account.model;
import history.model;
import util.sqlite;
import util.money;

class SqliteAccountRepository : AccountRepository {
    private Database db;
    this(Database db) {
        this.db = db;
    }

    Optional!Account findById(ulong id) {
        return findOne(db, "SELECT * FROM account WHERE id = ?", &parseAccount, id);
    }

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

    Account insert(AccountType type, string numberSuffix, string name, Currency currency, string description) {
        util.sqlite.update(
            db,
            "INSERT INTO account
            (created_at, type, number_suffix, name, currency, description)
            VALUES (?, ?, ?, ?, ?, ?)",
            Clock.currTime(UTC()).toISOExtString(),
            type.id,
            numberSuffix,
            name,
            currency.code,
            description
        );
        ulong accountId = db.lastInsertRowid();
        return findById(accountId).orElseThrow("Couldn't find account!");
    }

    void setArchived(ulong id, bool archived) {
        util.sqlite.update(
            db,
            "UPDATE account SET archived = ? WHERE id = ?",
            archived ? 1 : 0,
            id
        );
    }

    Account update(ulong id, in Account newData) {
        return doTransaction(db, () {
            Account oldAccount = this.findById(id).orElseThrow("Account doesn't exist.");
            bool typeDiff = oldAccount.type != newData.type;
            bool numberSuffixDiff = oldAccount.numberSuffix != newData.numberSuffix;
            bool nameDiff = oldAccount.name != newData.name;
            bool currencyDiff = oldAccount.currency != newData.currency;
            bool descriptionDiff = oldAccount.description != newData.description;
            util.sqlite.update(
                db,
                q"SQL
                UPDATE account
                SET type = ?,
                    number_suffix = ?,
                    name = ?,
                    currency = ?,
                    description = ?
                WHERE id = ?
SQL",
                newData.type.id,
                newData.numberSuffix,
                newData.name,
                newData.currency.code,
                newData.description,
                id
            );
            return this.findById(id).orElseThrow("Account doesn't exist");
        });
    }

    void deleteById(ulong id) {
        doTransaction(db, () {
            util.sqlite.update(
                db,
                "DELETE FROM history
                WHERE id IN (
                    SELECT history_id FROM account_history
                    WHERE account_id = ?
                )",
                id
            );
            util.sqlite.update(db, "DELETE FROM account WHERE id = ?", id);
        });
    }

    Account[] findAll() {
        return util.sqlite.findAll(db, "SELECT * FROM account", &parseAccount);
    }

    AccountCreditCardProperties getCreditCardProperties(ulong id) {
        Account account = findById(id).orElseThrow("Account doesn't exist.");
        if (account.type != AccountTypes.CREDIT_CARD) throw new Exception("Account is not credit card.");
        auto optionalProps = findOne(
            db,
            "SELECT * FROM account_credit_card_properties WHERE account_id = ?",
            &parseCreditCardProperties,
            id
        );
        if (!optionalProps.isNull) return optionalProps.value;
        // No properties exist, so set them and return the new data.
        const props = AccountCreditCardProperties(account.id, -1);
        util.sqlite.update(
            db,
            "INSERT INTO account_credit_card_properties (account_id, credit_limit) VALUES (?, ?)",
            props.account_id,
            props.creditLimit
        );
        return props;
    }

    void setCreditCardProperties(ulong id, in AccountCreditCardProperties props) {
        bool hasProps = exists(db, "SELECT * FROM account_credit_card_properties WHERE account_id = ?", id);
        if (hasProps) {
            util.sqlite.update(
                db,
                "UPDATE account_credit_card_properties SET credit_limit = ? WHERE account_id = ?",
                props.creditLimit,
                id
            );
        } else {
            util.sqlite.update(
                db,
                "INSERT INTO account_credit_card_properties (account_id, credit_limit) VALUES (?, ?)",
                id,
                props.creditLimit
            );
        }
    }

    History getHistory(ulong id) {
        if (!exists(db, "SELECT id FROM account WHERE id = ?", id)) {
            throw new Exception("Account doesn't exist.");
        }
        Optional!History history = findOne(
            db,
            q"SQL
        SELECT * FROM history
        LEFT JOIN account_history ah ON ah.history_id = history.id
        WHERE ah.account_id = ?
SQL",
            r => History(r.peek!ulong(0)),
            id
        );
        if (!history.empty) {
            return history.value;
        }
        // No history exists yet, so add it.
        ulong historyId = doTransaction(db, () {
            util.sqlite.update(db, "INSERT INTO history DEFAULT VALUES");
            ulong historyId = db.lastInsertRowid();
            util.sqlite.update(db, "INSERT INTO account_history (account_id, history_id) VALUES (?, ?)", id, historyId);
            return historyId;
        });
        return History(historyId);
    }

    static Account parseAccount(Row row) {
        return Account(
            row.peek!ulong(0),
            SysTime.fromISOExtString(row.peek!string(1)),
            row.peek!bool(2),
            AccountType.fromId(row.peek!string(3)),
            row.peek!string(4),
            row.peek!string(5),
            Currency.ofCode(row.peek!string(6)),
            row.peek!string(7)
        );
    }

    static AccountCreditCardProperties parseCreditCardProperties(Row row) {
        import std.typecons : Nullable;
        ulong accountId = row.peek!ulong(0);
        Nullable!ulong creditLimit = row.peek!ulong(1);
        return AccountCreditCardProperties(
            accountId,
            creditLimit.isNull ? -1 : creditLimit.get()
        );
    }
}

class SqliteAccountJournalEntryRepository : AccountJournalEntryRepository {
    private Database db;
    this(Database db) {
        this.db = db;
    }

    Optional!AccountJournalEntry findById(ulong id) {
        return util.sqlite.findById(db, "account_journal_entry", &parseEntry, id);
    }

    AccountJournalEntry insert(
        SysTime timestamp,
        ulong accountId,
        ulong transactionId,
        ulong amount,
        AccountJournalEntryType type,
        Currency currency
    ) {
        util.sqlite.update(
            db,
            "INSERT INTO account_journal_entry
            (timestamp, account_id, transaction_id, amount, type, currency)
            VALUES (?, ?, ?, ?, ?, ?)",
            timestamp.toISOExtString(),
            accountId,
            transactionId,
            amount,
            type,
            currency.code
        );
        ulong id = db.lastInsertRowid();
        return findById(id).orElseThrow();
    }

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

    static AccountJournalEntry parseEntry(Row row) {
        string typeStr = row.peek!(string, PeekMode.slice)(5);
        AccountJournalEntryType type;
        if (typeStr == "CREDIT") {
            type = AccountJournalEntryType.CREDIT;
        } else if (typeStr == "DEBIT") {
            type = AccountJournalEntryType.DEBIT;
        } else {
            throw new Exception("Invalid account journal entry type: " ~ typeStr);
        }
        return AccountJournalEntry(
            row.peek!ulong(0),
            SysTime.fromISOExtString(row.peek!string(1)),
            row.peek!ulong(2),
            row.peek!ulong(3),
            row.peek!ulong(4),
            type,
            Currency.ofCode(row.peek!(string, PeekMode.slice)(6))
        );
    }
}
