import re
import uuid
from datetime import timedelta
from math import ceil, floor

from passlib.hash import sha256_crypt

import model
import version
from connection import check_missing_attributes, BadRequest, Forbidden, PreconditionFailed, NotFound
from game import OWNABLE_NAME_PATTERN, BANK_NAME


def login(json_request):
    check_missing_attributes(json_request, ['username', 'password'])
    username = json_request['username']
    password = json_request['password']
    session_id = model.login(username, password)
    if session_id:
        return {'session_id': session_id}
    else:
        return Forbidden('Invalid login data')


def depot(json_request):
    check_missing_attributes(json_request, ['session_id'])
    user_id = model.get_user_id_by_session_id(json_request['session_id'])
    return {'data': model.get_user_ownership(user_id),
            'own_wealth': f'{model.user_wealth(user_id):.2f}',
            'minimum_reserve': model.required_minimum_reserve(user_id) if model.user_has_banking_license(user_id) else None,
            'banking_license': model.user_has_banking_license(user_id)}


def global_variables(_json_request):
    return model.global_control_values()


def register(json_request):
    check_missing_attributes(json_request, ['username', 'password'])
    username = json_request['username'].strip()
    if username == '':
        return BadRequest('Username can not be empty.')
    if model.user_exists(username):
        return BadRequest('User already exists.')
    if model.register(username, json_request['password']):
        return {'message': "successfully registered user"}
    else:
        return BadRequest('Registration not successful')


def order(json_request):
    check_missing_attributes(json_request, ['buy', 'session_id', 'amount', 'ownable', 'time_until_expiration'])
    if not model.ownable_name_exists(json_request['ownable']):
        return BadRequest('This kind of object can not be ordered.')

    buy = json_request['buy']
    sell = not buy
    if not isinstance(buy, bool):
        return BadRequest('`buy` must be a boolean')

    if 'ioc' in json_request:
        ioc = json_request['ioc']
        if not isinstance(ioc, bool):
            raise BadRequest('IOC must be a boolean.')
    else:
        ioc = False

    session_id = json_request['session_id']
    user_id = model.get_user_id_by_session_id(session_id)

    amount = json_request['amount']
    try:
        amount = float(amount)  # so that something like 5e6 also works but only integers
        if amount != round(amount):
            raise ValueError
        amount = round(amount)
    except ValueError:
        return BadRequest('Invalid amount.')
    if amount < 0:
        return BadRequest('You can not order a negative amount.')
    if amount < 1:
        return BadRequest('The minimum order size is 1.')
    ownable_name = json_request['ownable']
    time_until_expiration = float(json_request['time_until_expiration'])
    if time_until_expiration < 0:
        return BadRequest('Invalid expiration time.')

    ownable_id = model.ownable_id_by_name(ownable_name)
    model.own(user_id, ownable_name)
    ownership_id = model.get_ownership_id(ownable_id, user_id)

    try:
        if json_request['limit'] == '':
            limit = None
        elif json_request['limit'] is None:
            limit = None
        else:
            if buy:
                limit = floor(float(json_request['limit']) * 10000) / 10000
            else:
                limit = ceil(float(json_request['limit']) * 10000) / 10000
    except ValueError:  # for example when float fails
        return BadRequest('Invalid limit.')
    except KeyError:  # for example when limit was not specified
        limit = None

    if limit is not None and limit < 0:
        return BadRequest('Limit must not be negative.')

    if 'stop_loss' in json_request:
        if json_request['stop_loss'] == '':
            stop_loss = None
        elif json_request['stop_loss'] is None:
            stop_loss = None
        else:
            stop_loss = json_request['stop_loss']
    else:
        stop_loss = None

    if stop_loss and limit is None:
        return BadRequest('You need to specify a limit for stop-loss orders')

    if ioc and stop_loss:
        raise BadRequest('Stop loss orders can not be IOC orders.')

    if sell:
        if not model.user_has_at_least_available(amount, user_id, ownable_id):
            return BadRequest('You can not sell more than you own (this also takes into account existing sell orders and, if you are a bank, required minimum reserves at the ).')
    try:
        expiry = model.current_db_timestamp() + timedelta(minutes=time_until_expiration).total_seconds()
    except OverflowError:
        return BadRequest('The expiration time is too far in the future.')

    model.place_order(buy, ownership_id, limit, stop_loss, amount, expiry, ioc)
    return {'message': "Order placed."}


def gift(json_request):
    check_missing_attributes(json_request, ['session_id', 'amount', 'object_name', 'username'])
    if not model.ownable_name_exists(json_request['object_name']):
        return BadRequest('This kind of object can not be given away.')
    if json_request['username'] == BANK_NAME or not model.user_exists(json_request['username']):
        return BadRequest('There is no user with this name.')
    try:
        amount = float(json_request['amount'])
    except ValueError:
        return BadRequest('Invalid amount.')
    ownable_id = model.ownable_id_by_name(json_request['object_name'])
    sender_id = model.get_user_id_by_session_id(json_request['session_id'])

    if model.user_available_ownable(sender_id, ownable_id) == 0:
        return BadRequest('You do not own any of these.')
    if not model.user_has_at_least_available(amount, sender_id, ownable_id):
        # for example if you have a 1.23532143213 Kollar and want to give them all away
        amount = model.user_available_ownable(sender_id, ownable_id)

    recipient_id = model.get_user_id_by_name(json_request['username'])

    model.send_ownable(sender_id,
                       recipient_id,
                       ownable_id,
                       amount)

    return {'message': f"Sent {amount} {model.ownable_name_by_id(ownable_id)} to {model.user_name_by_id(recipient_id)}."}


def orders(json_request):
    check_missing_attributes(json_request, ['session_id'])
    data = model.get_user_orders(model.get_user_id_by_session_id(json_request['session_id']))
    return {'data': data}


def loans(json_request):
    check_missing_attributes(json_request, ['session_id'])
    data = model.get_user_loans(model.get_user_id_by_session_id(json_request['session_id']))
    return {'data': data}


def credits(json_request):
    if 'issuer' in json_request:
        issuer_id = model.get_user_id_by_name(json_request['issuer'])
    else:
        issuer_id = None
    if 'only_next_mro_qualified' in json_request:
        only_next_mro_qualified = json_request['only_next_mro_qualified']
        if isinstance(only_next_mro_qualified, str):
            raise BadRequest
    else:
        only_next_mro_qualified = False
    data = model.credits(issuer_id, only_next_mro_qualified)
    return {'data': data}


def orders_on(json_request):
    check_missing_attributes(json_request, ['session_id', 'ownable'])
    if not model.ownable_name_exists(json_request['ownable']):
        return BadRequest('This kind of object can not be ordered.')
    user_id = model.get_user_id_by_session_id(json_request['session_id'])
    ownable_id = model.ownable_id_by_name(json_request['ownable'])
    data = model.get_ownable_orders(user_id, ownable_id)
    return {'data': data}


def old_orders(json_request):
    check_missing_attributes(json_request, ['session_id', 'include_canceled', 'include_executed', 'limit'])
    include_executed = json_request['include_executed']
    include_canceled = json_request['include_canceled']
    user_id = model.get_user_id_by_session_id(json_request['session_id'])
    limit = json_request['limit']
    data = model.get_old_orders(user_id, include_executed, include_canceled, limit)
    return {'data': data}


def cancel_order(json_request):
    check_missing_attributes(json_request, ['session_id', 'order_id'])
    if not model.user_has_order_with_id(json_request['session_id'], json_request['order_id']):
        return BadRequest('You do not have an order with that number.')
    model.delete_order(json_request['order_id'], 'Canceled')
    return {'message': "Successfully deleted order"}


def change_password(json_request):
    check_missing_attributes(json_request, ['session_id', 'password'])
    salt = str(uuid.uuid4())
    hashed_password = sha256_crypt.encrypt(json_request['password'] + salt)
    model.change_password(json_request['session_id'], hashed_password, salt)
    model.sign_out_user(json_request['session_id'])
    return {'message': "Successfully changed password"}


def logout(json_request):
    check_missing_attributes(json_request, ['session_id'])
    model.sign_out_user(json_request['session_id'])
    return {'message': "Successfully logged out"}


def buy_banking_license(json_request):
    check_missing_attributes(json_request, ['session_id'])
    user_id = model.get_user_id_by_session_id(json_request['session_id'])
    if model.user_has_banking_license(user_id):
        raise PreconditionFailed('You already have a banking license.')
    price = model.global_control_value('banking_license_price')
    if model.user_available_money(user_id) < price:
        raise PreconditionFailed('You do not have enough money.')
    model.send_ownable(user_id, model.bank_id(), model.currency_id(), price)
    model.assign_banking_licence(user_id)
    return {'message': "Successfully bought banking license"}


def news(_json_request):
    return {'data': model.news()}

def tender_calendar(_json_request):
    return {'data': model.tender_calendar()}


def tradables(_json_request):
    return {'data': model.ownables()}


def trades(json_request):
    check_missing_attributes(json_request, ['session_id', 'limit'])
    return {'data': model.trades(model.get_user_id_by_session_id(json_request['session_id']), json_request['limit'])}


def trades_on(json_request):
    check_missing_attributes(json_request, ['session_id', 'ownable', 'limit'])
    if not model.ownable_name_exists(json_request['ownable']):
        return BadRequest('This kind of object can not have transactions.')
    return {'data': model.trades_on(model.ownable_id_by_name(json_request['ownable']), json_request['limit'])}


def leaderboard(_json_request):
    return {'data': model.leaderboard()}


def take_out_personal_loan(json_request):
    check_missing_attributes(json_request, ['session_id', 'amount', ])
    amount = json_request['amount']
    if not isinstance(amount, float) or amount <= 0:
        raise BadRequest('Amount must be a number larger than 0')
    user_id = model.get_user_id_by_session_id(json_request['session_id'])
    model.take_out_personal_loan(user_id, amount)
    return {'message': "Successfully took out personal loan"}


def issue_bond(json_request):
    check_missing_attributes(json_request, ['session_id', 'name', 'coupon', 'run_time'])
    user_id = model.get_user_id_by_session_id(json_request['session_id'])

    coupon = json_request['coupon']
    if coupon == 'next_mro':
        coupon = model.next_mro_interest()
    else:
        try:
            coupon = float(coupon)
        except ValueError:
            raise BadRequest('Coupon must be a number.')

    ownable_name = json_request['name']
    if not re.fullmatch(OWNABLE_NAME_PATTERN, ownable_name):
        raise BadRequest('Invalid name.')

    run_time = json_request['run_time']
    if run_time == 'next_mro':
        maturity_dt = model.next_mro_dt()
    else:
        try:
            run_time = int(run_time)
        except ValueError:
            raise BadRequest('Run-time must be a positive integer number.')
        if run_time < 0:
            raise BadRequest('Run-time must be a positive integer number.')

        maturity_dt = model.current_db_timestamp() + 60 * run_time

    model.issue_bond(user_id, ownable_name, coupon, maturity_dt)
    return {'message': "Successfully issued bond"}


def repay_loan(json_request):
    check_missing_attributes(json_request, ['session_id', 'amount', 'loan_id'])
    amount = json_request['amount']
    user_id = model.get_user_id_by_session_id(json_request['session_id'])
    loan_id = json_request['loan_id']
    if amount == 'all':
        amount = model.loan_remaining_amount(loan_id)
    if amount < 0:
        raise BadRequest('You can not repay negative amounts.')
    if model.user_available_money(user_id) < amount:
        if model.user_has_banking_license(user_id):
            raise PreconditionFailed('You do not have enough money. '
                                     'If you are a bank this also takes into account the minimum reserve you need to keep at the central bank.')
        else:
            raise PreconditionFailed('You do not have enough money.')
    if not model.loan_id_exists(loan_id) or model.loan_recipient_id(loan_id) != user_id:
        raise NotFound(f'You do not have a loan with that id.')
    loan_volume = model.loan_remaining_amount(loan_id)
    if loan_volume < amount:
        raise PreconditionFailed(f'You can not repay more than the remaining loan volume of {loan_volume}.')
    model.repay_loan(loan_id, amount, known_user_id=user_id)
    return {'message': "Successfully repayed loan"}


def server_version(_json_request):
    return {'version': version.__version__}


def _before_request(_json_request):
    # update tender calendar
    model.update_tender_calendar()

    for mro_id, expiry, min_interest, mro_dt in model.triggered_mros():
        # pay interest rates for loans until this mro
        model.pay_loan_interest(until=mro_dt)

        # pay interest rates for credits until this mro
        model.pay_bond_interest(until=mro_dt)

        # pay deposit facility for minimum reserves until this mro
        model.pay_deposit_facility(until=mro_dt)

        # handle MROs
        model.mro(mro_id, expiry, min_interest)

    # pay interest rates for loans until current time
    model.pay_loan_interest()

    # pay interest rates for credits until current time
    model.pay_bond_interest()

    # pay deposit facility for minimum reserves until current time
    model.pay_deposit_facility()