import json
import os
import random
import re
import sqlite3 as db
import uuid
from logging import INFO
from math import floor
from shutil import copyfile
from typing import Optional, Dict

from passlib.handlers.sha2_crypt import sha256_crypt

import db_setup
import trading_bot
from game import CURRENCY_NAME, logger, DB_NAME
from util import random_chars

DBName = str
connections: Dict[DBName, db.Connection] = {}
current_connection: Optional[db.Connection] = None
current_cursor: Optional[db.Cursor] = None
current_db_name: Optional[DBName] = None
current_user_id: Optional[int] = None


def execute(sql, parameters=()):
    if not re.search(r"(?i)\s*SELECT", sql):
        logger.info(sql, 'sql_query', data=json.dumps(parameters))
    return current_cursor.execute(sql, parameters)


def valid_db_name(name):
    return re.match(r"[a-z0-9.-]{0,20}", name)


def query_save_name():
    while True:
        # save_name = input('Name of the database (You can also enter a new filename here): ')
        save_name = DB_NAME
        if valid_db_name(save_name):
            return save_name
        else:
            print('Must match "[a-z0-9.-]{0,20}"')


def connect(db_name=None, create_if_not_exists=False):
    """
    connects to the database with the given name, if it exists
    if the database does not exist an exception is raised
        (unless create_if_not_exists is true, then the database is created)
    if there is already a connection to this database, that connection is used
    :return: the connection and the connections' cursor
    """
    if db_name is None:
        db_name = query_save_name()
    if not db_name.endswith('.db'):
        db_name += '.db'
    db_name = db_name.lower()
    if not os.path.isfile(db_name) and not create_if_not_exists:
        raise FileNotFoundError('There is no database with this name.')
    creating_new_db = not os.path.isfile(db_name)
    if db_name not in connections:
        try:
            db_connection = db.connect(db_name, check_same_thread=False)
            db_setup.create_functions(db_connection)
            db_setup.set_pragmas(db_connection.cursor())
            # connection.text_factory = lambda x: x.encode('latin-1')
        except db.Error as e:
            print("Database error %s:" % e.args[0])
            raise
        connections[db_name] = db_connection
    global current_connection
    global current_db_name
    global current_cursor
    current_connection = connections[db_name]
    current_cursor = connections[db_name].cursor()
    current_db_name = db_name
    if creating_new_db:
        try:
            if os.path.isfile('/test-db/' + db_name):
                print('Using test database containing fake data')
                copyfile('/test-db/' + db_name, db_name)
            else:
                logger.log('Creating database', INFO, 'database_creation')
                logger.commit()
                setup()
        except Exception:
            if current_connection is not None:
                current_connection.rollback()
            if db_name in connections:
                disconnect(db_name, rollback=True)
            os.remove(db_name)
            current_connection = None
            current_cursor = None
            current_db_name = None
            raise


def disconnect(connection_name, rollback=True):
    global connections
    if connection_name not in connections:
        raise ValueError('Invalid connection')
    if rollback:
        connections[connection_name].rollback()
    else:
        connections[connection_name].commit()
    connections[connection_name].close()
    del connections[connection_name]


def setup():
    db_setup.setup(current_cursor)


def used_key_count():
    connect()

    execute('''
        SELECT COUNT(*) -- rarely executed, no index needed, O(n) query
        FROM keys
        WHERE used_by_user_id IS NOT NULL
        ''')

    return current_cursor.fetchone()[0]


def login(username, password):
    execute('''
                SELECT rowid, password, salt
                FROM users
                WHERE username = ?
                ''', (username,))
    data = current_cursor.fetchone()
    if not data:
        return None
    user_id, hashed_password, salt = data
    # if a ValueError occurs here, then most likely a password that was stored as plain text
    if sha256_crypt.verify(password + salt, hashed_password):
        return new_session(user_id)
    else:
        return None


def register(username, password, game_key):
    salt = str(uuid.uuid4())
    hashed_password = sha256_crypt.using(rounds=100000).encrypt(str(password) + salt)
    connect()
    if username == '':
        return False
    if password == '':
        return False
    execute('''
                INSERT INTO users 
                (username, password, salt)
                VALUES (? , ?, ?)
                ''', (username, hashed_password, salt))
    if game_key != '':
        if valid_key(game_key):
            activate_key(game_key, get_user_id_by_name(username))
    return True


def own(user_id, ownable_name, amount=0):
    if not isinstance(ownable_name, str):
        return AssertionError('A name must be a string.')

    execute('''
                INSERT OR IGNORE INTO ownership (user_id, ownable_id, amount)
                SELECT ?, (SELECT rowid FROM ownables WHERE name = ?), ?
                ''', (user_id, ownable_name, amount))


def send_ownable(from_user_id, to_user_id, ownable_id, amount):
    connect()

    if amount < 0:
        raise AssertionError('Can not send negative amount')

    execute('''
                UPDATE ownership
                SET amount = amount - ?
                WHERE user_id = ?
                AND ownable_id = ?
                ''', (amount, from_user_id, ownable_id,))

    own(to_user_id, ownable_name_by_id(ownable_id))

    execute('''
                UPDATE ownership
                SET amount = amount + ?
                WHERE user_id = ?
                AND ownable_id = ?
                ''', (amount, to_user_id, ownable_id,))
    return True


def valid_key(key):
    connect()

    execute('''
                SELECT key
                FROM keys
                WHERE used_by_user_id IS NULL
                AND key = ?
                ''', (key,))

    if current_cursor.fetchone():
        return True
    else:
        return False


def new_session(user_id):
    connect()

    session_id = str(uuid.uuid4())

    execute('''
                INSERT INTO SESSIONS 
                (user_id, session_id)
                VALUES (? , ?)
                ''', (user_id, session_id))

    return session_id


def save_key(key):
    connect()

    execute('''
                INSERT INTO keys 
                (key)
                VALUES (?)
                ''', (key,))


def drop_old_sessions():
    connect()

    execute(''' -- no need to optimize this very well
                DELETE FROM sessions
                WHERE 
                    (SELECT COUNT(*) as newer
                     FROM sessions s2
                     WHERE user_id = s2.user_id
                     AND rowid < s2.rowid) >= 10
                ''')


def user_exists(username):
    connect()

    execute('''
                SELECT rowid
                FROM users
                WHERE username = ?
                ''', (username,))

    if current_cursor.fetchone():
        return True
    else:
        return False


def get_user_id_by_session_id(session_id):
    connect()

    execute('''
        SELECT users.rowid
        FROM sessions, users
        WHERE sessions.session_id = ?
        AND users.rowid = sessions.user_id
        ''', (session_id,))

    ids = current_cursor.fetchone()
    if not ids:
        return False
    return ids[0]


def get_user_id_by_name(username):
    connect()

    execute('''
        SELECT users.rowid
        FROM users
        WHERE username = ?
        ''', (username,))

    return current_cursor.fetchone()[0]


def get_user_ownership(user_id):
    connect()

    execute('''
        SELECT 
            ownables.name, 
            ownership.amount, 
            COALESCE (
            CASE -- sum score for each of the users ownables
            WHEN ownership.ownable_id = ? THEN 1
            ELSE (SELECT price 
                  FROM transactions
                  WHERE ownable_id = ownership.ownable_id 
                  ORDER BY rowid DESC -- equivalent to ordering by dt
                  LIMIT 1)
            END, 0) AS price, 
            (SELECT MAX("limit") 
             FROM orders, ownership o2
             WHERE o2.rowid = orders.ownership_id
             AND o2.ownable_id = ownership.ownable_id
             AND buy
             AND NOT stop_loss) AS bid, 
            (SELECT MIN("limit") 
             FROM orders, ownership o2
             WHERE o2.rowid = orders.ownership_id
             AND o2.ownable_id = ownership.ownable_id
             AND NOT buy
             AND NOT stop_loss) AS ask
        FROM ownership, ownables
        WHERE user_id = ?
        AND (ownership.amount > 0 OR ownership.ownable_id = ?)
        AND ownership.ownable_id = ownables.rowid
        ORDER BY ownables.rowid ASC
        ''', (currency_id(), user_id, currency_id(),))

    return current_cursor.fetchall()


def activate_key(key, user_id):
    connect()
    execute('''
                UPDATE keys
                SET used_by_user_id = ?
                WHERE used_by_user_id IS NULL
                AND key = ?
                ''', (user_id, key,))

    send_ownable(bank_id(), user_id, currency_id(), 1000)


def bank_id():
    connect()

    execute('''
        SELECT users.rowid
        FROM users
        WHERE username = 'bank'
        ''')

    return current_cursor.fetchone()[0]


def valid_session_id(session_id):
    connect()

    execute('''
                SELECT rowid
                FROM sessions
                WHERE session_id = ?
                ''', (session_id,))

    if current_cursor.fetchone():
        return True
    else:
        return False


def get_user_orders(user_id):
    connect()

    execute('''
        SELECT 
            CASE 
                WHEN orders.buy  THEN 'Buy'
                ELSE 'Sell'
            END,
            ownables.name, 
            (orders.ordered_amount - orders.executed_amount) || '/' || orders.ordered_amount, 
            orders."limit", 
            CASE 
                WHEN orders."limit" IS NULL THEN NULL 
                WHEN orders.stop_loss THEN 'Yes'
                ELSE 'No'
            END, 
            datetime(orders.expiry_dt, 'localtime'),
            orders.rowid
        FROM orders, ownables, ownership
        WHERE ownership.user_id = ?
        AND ownership.ownable_id = ownables.rowid
        AND orders.ownership_id = ownership.rowid
        ORDER BY ownables.name ASC, orders.stop_loss ASC, orders.buy DESC, orders."limit" ASC
        ''', (user_id,))

    return current_cursor.fetchall()


def get_ownable_orders(user_id, ownable_id):
    connect()

    execute('''
        SELECT 
            CASE 
                WHEN ownership.user_id = ? THEN 'X'
                ELSE NULL
            END,
            CASE 
                WHEN orders.buy THEN 'Buy'
                ELSE 'Sell'
            END,
            ownables.name, 
            orders.ordered_amount - orders.executed_amount, 
            orders."limit", 
            datetime(orders.expiry_dt, 'localtime'),
            orders.rowid
        FROM orders, ownables, ownership
        WHERE ownership.ownable_id = ?
        AND ownership.ownable_id = ownables.rowid
        AND orders.ownership_id = ownership.rowid
        AND (orders.stop_loss IS NULL OR NOT orders.stop_loss)
        ORDER BY ownables.name ASC, orders.stop_loss ASC, orders.buy DESC, orders."limit" ASC
        ''', (user_id, ownable_id,))

    return current_cursor.fetchall()


def sell_ordered_amount(user_id, ownable_id):
    connect()

    execute('''
                SELECT COALESCE(SUM(orders.ordered_amount - orders.executed_amount),0)
                FROM orders, ownership
                WHERE ownership.rowid = orders.ownership_id
                AND ownership.user_id = ?
                AND ownership.ownable_id = ?
                AND NOT orders.buy
                ''', (user_id, ownable_id))

    return current_cursor.fetchone()[0]


def available_amount(user_id, ownable_id):
    connect()

    execute('''
                SELECT amount
                FROM ownership
                WHERE user_id = ?
                AND ownable_id = ?
                ''', (user_id, ownable_id))

    return current_cursor.fetchone()[0] - sell_ordered_amount(user_id, ownable_id)


def user_has_at_least_available(amount, user_id, ownable_id):
    connect()

    if not isinstance(amount, float) and not isinstance(amount, int):
        # comparison of float with strings does not work so well in sql
        raise AssertionError()

    execute('''
                SELECT rowid
                FROM ownership
                WHERE user_id = ?
                AND ownable_id = ?
                AND amount - ? >= ?
                ''', (user_id, ownable_id, sell_ordered_amount(user_id, ownable_id), amount))

    if current_cursor.fetchone():
        return True
    else:
        return False


def news():
    connect()

    execute('''
        SELECT dt, title FROM
            (SELECT *, rowid 
            FROM news
            ORDER BY rowid DESC -- equivalent to order by dt
            LIMIT 20) n
        ORDER BY rowid ASC -- equivalent to order by dt
        ''')

    return current_cursor.fetchall()


def ownable_name_exists(name):
    connect()

    execute('''
                SELECT rowid
                FROM ownables
                WHERE name = ?
                ''', (name,))

    if current_cursor.fetchone():
        return True
    else:
        return False


def new_stock(expiry, name=None):
    connect()

    while name is None:
        name = random_chars(6)
        if ownable_name_exists(name):
            name = None

    execute('''
        INSERT INTO ownables(name)
        VALUES (?)
        ''', (name,))

    new_news('A new stock can now be bought: ' + name)
    if random.getrandbits(1):
        new_news('Experts expect the price of ' + name + ' to fall')
    else:
        new_news('Experts expect the price of ' + name + ' to rise')

    amount = random.randrange(100, 10000)
    price = random.randrange(10000, 20000) / amount
    ownable_id = ownable_id_by_name(name)
    own(bank_id(), name, amount)
    bank_order(False,
               ownable_id,
               price,
               amount,
               expiry)
    return name


def ownable_id_by_name(ownable_name):
    connect()

    execute('''
        SELECT rowid
        FROM ownables
        WHERE name = ?
        ''', (ownable_name,))

    return current_cursor.fetchone()[0]


def get_ownership_id(ownable_id, user_id):
    connect()

    execute('''
        SELECT rowid
        FROM ownership
        WHERE ownable_id = ?
        AND user_id = ?
        ''', (ownable_id, user_id,))

    return current_cursor.fetchone()[0]


def currency_id():
    connect()

    execute('''
        SELECT rowid
        FROM ownables
        WHERE name = ?
        ''', (CURRENCY_NAME,))

    return current_cursor.fetchone()[0]


def user_money(user_id):
    connect()

    execute('''
        SELECT amount
        FROM ownership
        WHERE user_id = ?
        AND ownable_id = ?
        ''', (user_id, currency_id()))

    return current_cursor.fetchone()[0]


def delete_order(order_id, new_order_status):
    connect()

    execute('''
        INSERT INTO order_history
        (ownership_id, buy, "limit", ordered_amount, executed_amount, expiry_dt, status, order_id)
        SELECT 
            ownership_id, 
            buy, 
            "limit", 
            ordered_amount, 
            executed_amount, 
            expiry_dt, 
            ?, 
            rowid
        FROM orders
        WHERE rowid = ?
        ''', (new_order_status, order_id,))

    execute('''
        DELETE FROM orders
        WHERE rowid = ?
        ''', (order_id,))


def current_value(ownable_id):
    connect()

    if ownable_id == currency_id():
        return 1

    execute('''SELECT price 
                      FROM transactions
                      WHERE ownable_id = ?
                      ORDER BY rowid DESC -- equivalent to order by dt 
                      LIMIT 1
        ''', (ownable_id,))
    return current_cursor.fetchone()[0]


def execute_orders(ownable_id):
    connect()
    orders_traded = False
    while True:
        # find order to execute
        execute('''
            -- two best orders
            SELECT * FROM (
                SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
                FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
                WHERE buy_order.buy AND NOT sell_order.buy
                AND buyer.rowid = buy_order.ownership_id
                AND seller.rowid = sell_order.ownership_id
                AND buyer.ownable_id = ?
                AND seller.ownable_id = ?
                AND buy_order."limit" IS NULL
                AND sell_order."limit" IS NULL
                ORDER BY buy_order.rowid ASC,
                         sell_order.rowid ASC
                LIMIT 1)
            UNION ALL -- best buy orders
            SELECT * FROM (
                SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
                FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
                WHERE buy_order.buy AND NOT sell_order.buy
                AND buyer.rowid = buy_order.ownership_id
                AND seller.rowid = sell_order.ownership_id
                AND buyer.ownable_id = ?
                AND seller.ownable_id = ?
                AND buy_order."limit" IS NULL
                AND sell_order."limit" IS NOT NULL
                AND NOT sell_order.stop_loss
                ORDER BY sell_order."limit" ASC,
                         buy_order.rowid ASC,
                         sell_order.rowid ASC
                LIMIT 1)
            UNION ALL -- best sell orders
            SELECT * FROM (
                SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
                FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
                WHERE buy_order.buy AND NOT sell_order.buy
                AND buyer.rowid = buy_order.ownership_id
                AND seller.rowid = sell_order.ownership_id
                AND buyer.ownable_id = ?
                AND seller.ownable_id = ?
                AND buy_order."limit" IS NOT NULL
                AND NOT buy_order.stop_loss
                AND sell_order."limit" IS NULL
                ORDER BY buy_order."limit" DESC,
                         buy_order.rowid ASC,
                         sell_order.rowid ASC
                LIMIT 1)
            UNION ALL -- both limit orders
            SELECT * FROM (
                SELECT buy_order.*, sell_order.*, buyer.user_id, seller.user_id, buy_order.rowid, sell_order.rowid
                FROM orders buy_order, orders sell_order, ownership buyer, ownership seller
                WHERE buy_order.buy AND NOT sell_order.buy
                AND buyer.rowid = buy_order.ownership_id
                AND seller.rowid = sell_order.ownership_id
                AND buyer.ownable_id = ?
                AND seller.ownable_id = ?
                AND buy_order."limit" IS NOT NULL
                AND sell_order."limit" IS NOT NULL
                AND sell_order."limit" <= buy_order."limit"
                AND NOT sell_order.stop_loss
                AND NOT buy_order.stop_loss
                ORDER BY buy_order."limit" DESC,
                         sell_order."limit" ASC,
                         buy_order.rowid ASC,
                         sell_order.rowid ASC
                LIMIT 1)
            LIMIT 1
            ''', tuple(ownable_id for _ in range(8)))

        matching_orders = current_cursor.fetchone()
        # return type: (ownership_id,buy,limit,stop_loss,ordered_amount,executed_amount,expiry_dt,
        #               ownership_id,buy,limit,stop_loss,ordered_amount,executed_amount,expiry_dt,
        #               user_id,user_id,rowid,rowid)

        if not matching_orders:
            if not orders_traded:
                break
            # check if the trading bot has any new offers to make
            new_order_was_placed = trading_bot.notify_order_traded(ownable_id)
            if new_order_was_placed:
                orders_traded = False
                continue
            else:
                break

        buy_ownership_id, _, buy_limit, _, buy_order_amount, buy_executed_amount, buy_expiry_dt, \
        sell_ownership_id, _, sell_limit, _, sell_order_amount, sell_executed_amount, sell_expiry_dt, \
        buyer_id, seller_id, buy_order_id, sell_order_id \
            = matching_orders

        if buy_limit is None and sell_limit is None:
            price = current_value(ownable_id)
        elif buy_limit is None:
            price = sell_limit
        elif sell_limit is None:
            price = buy_limit
        else:  # both not NULL
            # the price of the older order is used, just like in the real exchange
            if buy_order_id < sell_order_id:
                price = buy_limit
            else:
                price = sell_limit

        buyer_money = user_money(buyer_id)

        def _my_division(x, y):
            try:
                return floor(x / y)
            except ZeroDivisionError:
                return float('Inf')

        amount = min(buy_order_amount - buy_executed_amount,
                     sell_order_amount - sell_executed_amount,
                     _my_division(buyer_money, price))

        if amount == 0:  # probable because buyer has not enough money
            delete_order(buy_order_id, 'Unable to pay')
            continue

        buy_order_finished = (buy_order_amount - buy_executed_amount - amount <= 0) or (
                buyer_money - amount * price < price)
        sell_order_finished = (sell_order_amount - sell_executed_amount - amount <= 0)

        if price < 0 or amount <= 0:  # price of 0 is possible though unlikely
            return AssertionError()

        # actually execute the order, but the bank does not send or receive anything
        send_ownable(buyer_id, seller_id, currency_id(), price * amount)
        send_ownable(seller_id, buyer_id, ownable_id, amount)

        # update order execution state
        execute('''
            UPDATE orders 
            SET executed_amount = executed_amount + ?
            WHERE rowid = ?
            OR rowid = ?
            ''', (amount, buy_order_id, sell_order_id))

        if buy_order_finished:
            delete_order(buy_order_id, 'Executed')
            orders_traded = True
        if sell_order_finished:
            delete_order(sell_order_id, 'Executed')
            orders_traded = True

        if seller_id != buyer_id:  # prevent showing self-transactions
            execute('''
                INSERT INTO transactions
                (price, ownable_id, amount, buyer_id, seller_id)
                VALUES(?, ?, ?, ?, ?)
                ''', (price, ownable_id, amount, buyer_id, seller_id))

        # trigger stop-loss orders
        if buyer_id != seller_id:
            execute('''
                UPDATE orders
                SET stop_loss = NULL,
                    "limit" = NULL
                WHERE stop_loss IS NOT NULL
                AND stop_loss
                AND ? IN (SELECT ownable_id FROM ownership WHERE rowid = ownership_id)
                AND ((buy AND "limit" < ?) OR (NOT buy AND "limit" > ?))
                ''', (ownable_id, price, price,))


def ownable_id_by_ownership_id(ownership_id):
    connect()

    execute('''
        SELECT ownable_id
        FROM ownership
        WHERE rowid = ?
        ''', (ownership_id,))

    return current_cursor.fetchone()[0]


def ownable_name_by_id(ownable_id):
    connect()

    execute('''
        SELECT name
        FROM ownables
        WHERE rowid = ?
        ''', (ownable_id,))

    return current_cursor.fetchone()[0]


def bank_order(buy, ownable_id, limit, amount, expiry):
    if not limit:
        raise AssertionError('The bank does not give away anything.')
    place_order(buy,
                get_ownership_id(ownable_id, bank_id()),
                limit,
                False,
                amount,
                expiry)
    ownable_name = ownable_name_by_id(ownable_id)
    new_news('External investors are selling ' + ownable_name + ' atm')


def current_db_time():  # might differ from datetime.datetime.now() for time zone reasons
    connect()

    execute('''
        SELECT datetime('now')
        ''')

    return current_cursor.fetchone()[0]


def place_order(buy, ownership_id, limit, stop_loss, amount, expiry):
    connect()
    execute('''
                INSERT INTO orders 
                (buy, ownership_id, "limit", stop_loss, ordered_amount, expiry_dt)
                VALUES (?, ?, ?, ?, ?, ?)
                ''', (buy, ownership_id, limit, stop_loss, amount, expiry))

    execute_orders(ownable_id_by_ownership_id(ownership_id))
    return True


def trades_on(ownable_id, limit):
    connect()

    execute('''
        SELECT datetime(dt,'localtime'), amount, price
        FROM transactions
        WHERE ownable_id = ?
        ORDER BY rowid DESC -- equivalent to order by dt
        LIMIT ?
        ''', (ownable_id, limit,))

    return current_cursor.fetchall()


def trades(user_id, limit):
    connect()
    execute('''
        SELECT 
            (CASE WHEN seller_id = ? THEN 'Sell' ELSE 'Buy' END), 
            (SELECT name FROM ownables WHERE rowid = transactions.ownable_id), 
            amount, 
            price,
            datetime(dt,'localtime')
        FROM transactions
        WHERE seller_id = ? OR buyer_id = ?
        ORDER BY rowid DESC -- equivalent to order by dt
        LIMIT ?
        ''', (user_id, user_id, user_id, limit,))

    return current_cursor.fetchall()


def drop_expired_orders():
    connect()

    execute('''
        SELECT rowid, ownership_id, * FROM orders 
        WHERE expiry_dt < DATETIME('now')
        ''')

    data = current_cursor.fetchall()
    for order in data:
        order_id = order[0]
        delete_order(order_id, 'Expired')

    return data


def generate_keys(count=1):
    # source https://stackoverflow.com/questions/17049308/python-3-3-serial-key-generator-list-problems

    for i in range(count):
        key = '-'.join(random_chars(5) for _ in range(5))
        save_key(key)
        print(key)


def user_has_order_with_id(session_id, order_id):
    connect()

    execute('''
                SELECT orders.rowid
                FROM orders, ownership, sessions
                WHERE orders.rowid = ?
                AND sessions.session_id = ?
                AND sessions.user_id = ownership.user_id
                AND ownership.rowid = orders.ownership_id
                ''', (order_id, session_id,))

    if current_cursor.fetchone():
        return True
    else:
        return False


def leaderboard():
    connect()

    execute('''
        SELECT * 
        FROM ( -- one score for each user
            SELECT 
                username, 
                SUM(CASE -- sum score for each of the users ownables
                    WHEN ownership.ownable_id = ? THEN ownership.amount
                    ELSE ownership.amount * (SELECT price 
                                             FROM transactions
                                             WHERE ownable_id = ownership.ownable_id 
                                             ORDER BY rowid DESC -- equivalent to ordering by dt
                                             LIMIT 1)
                    END
                ) score
            FROM users, ownership
            WHERE ownership.user_id = users.rowid
            AND users.username != 'bank'
            GROUP BY users.rowid
        ) AS scores
        ORDER BY score DESC
        LIMIT 50
        ''', (currency_id(),))

    return current_cursor.fetchall()


def user_wealth(user_id):
    connect()

    execute('''
        SELECT COALESCE(SUM(
            CASE -- sum score for each of the users ownables
            WHEN ownership.ownable_id = ? THEN ownership.amount
            ELSE ownership.amount * (SELECT price 
                                     FROM transactions
                                     WHERE ownable_id = ownership.ownable_id 
                                     ORDER BY rowid DESC -- equivalent to ordering by dt
                                     LIMIT 1)
            END
        ), 0) score
        FROM ownership
        WHERE ownership.user_id = ?
        ''', (currency_id(), user_id,))

    return current_cursor.fetchone()[0]


def change_password(session_id, password, salt):
    connect()

    execute('''
                UPDATE users
                SET password = ?, salt= ?
                WHERE rowid = (SELECT user_id FROM sessions WHERE sessions.session_id = ?)
                ''', (password, salt, session_id,))


def sign_out_user(session_id):
    connect()

    execute('''
        DELETE FROM sessions
        WHERE user_id = (SELECT user_id FROM sessions s2 WHERE s2.session_id = ?)
        ''', (session_id,))


def delete_user(user_id):
    connect()

    execute('''
        DELETE FROM sessions
        WHERE user_id = ?
        ''', (user_id,))

    execute('''
        DELETE FROM orders
        WHERE ownership_id IN (
            SELECT rowid FROM ownership WHERE user_id = ?)
        ''', (user_id,))

    execute('''
        DELETE FROM ownership
        WHERE user_id = ?
        ''', (user_id,))

    execute('''
        DELETE FROM keys
        WHERE used_by_user_id = ?
        ''', (user_id,))

    execute('''
        INSERT INTO news(title)
        VALUES ((SELECT username FROM users WHERE rowid = ?) || ' retired.')
        ''', (user_id,))

    execute('''
        DELETE FROM users
        WHERE rowid = ?
        ''', (user_id,))


def delete_ownable(ownable_id):
    connect()

    execute('''
        DELETE FROM transactions
        WHERE ownable_id = ?
        ''', (ownable_id,))

    execute('''
        DELETE FROM orders
        WHERE ownership_id IN (
            SELECT rowid FROM ownership WHERE ownable_id = ?)
        ''', (ownable_id,))

    execute('''
        DELETE FROM order_history
        WHERE ownership_id IN (
            SELECT rowid FROM ownership WHERE ownable_id = ?)
        ''', (ownable_id,))

    # only delete empty ownerships
    execute('''
        DELETE FROM ownership
        WHERE ownable_id = ?
        AND amount = 0
        ''', (ownable_id,))

    execute('''
        INSERT INTO news(title)
        VALUES ((SELECT name FROM ownables WHERE rowid = ?) || ' can not be traded any more.')
        ''', (ownable_id,))

    execute('''
        DELETE FROM ownables
        WHERE rowid = ?
        ''', (ownable_id,))


def hash_all_users_passwords():
    connect()

    execute('''
        SELECT rowid, password, salt
        FROM users
        ''')

    users = current_cursor.fetchall()

    for user_id, pw, salt in users:
        valid_hash = True
        try:
            sha256_crypt.verify('password' + salt, pw)
        except ValueError:
            valid_hash = False
        if valid_hash:
            raise AssertionError('There is already a hashed password in the database! Be careful what you are doing!')
        pw = sha256_crypt.encrypt(pw + salt)
        execute('''
            UPDATE users
            SET password = ?
            WHERE rowid = ?
            ''', (pw, user_id,))


def new_news(message):
    connect()
    execute('''
        INSERT INTO news(title)
        VALUES (?)
        ''', (message,))


def abs_spread(ownable_id):
    connect()

    execute('''
        SELECT 
            (SELECT MAX("limit") 
             FROM orders, ownership
             WHERE ownership.rowid = orders.ownership_id
             AND ownership.ownable_id = ?
             AND buy
             AND NOT stop_loss) AS bid, 
            (SELECT MIN("limit") 
             FROM orders, ownership
             WHERE ownership.rowid = orders.ownership_id
             AND ownership.ownable_id = ?
             AND NOT buy
             AND NOT stop_loss) AS ask
        ''', (ownable_id, ownable_id,))

    return current_cursor.fetchone()


def ownables():
    connect()

    execute('''
        SELECT name, course,
            (SELECT SUM(amount)
            FROM ownership
            WHERE ownership.ownable_id = ownables_with_course.rowid) market_size
        FROM (SELECT
                name, ownables.rowid,
                CASE WHEN ownables.rowid = ? 
                THEN 1
                ELSE (SELECT price
                      FROM transactions
                      WHERE ownable_id = ownables.rowid
                      ORDER BY rowid DESC -- equivalent to ordering by dt
                      LIMIT 1) END course
             FROM ownables) ownables_with_course
        ''', (currency_id(),))

    data = current_cursor.fetchall()

    for idx in range(len(data)):
        # compute market cap
        row = data[idx]
        if row[1] is None:
            market_cap = None
        elif row[2] is None:
            market_cap = None
        else:
            market_cap = row[1] * row[2]
        data[idx] = (row[0], row[1], market_cap)

    return data


def reset_bank():
    connect()
    execute('''
        DELETE FROM ownership 
        WHERE user_id = ?
        ''', (bank_id(),))


def cleanup():
    global connections
    global current_connection
    global current_cursor
    global current_db_name
    global current_user_id
    for name in connections:
        connections[name].rollback()
        connections[name].close()
    connections = []
    current_connection = None
    current_cursor = None
    current_db_name = None
    current_user_id = None


def ownable_ids():
    connect()

    execute('''
        SELECT rowid FROM ownables
        ''')

    return [ownable_id[0] for ownable_id in current_cursor.fetchall()]


def get_old_orders(user_id, include_executed, include_canceled, limit):
    connect()
    execute('''
        SELECT 
            (CASE WHEN order_history.buy THEN 'Buy' ELSE 'Sell' END),
            ownables.name,
            (order_history.ordered_amount - order_history.executed_amount) || '/' || order_history.ordered_amount,
            order_history."limit",
            order_history.expiry_dt,
            order_history.order_id,
            order_history.status
        FROM order_history, ownership, ownables
        WHERE ownership.user_id = ?
        AND ownership.rowid = order_history.ownership_id
        AND ownables.rowid = ownership.ownable_id
        AND (
             (order_history.status = 'Executed' AND ?)
             OR 
             ((order_history.status = 'Expired' OR order_history.status = 'Canceled') AND ?)
            )
        ORDER BY order_history.rowid DESC -- equivalent to ordering by creation time
        LIMIT ?
        ''', (user_id, include_executed, include_canceled, limit))

    return current_cursor.fetchall()