#! /usr/bin/python3

"""
Datastreams are identified by the address that publishes them, and referenced
in transaction outputs.

For CFD leverage, 1x = 5040, 2x = 10080, etc.: 5040 is a superior highly
composite number and a colossally abundant number, and has 1-10, 12 as factors.

All wagers are in XBJ.

Expiring a bet match doesn’t re‐open the constituent bets. (So all bets may be ‘filled’.)
"""

import struct
import decimal
D = decimal.Decimal
import time
import logging

from . import (util, config, worldcoin, exceptions, util)

FORMAT = '>HIQQdII'
LENGTH = 2 + 4 + 8 + 8 + 8 + 4 + 4
ID = 40

def cancel_bet (db, bet, status, block_index):
    cursor = db.cursor()

    # Update status of bet.
    bindings = {
        'status': status,
        'tx_hash': bet['tx_hash']
    }
    sql='update bets set status = :status where tx_hash = :tx_hash'
    cursor.execute(sql, bindings)
    util.message(db, block_index, 'update', 'bets', bindings)

    util.credit(db, block_index, bet['source'], config.XBJ, bet['wager_remaining'], action='recredit wager remaining', event=bet['tx_hash'])

    cursor = db.cursor()

def cancel_bet_match (db, bet_match, status, block_index):
    # Does not re‐open, re‐fill, etc. constituent bets.

    cursor = db.cursor()

    # Recredit tx0 address.
    util.credit(db, block_index, bet_match['tx0_address'], config.XBJ,
                bet_match['forward_quantity'], action='recredit forward quantity', event=bet_match['id'])

    # Recredit tx1 address.
    util.credit(db, block_index, bet_match['tx1_address'], config.XBJ,
                bet_match['backward_quantity'], action='recredit backward quantity', event=bet_match['id'])

    # Update status of bet match.
    bindings = {
        'status': status,
        'bet_match_id': bet_match['id']
    }
    sql='update bet_matches set status = :status where id = :bet_match_id'
    cursor.execute(sql, bindings)
    util.message(db, block_index, 'update', 'bet_matches', bindings)

    cursor.close()


def get_fee_fraction (db, feed_address):
    '''Get fee fraction from the last broadcast from the feed_address address.
    '''
    cursor = db.cursor()
    broadcasts = list(cursor.execute('''SELECT * FROM broadcasts WHERE (status = ? AND source = ?) ORDER BY tx_index ASC''', ('valid', feed_address)))
    cursor.close()
    if broadcasts:
        last_broadcast = broadcasts[-1]
        fee_fraction_int = last_broadcast['fee_fraction_int']
        if fee_fraction_int: return fee_fraction_int / 1e8
        else: return 0
    else:
        return 0

def validate (db, source, feed_address, bet_type, deadline, wager_quantity,
              counterwager_quantity, target_value, leverage, expiration, block_index):
    problems = []

    # Look at feed to be bet on.
    cursor = db.cursor()
    broadcasts = list(cursor.execute('''SELECT * FROM broadcasts WHERE (status = ? AND source = ?) ORDER BY tx_index ASC''', ('valid', feed_address)))
    cursor.close()
    if not broadcasts:
        problems.append('feed doesn’t exist')
    elif not broadcasts[-1]['text']:
        problems.append('feed is locked')
    elif broadcasts[-1]['timestamp'] >= deadline:
        problems.append('deadline in that feed’s past')

    if not bet_type in (0, 1, 2, 3):
        problems.append('unknown bet type')

    # Valid leverage level?
    if leverage != 5040 and bet_type in (2,3):   # Equal, NotEqual
        problems.append('leverage used with Equal or NotEqual')
    if leverage < 5040 and not bet_type in (0,1):   # BullCFD, BearCFD (fractional leverage makes sense precisely with CFDs)
        problems.append('leverage level too low')

    if bet_type in (0,1):   # BullCFD, BearCFD
        if block_index >= 312350:   # Protocol change.
            problems.append('CFDs temporarily disabled')

    if not isinstance(wager_quantity, int):
        problems.append('wager_quantity must be in satoshis')
        return problems
    if not isinstance(counterwager_quantity, int):
        problems.append('counterwager_quantity must be in satoshis')
        return problems
    if not isinstance(expiration, int):
        problems.append('expiration must be expressed as an integer block delta')
        return problems

    if wager_quantity <= 0: problems.append('non‐positive wager')
    if counterwager_quantity <= 0: problems.append('non‐positive counterwager')
    if target_value < 0: problems.append('negative target value')
    if deadline < 0: problems.append('negative deadline')
    if expiration < 0: problems.append('negative expiration')
    if expiration == 0 and not (block_index >= 317500 or config.TESTNET):   # Protocol change.
        problems.append('zero expiration')

    if target_value and bet_type in (0,1):   # BullCFD, BearCFD
        problems.
append('CFDs have no target value')

    if expiration > config.MAX_EXPIRATION:
        problems.append('expiration overflow')

    # For SQLite3
    if wager_quantity > config.MAX_INT or counterwager_quantity > config.MAX_INT or bet_type >
 config.MAX_INT or deadline > config.MAX_INT or leverage > config.MAX_INT:
        problems.append('integer overflow')

    return problems

def compose (db, source, feed_address, bet_type, deadline, wager_quantity,
            counterwager_quantity, target_value, leverage, expiration):

    problems = validate(db, source, feed_address, bet_type, deadline, wager_quantity,
                        counterwager_quantity, target_value, leverage, expiration, util.last_block(db)['block_index'])
    if util.date_passed(deadline):
        problems.append('deadline passed')
    if problems: raise exceptions.BetError(problems)

    data = struct.pack(config.TXTYPE_FORMAT, ID)
    data += struct.pack(FORMAT, bet_type, deadline,
                        wager_quantity, counterwager_quantity, target_value,
                        leverage, expiration)
    return (source, [(feed_address, None)], data)

def parse (db, tx, message):
    bet_parse_cursor = db.cursor()

    # Unpack message.
    try:
        if len(message) != LENGTH:
            raise exceptions.UnpackError
        (bet_type, deadline, wager_quantity,
         counterwager_quantity, target_value, leverage,
         expiration) = struct.unpack(FORMAT, message)
        status = 'open'
    except (exceptions.UnpackError, struct.error):
        (bet_type, deadline, wager_quantity,
         counterwager_quantity, target_value, leverage,
         expiration, fee_fraction_int) = 0, 0, 0, 0, 0, 0, 0, 0
        status = 'invalid: could not unpack'

    odds, fee_fraction = 0, 0
    feed_address = tx['destination']
    if status == 'open':
        try:
            odds = util.price(wager_quantity, counterwager_quantity, tx['block_index'])
        except ZeroDivisionError:
            odds = 0

        fee_fraction = get_fee_fraction(db, feed_address)

        # Overbet
        bet_parse_cursor.execute('''SELECT * FROM balances \
                                    WHERE (address = ? AND asset = ?)''', (tx['source'], config.XBJ))
        balances = list(bet_parse_cursor)
        if not balances:
            wager_quantity = 0
        else:
            balance = balances[0]['quantity']
            if balance < wager_quantity:
                wager_quantity = balance
                counterwager_quantity = int(util.price(wager_quantity, odds, tx['block_index']))

        problems = validate(db, tx['source'], feed_address, bet_type, deadline, wager_quantity,
                            counterwager_quantity, target_value, leverage, expiration, tx['block_index'])
        if problems: status = 'invalid: ' + '; '.join(problems)

    # Debit quantity wagered. (Escrow.)
    if status == 'open':
        util.debit(db, tx['block_index'], tx['source'], config.XBJ, wager_quantity, action='bet', event=tx['tx_hash'])

    # Add parsed transaction to message-type–specific table.
    bindings = {
        'tx_index': tx['tx_index'],
        'tx_hash': tx['tx_hash'],
        'block_index': tx['block_index'],
        'source': tx['source'],
        'feed_address': feed_address,
        'bet_type': bet_type,
        'deadline': deadline,
        'wager_quantity': wager_quantity,
        'wager_rema