#!/usr/bin/env python3
from functools import cmp_to_key

import base64
import hashlib
import html
import http.server
import io
import json
import os
import threading
import time
import sys
from datetime import datetime, timedelta
from decimal import Decimal
from optparse import OptionParser
from typing import Tuple, Union
from twisted.internet import reactor
from urllib.parse import parse_qs

from jmbase import bintohex
from jmbase.support import EXIT_FAILURE
from jmbitcoin import bitcoin_unit_to_power, sat_to_unit, sat_to_unit_power
from jmclient import FidelityBondMixin, get_interest_rate, check_and_start_tor
from jmclient.fidelity_bond import FidelityBondProof

import sybil_attack_calculations as sybil

from jmbase import get_log
log = get_log()

try:
    import matplotlib
except:
    log.warning("matplotlib not found, charts will not be available. "
                "Do `pip install matplotlib` in the joinmarket virtual environment.")

if 'matplotlib' in sys.modules:
    # https://stackoverflow.com/questions/2801882/generating-a-png-with-matplotlib-when-display-is-undefined
    matplotlib.use('Agg')
    import matplotlib.pyplot as plt

from jmclient import jm_single, load_program_config, calc_cj_fee, \
     get_mchannels, add_base_options
from jmdaemon import (OrderbookWatch, MessageChannelCollection,
                      OnionMessageChannel, IRCMessageChannel)
#TODO this is only for base58, find a solution for a client without jmbitcoin
import jmbitcoin as btc
from jmdaemon.protocol import *

bond_exponent = None

#Initial state: allow only SW offer types
sw0offers = list(filter(lambda x: x[0:3] == 'sw0', offername_list))
swoffers = list(filter(lambda x: x[0:3] == 'swa' or x[0:3] == 'swr', offername_list))
filtered_offername_list = sw0offers

rotateObform = '<form action="rotateOb" method="post"><input type="submit" value="Rotate orderbooks"/></form>'
refresh_orderbook_form = '<form action="refreshorderbook" method="post"><input type="submit" value="Check for timed-out counterparties" /></form>'
sorted_units = ('BTC', 'mBTC', '&#956;BTC', 'satoshi')
sorted_rel_units = ('%', '&#8241;', 'ppm')
rel_unit_to_factor = {'%': 100, '&#8241;': 1e4, 'ppm': 1e6}


def calc_depth_data(db, value):
    pass


def get_graph_html(fig):
    imbuf = io.BytesIO()
    fig.savefig(imbuf, format='png')
    b64 = base64.b64encode(imbuf.getvalue()).decode('utf-8')
    return '<img src="data:image/png;base64,' + b64 + '" />'


# callback functions for displaying order data
def do_nothing(arg, order, btc_unit, rel_unit):
    return arg


def ordertype_display(ordertype, order, btc_unit, rel_unit):
    ordertypes = {'sw0absoffer': 'Native SW Absolute Fee', 'sw0reloffer': 'Native SW Relative Fee',
                  'swabsoffer': 'SW Absolute Fee', 'swreloffer': 'SW Relative Fee'}
    return ordertypes[ordertype]


def cjfee_display(cjfee: Union[Decimal, float, int],
                  order: dict,
                  btc_unit: str,
                  rel_unit: str) -> str:
    if order['ordertype'] in ['swabsoffer', 'sw0absoffer']:
        val = sat_to_unit(cjfee, html.unescape(btc_unit))
        if btc_unit == "BTC":
            return "%.8f" % val
        else:
            return str(val)
    elif order['ordertype'] in ['reloffer', 'swreloffer', 'sw0reloffer']:
        return str(Decimal(cjfee) * Decimal(rel_unit_to_factor[rel_unit])) + rel_unit


def order_str(s, order, btc_unit, rel_unit):
    return str(s)


def bond_value_to_str(bond_value: Decimal, btc_unit: str) -> str:
    if btc_unit == "BTC":
        return "%.16f" % bond_value
    elif btc_unit == "mBTC":
        return "%.10f" % bond_value
    else:
        return str(bond_value)


def create_offerbook_table_heading(btc_unit, rel_unit):
    col = '  <th>{1}</th>\n'  # .format(field,label)
    tableheading = '<table class="tftable sortable" border="1">\n <tr>' + ''.join(
            [
                col.format('ordertype', 'Type'),
                col.format('counterparty', 'Counterparty'),
                col.format('oid', 'Order ID'),
                col.format('cjfee', 'Fee'),
                col.format('txfee', 'Miner Fee Contribution / ' + btc_unit),
                col.format('minsize', 'Minimum Size / ' + btc_unit),
                col.format('maxsize', 'Maximum Size / ' + btc_unit),
                col.format('bondvalue', 'Bond value / ' + btc_unit + '<sup>' + bond_exponent + '</sup>')
            ]) + ' </tr>'
    return tableheading

def create_bonds_table_heading(btc_unit):
    tableheading = ('<table class="tftable sortable" border="1"><tr>'
        + '<th>Counterparty</th>'
        + '<th>UTXO</th>'
        + '<th>Bond value / ' + btc_unit + '<sup>' + bond_exponent + '</sup></th>'
        + '<th>Locktime</th>'
        + '<th>Locked coins / ' + btc_unit + '</th>'
        + '<th>Confirmation time</th>'
        + '<th>Signature expiry height</th>'
        + '<th>Redeem script</th>'
        + '</tr>'
    )
    return tableheading

def create_choose_units_form(selected_btc, selected_rel):
    choose_units_form = (
        '<form method="get" action="">' +
        '<select name="btcunit" onchange="this.form.submit();">' +
        ''.join(('<option>' + u + ' </option>' for u in sorted_units)) +
        '</select><select name="relunit" onchange="this.form.submit();">' +
        ''.join(('<option>' + u + ' </option>' for u in sorted_rel_units)) +
        '</select></form>')
    choose_units_form = choose_units_form.replace(
            '<option>' + selected_btc,
            '<option selected="selected">' + selected_btc)
    choose_units_form = choose_units_form.replace(
            '<option>' + selected_rel,
            '<option selected="selected">' + selected_rel)
    return choose_units_form

def get_fidelity_bond_data(taker):
    with taker.dblock:
        fbonds = taker.db.execute("SELECT * FROM fidelitybonds;").fetchall()

    blocks = jm_single().bc_interface.get_current_block_height()
    mediantime = jm_single().bc_interface.get_best_block_median_time()
    interest_rate = get_interest_rate()

    bond_utxo_set = set()
    fidelity_bond_data = []
    bond_outpoint_conf_times = []
    fidelity_bond_values = []
    for fb in fbonds:
        try:
            parsed_bond = FidelityBondProof.parse_and_verify_proof_msg(fb["counterparty"],
                fb["takernick"], fb["proof"])
        except ValueError:
            continue
        bond_utxo_data = FidelityBondMixin.get_validated_timelocked_fidelity_bond_utxo(
            parsed_bond.utxo, parsed_bond.utxo_pub, parsed_bond.locktime, parsed_bond.cert_expiry,
            blocks)
        if bond_utxo_data == None:
            continue
        #check for duplicated utxos i.e. two or more makers using the same UTXO
        # which is obviously not allowed, a fidelity bond must only be usable by one maker nick
        utxo_str = parsed_bond.utxo[0] + b":" + str(parsed_bond.utxo[1]).encode("ascii")
        if utxo_str in bond_utxo_set:
            continue
        bond_utxo_set.add(utxo_str)

        fidelity_bond_data.append((parsed_bond, bond_utxo_data))
        conf_time = jm_single().bc_interface.get_block_time(
            jm_single().bc_interface.get_block_hash(
                blocks - bond_utxo_data["confirms"] + 1
            )
        )
        bond_outpoint_conf_times.append(conf_time)

        bond_value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
            bond_utxo_data["value"],
            conf_time,
            parsed_bond.locktime,
            mediantime,
            interest_rate)
        fidelity_bond_values.append(bond_value)
    return (fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times)

class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler):
    def __init__(self, request, client_address, base_server):
        self.taker = base_server.taker
        self.base_server = base_server
        http.server.SimpleHTTPRequestHandler.__init__(
                self, request, client_address, base_server,
                directory=os.path.dirname(os.path.realpath(__file__)))

    def create_orderbook_obj(self):
        with self.taker.dblock:
            rows = self.taker.db.execute('SELECT * FROM orderbook;').fetchall()
            fbonds = self.taker.db.execute("SELECT * FROM fidelitybonds;").fetchall()

        fidelitybonds = []
        if fbonds and jm_single().bc_interface != None:
            (fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times) =\
                get_fidelity_bond_data(self.taker)
            fidelity_bond_values_dict = dict([(bond_data.maker_nick, bond_value)
                for (bond_data, _), bond_value in zip(fidelity_bond_data, fidelity_bond_values)])
            for ((parsed_bond, bond_utxo_data), fidelity_bond_value, bond_outpoint_conf_time)\
                    in zip(fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times):
                fb = {
                    "counterparty": parsed_bond.maker_nick,
                    "utxo": {"txid": bintohex(parsed_bond.utxo[0]),
                        "vout": parsed_bond.utxo[1]},
                    "bond_value": fidelity_bond_value,
                    "locktime": parsed_bond.locktime,
                    "amount":  bond_utxo_data["value"],
                    "script": bintohex(bond_utxo_data["script"]),
                    "utxo_confirmations": bond_utxo_data["confirms"],
                    "utxo_confirmation_timestamp": bond_outpoint_conf_time,
                    "utxo_pub": bintohex(parsed_bond.utxo_pub),
                    "cert_expiry": parsed_bond.cert_expiry
                }
                fidelitybonds.append(fb)
        else:
            fidelity_bond_values_dict = {}

        offers = []
        for row in rows:
            o = dict(row)
            if 'cjfee' in o:
                if o['ordertype'] == 'swabsoffer'\
                   or o['ordertype'] == 'sw0absoffer':
                    o['cjfee'] = int(o['cjfee'])
                else:
                    o['cjfee'] = str(Decimal(o['cjfee']))
            o["fidelity_bond_value"] = fidelity_bond_values_dict.get(o["counterparty"], 0)
            offers.append(o)

        return {"offers": offers, "fidelitybonds": fidelitybonds}

    def create_depth_chart(self, cj_amount, args=None):
        if 'matplotlib' not in sys.modules:
            return 'matplotlib not installed, charts not available'

        if args is None:
            args = {}
        try:
            self.taker.dblock.acquire(True)
            rows = self.taker.db.execute('SELECT * FROM orderbook;').fetchall()
        finally:
            self.taker.dblock.release()
        sqlorders = [o for o in rows if o["ordertype"] in filtered_offername_list]
        orderfees = sorted([calc_cj_fee(o['ordertype'], o['cjfee'], cj_amount) / 1e8
                            for o in sqlorders
                            if o['minsize'] <= cj_amount <= o[
                                'maxsize']])

        if len(orderfees) == 0:
            return 'No orders at amount ' + str(cj_amount / 1e8)
        fig = plt.figure()
        scale = args.get("scale")
        if (scale is not None) and (scale[0] == "log"):
            orderfees = [float(fee) for fee in orderfees]
            if orderfees[0] > 0:
                ratio = orderfees[-1] / orderfees[0]
                step = ratio ** 0.0333  # 1/30
                bins = [orderfees[0] * (step ** i) for i in range(30)]
            else:
                ratio = orderfees[-1] / 1e-8  # single satoshi placeholder
                step = ratio ** 0.0333  # 1/30
                bins = [1e-8 * (step ** i) for i in range(30)]
                bins[0] = orderfees[0]  # replace placeholder
            plt.xscale('log')
        else:
            bins = 30
        if len(orderfees) == 1:  # these days we have liquidity, but just in case...
            plt.hist(orderfees, bins, rwidth=0.8, range=(0, orderfees[0] * 2))
        else:
            plt.hist(orderfees, bins, rwidth=0.8)
        plt.grid()
        plt.title('CoinJoin Orderbook Depth Chart for amount=' + str(cj_amount /
                                                                     1e8) + 'btc')
        plt.xlabel('CoinJoin Fee / btc')
        plt.ylabel('Frequency')
        return get_graph_html(fig)

    def create_size_histogram(self, args):
        if 'matplotlib' not in sys.modules:
            return 'matplotlib not installed, charts not available'

        try:
            self.taker.dblock.acquire(True)
            rows = self.taker.db.execute('SELECT maxsize, ordertype FROM orderbook;').fetchall()
        finally:
            self.taker.dblock.release()
        rows = [o for o in rows if o["ordertype"] in filtered_offername_list]
        ordersizes = sorted([r['maxsize'] / 1e8 for r in rows])

        fig = plt.figure()
        scale = args.get("scale")
        if (scale is not None) and (scale[0] == "log"):
            ratio = ordersizes[-1] / ordersizes[0]
            step = ratio ** 0.0333  # 1/30
            bins = [ordersizes[0] * (step ** i) for i in range(30)]
        else:
            bins = 30
        plt.hist(ordersizes, bins, histtype='bar', rwidth=0.8)
        if bins != 30:
            fig.axes[0].set_xscale('log')
        plt.grid()
        plt.xlabel('Order sizes / btc')
        plt.ylabel('Frequency')
        return get_graph_html(fig) + ("<br/><a href='?scale=log'>log scale</a>" if
                                      bins == 30 else "<br/><a href='?'>linear</a>")

    def create_fidelity_bond_table(self, btc_unit: str) -> Tuple[str, str]:
        if jm_single().bc_interface == None:
            with self.taker.dblock:
                fbonds = self.taker.db.execute("SELECT * FROM fidelitybonds;").fetchall()
            fidelity_bond_data = []
            for fb in fbonds:
                try:
                    proof = FidelityBondProof.parse_and_verify_proof_msg(
                        fb["counterparty"],
                        fb["takernick"],
                        fb["proof"])
                except ValueError:
                    proof = None
                fidelity_bond_data.append((proof, None))
            fidelity_bond_values = [-1]*len(fidelity_bond_data) #-1 means no data
            bond_outpoint_conf_times = [-1]*len(fidelity_bond_data)
            total_btc_committed_str = "unknown"
        else:
            (fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times) =\
                get_fidelity_bond_data(self.taker)
            total_btc_committed_str = str(sat_to_unit(
                sum([utxo_data["value"] for _, utxo_data in fidelity_bond_data]),
                html.unescape(btc_unit)))

        RETARGET_INTERVAL = 2016
        elem = lambda e: f"<td>{e}</td>"
        bondtable = ""
        for (bond_data, utxo_data), bond_value, conf_time in zip(
                fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times):

            if bond_value == -1 or conf_time == -1 or utxo_data == None:
                bond_value_str = "No data"
                conf_time_str = "No data"
                utxo_value_str = "No data"
            else:
                bond_value_str = bond_value_to_str(sat_to_unit_power(bond_value,
                    2 * bitcoin_unit_to_power(html.unescape(btc_unit))),
                    html.unescape(btc_unit))
                conf_time_str = str(datetime.utcfromtimestamp(0) + timedelta(seconds=conf_time))
                utxo_value_str = sat_to_unit(utxo_data["value"], html.unescape(btc_unit))
            bondtable += ("<tr>"
                + elem(bond_data.maker_nick)
                + elem(bintohex(bond_data.utxo[0]) + ":" + str(bond_data.utxo[1]))
                + elem(bond_value_str)
                + elem((datetime.utcfromtimestamp(0) + timedelta(seconds=bond_data.locktime)).strftime("%Y-%m-%d"))
                + elem(utxo_value_str)
                + elem(conf_time_str)
                + elem(str(bond_data.cert_expiry*RETARGET_INTERVAL))
                + elem(bintohex(btc.mk_freeze_script(bond_data.utxo_pub,
                    bond_data.locktime)))
                + "</tr>"
            )

        heading2 = (str(len(fidelity_bond_data)) + " fidelity bonds found with "
            + total_btc_committed_str + " " + btc_unit
            + " total locked up")
        choose_units_form = (
            '<form method="get" action="">' +
            '<select name="btcunit" onchange="this.form.submit();">' +
            ''.join(('<option>' + u + ' </option>' for u in sorted_units)) +
            '</select></form>')
        choose_units_form = choose_units_form.replace(
                '<option>' + btc_unit,
                '<option selected="selected">' + btc_unit)

        decodescript_tip = ("<br/>Tip: try running the RPC <code>decodescript "
            + "&lt;redeemscript&gt;</code> as proof that the fidelity bond address matches the "
            + "locktime.<br/>Also run <code>gettxout &lt;utxo_txid&gt; &lt;utxo_vout&gt;</code> "
            + "as proof that the fidelity bond UTXO is real.")

        return (heading2,
            choose_units_form + create_bonds_table_heading(btc_unit) + bondtable + "</table>"
            + decodescript_tip)

    def create_sybil_resistance_page(self, btc_unit: str) -> Tuple[str, str]:
        if jm_single().bc_interface == None:
            return "", "Calculations unavailable, requires configured bitcoin node."

        (fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times) =\
            get_fidelity_bond_data(self.taker)

        choose_units_form = (
            '<form method="get" action="">' +
            '<select name="btcunit" onchange="this.form.submit();">' +
            ''.join(('<option>' + u + ' </option>' for u in sorted_units)) +
            '</select></form>')
        choose_units_form = choose_units_form.replace(
                '<option>' + btc_unit,
                '<option selected="selected">' + btc_unit)
        mainbody = choose_units_form

        honest_weight = sum(fidelity_bond_values)
        mainbody += ("Assuming the makers in the offerbook right now are not sybil attackers, "
            + "how much would a sybil attacker starting now have to sacrifice to succeed in their"
            + " attack with 95% probability. Honest weight="
            + str(sat_to_unit_power(honest_weight, 2 * bitcoin_unit_to_power(html.unescape(btc_unit)))) + " " + btc_unit
            + "<sup>" + bond_exponent + "</sup><br/>Also assumes that takers "
            + "are not price-sensitive and that their max "
            + "coinjoin fee is configured high enough that they dont exclude any makers.")
        heading2 = "Sybil attacks from external enemies."

        mainbody += ('<table class="tftable" border="1"><tr>'
            + '<th>Maker count</th>'
            + '<th>6month locked coins / ' + btc_unit + '</th>'
            + '<th>1y locked coins / ' + btc_unit + '</th>'
            + '<th>2y locked coins / ' + btc_unit + '</th>'
            + '<th>5y locked coins / ' + btc_unit + '</th>'
            + '<th>10y locked coins / ' + btc_unit + '</th>'
            + '<th>Required burned coins / ' + btc_unit + '</th>'
            + '</tr>'
        )

        timelocks = [0.5, 1.0, 2.0, 5.0, 10.0, None]
        interest_rate = get_interest_rate()
        for makercount, unit_success_sybil_weight in sybil.successful_attack_95pc_sybil_weight.items():
            success_sybil_weight = unit_success_sybil_weight * honest_weight
            row = "<tr><td>" + str(makercount) + "</td>"
            for timelock in timelocks:
                if timelock != None:
                    coins_per_sybil = sybil.weight_to_locked_coins(success_sybil_weight,
                        interest_rate, timelock)
                else:
                    coins_per_sybil = sybil.weight_to_burned_coins(success_sybil_weight)
                row += ("<td>" + str(sat_to_unit(coins_per_sybil * makercount, html.unescape(btc_unit)))
                    + "</td>")
            row += "</tr>"
            mainbody += row
        mainbody += "</table>"

        mainbody += ("<h2>Sybil attacks from enemies within</h2>Assume a sybil attack is ongoing"
            + " right now and that the counterparties with the most valuable fidelity bonds are "
            + " actually controlled by the same entity. Then, what is the probability of a "
            + " successful sybil attack for a given makercount, and what is the fidelity bond "
            + " value being foregone by not putting all bitcoins into just one maker.")
        mainbody += ('<table class="tftable" border="1"><tr>'
            + '<th>Maker count</th>'
            + '<th>Success probability</th>'
            + '<th>Foregone value / ' + btc_unit + '<sup>' + bond_exponent + '</sup></th>'
            + '</tr>'
        )

        #limited because calculation is slow, so this avoids server being too slow to respond
        MAX_MAKER_COUNT_INTERNAL = 10
        weights = sorted(fidelity_bond_values)[::-1]
        for makercount in range(1, MAX_MAKER_COUNT_INTERNAL+1):
            makercount_str = (str(makercount) + " - " + str(MAX_MAKER_COUNT_INTERNAL)
                if makercount == len(fidelity_bond_data) and len(fidelity_bond_data) !=
                MAX_MAKER_COUNT_INTERNAL else str(makercount))
            success_prob = sybil.calculate_top_makers_sybil_attack_success_probability(weights,
                makercount)
            total_sybil_weight = sum(weights[:makercount])
            sacrificed_values = [sybil.weight_to_burned_coins(w) for w in weights[:makercount]]
            foregone_value = (sybil.coins_burned_to_weight(sum(sacrificed_values))
                - total_sybil_weight)
            mainbody += ("<tr><td>" + makercount_str + "</td><td>" + str(round(success_prob * 100.0, 5))
                + "%</td><td>" + bond_value_to_str(sat_to_unit_power(
                    foregone_value, 2 * bitcoin_unit_to_power(
                        html.unescape(btc_unit))), html.unescape(btc_unit))
                + "</td></tr>")
            if makercount == len(weights):
                break
        mainbody += "</table>"

        return heading2, mainbody

    def create_orderbook_table(self, btc_unit: str, rel_unit: str) -> Tuple[int, str]:
        result = ''
        try:
            self.taker.dblock.acquire(True)
            rows = self.taker.db.execute('SELECT * FROM orderbook;').fetchall()
        finally:
            self.taker.dblock.release()
        if not rows:
            return 0, result
        rows = [o for o in rows if o["ordertype"] in filtered_offername_list]

        if jm_single().bc_interface == None:
            for row in rows:
                row["bondvalue"] = "No data"
        else:
            blocks = jm_single().bc_interface.get_current_block_height()
            mediantime = jm_single().bc_interface.get_best_block_median_time()
            interest_rate = get_interest_rate()
            for row in rows:
                with self.taker.dblock:
                    fbond_data = self.taker.db.execute(
                        "SELECT * FROM fidelitybonds WHERE counterparty=?;", (row["counterparty"],)
                    ).fetchall()
                if len(fbond_data) == 0:
                    row["bondvalue"] = "0"
                    continue
                else:
                    try:
                        parsed_bond = FidelityBondProof.parse_and_verify_proof_msg(
                            fbond_data[0]["counterparty"],
                            fbond_data[0]["takernick"],
                            fbond_data[0]["proof"]
                        )
                    except ValueError:
                        row["bondvalue"] = "0"
                        continue
                    utxo_data = FidelityBondMixin.get_validated_timelocked_fidelity_bond_utxo(
                        parsed_bond.utxo, parsed_bond.utxo_pub, parsed_bond.locktime,
                        parsed_bond.cert_expiry, blocks)
                    if utxo_data == None:
                        row["bondvalue"] = "0"
                        continue
                    bond_value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
                        utxo_data["value"],
                        jm_single().bc_interface.get_block_time(
                            jm_single().bc_interface.get_block_hash(
                                blocks - utxo_data["confirms"] + 1
                            )
                        ),
                        parsed_bond.locktime,
                        mediantime,
                        interest_rate)
                    row["bondvalue"] = bond_value_to_str(sat_to_unit_power(
                        bond_value,
                        2 * bitcoin_unit_to_power(html.unescape(btc_unit))),
                        html.unescape(btc_unit))

        def _okd_satoshi_to_unit(sat, order, btc_unit, rel_unit):
            val = sat_to_unit(sat, html.unescape(btc_unit))
            if btc_unit == "BTC":
                return "%.8f" % val
            else:
                return str(val)

        order_keys_display = (('ordertype', ordertype_display),
                              ('counterparty', do_nothing),
                              ('oid', order_str),
                              ('cjfee', cjfee_display),
                              ('txfee', _okd_satoshi_to_unit),
                              ('minsize', _okd_satoshi_to_unit),
                              ('maxsize', _okd_satoshi_to_unit),
                              ('bondvalue', do_nothing))

        def _cmp(x, y):
            if x < y:
                return -1
            elif x > y:
                return 1
            else:
                return 0

        # somewhat complex sorting to sort by cjfee but with swabsoffers on top
        def orderby_cmp(x, y):
            if x['ordertype'] == y['ordertype']:
                return _cmp(Decimal(x['cjfee']), Decimal(y['cjfee']))
            return _cmp(offername_list.index(x['ordertype']),
                       offername_list.index(y['ordertype']))

        for o in sorted(rows, key=cmp_to_key(orderby_cmp)):
            result += ' <tr>\n'
            for key, displayer in order_keys_display:
                result += '  <td>' + str(displayer(o[key], o, btc_unit,
                                               rel_unit)) + '</td>\n'
            result += ' </tr>\n'
        return len(rows), result

    def get_counterparty_count(self):
        try:
            self.taker.dblock.acquire(True)
            counterparties = self.taker.db.execute(
                'SELECT DISTINCT counterparty FROM orderbook WHERE ordertype=? OR ordertype=?;',
                filtered_offername_list).fetchall()
        finally:
            self.taker.dblock.release()
        return str(len(counterparties))

    def do_GET(self):
        # http.server.SimpleHTTPRequestHandler.do_GET(self)
        # print('httpd received ' + self.path + ' request')
        self.path, query = self.path.split('?', 1) if '?' in self.path else (
            self.path, '')
        args = parse_qs(query)
        pages = ['/', '/fidelitybonds', '/ordersize', '/depth', '/sybilresistance',
            '/orderbook.json']
        static_files = {'/vendor/sorttable.js', '/vendor/bootstrap.min.css', '/vendor/jquery-3.5.1.slim.min.js'}
        if self.path in static_files or self.path not in pages:
            return super().do_GET()
        fd = open(os.path.join(os.path.dirname(os.path.realpath(__file__)),
            'orderbook.html'), 'r')
        orderbook_fmt = fd.read()
        fd.close()
        alert_msg = ''
        if jm_single().joinmarket_alert[0]:
            alert_msg = '<br />JoinMarket Alert Message:<br />' + \
                        jm_single().joinmarket_alert[0]
        if self.path == '/':
            btc_unit = args['btcunit'][
                0] if 'btcunit' in args else sorted_units[0]
            rel_unit = args['relunit'][
                0] if 'relunit' in args else sorted_rel_units[0]
            if btc_unit not in sorted_units:
                btc_unit = sorted_units[0]
            if rel_unit not in sorted_rel_units:
                rel_unit = sorted_rel_units[0]
            ordercount, ordertable = self.create_orderbook_table(
                    btc_unit, rel_unit)
            choose_units_form = create_choose_units_form(btc_unit, rel_unit)
            table_heading = create_offerbook_table_heading(btc_unit, rel_unit)
            replacements = {
                'PAGETITLE': 'JoinMarket Browser Interface',
                'MAINHEADING': 'JoinMarket Orderbook',
                'SECONDHEADING':
                    (str(ordercount) + ' orders found by ' +
                     self.get_counterparty_count() + ' counterparties' + alert_msg),
                'MAINBODY': (
                    rotateObform + refresh_orderbook_form + choose_units_form +
                    table_heading + ordertable + '</table>\n')
            }
        elif self.path == '/fidelitybonds':
            btc_unit = args['btcunit'][0] if 'btcunit' in args else sorted_units[0]
            if btc_unit not in sorted_units:
                btc_unit = sorted_units[0]
            heading2, mainbody = self.create_fidelity_bond_table(btc_unit)

            replacements = {
                'PAGETITLE': 'JoinMarket Browser Interface',
                'MAINHEADING': 'Fidelity Bonds',
                'SECONDHEADING': heading2,
                'MAINBODY': mainbody
            }
        elif self.path == '/ordersize':
            replacements = {
                'PAGETITLE': 'JoinMarket Browser Interface',
                'MAINHEADING': 'Order Sizes',
                'SECONDHEADING': 'Order Size Histogram' + alert_msg,
                'MAINBODY': self.create_size_histogram(args)
            }
        elif self.path.startswith('/depth'):
            # if self.path[6] == '?':
            #	quantity =
            cj_amounts = [10 ** cja for cja in range(4, 12, 1)]
            mainbody = [self.create_depth_chart(cja, args) \
                        for cja in cj_amounts] + \
                       ["<br/><a href='?'>linear</a>" if args.get("scale") \
                            else "<br/><a href='?scale=log'>log scale</a>"]
            replacements = {
                'PAGETITLE': 'JoinMarket Browser Interface',
                'MAINHEADING': 'Depth Chart',
                'SECONDHEADING': 'Orderbook Depth' + alert_msg,
                'MAINBODY': '<br />'.join(mainbody)
            }
        elif self.path == '/sybilresistance':
            btc_unit = args['btcunit'][0] if 'btcunit' in args else sorted_units[0]
            if btc_unit not in sorted_units:
                btc_unit = sorted_units[0]
            heading2, mainbody = self.create_sybil_resistance_page(btc_unit)
            replacements = {
                'PAGETITLE': 'JoinMarket Browser Interface',
                'MAINHEADING': 'Resistance to Sybil Attacks from Fidelity Bonds',
                'SECONDHEADING': heading2,
                'MAINBODY': mainbody
            }
        elif self.path == '/orderbook.json':
            replacements = {}
            orderbook_fmt = json.dumps(self.create_orderbook_obj())
        orderbook_page = orderbook_fmt
        for key, rep in replacements.items():
            orderbook_page = orderbook_page.replace(key, rep)
        self.send_response(200)
        if self.path.endswith('.json'):
            self.send_header('Content-Type', 'application/json')
        else:
            self.send_header('Content-Type', 'text/html')
        self.send_header('Content-Length', len(orderbook_page))
        self.end_headers()
        self.wfile.write(orderbook_page.encode('utf-8'))

    def get_url_base(self) -> str:
        # This is to handle the case where the server is behind a reverse proxy
        # and base path may not be /.
        # First we get HTTP or HTTPS protocol from Origin header and then use
        # Host header to get the base path.
        # Will work with nginx config like this:
        # location /ob-watcher {
        #     rewrite /ob-watcher/(.*) /$1 break;
        #     proxy_pass http://localhost:62601;
        #     proxy_set_header Host $host/ob-watcher;
        # }
        is_https = self.headers.get('Origin', '').startswith('https://')
        host = self.headers.get('Host', '')
        return 'https://' + host if is_https else 'http://' + host

    def do_POST(self):
        global filtered_offername_list
        pages = ['/refreshorderbook', '/rotateOb']
        if self.path not in pages:
            return
        if self.path == '/refreshorderbook':
            with self.taker.dblock:
                self.taker.db.execute("DELETE FROM orderbook;")
                self.taker.db.execute("DELETE FROM fidelitybonds;")
            self.taker.msgchan.request_orderbook()
            time.sleep(5)
            self.send_response(302)
            self.send_header('Location', self.get_url_base() + '/')
            self.end_headers()
        elif self.path == '/rotateOb':
            if filtered_offername_list == sw0offers:
                log.debug('Showing nested segwit orderbook')
                filtered_offername_list = swoffers
            elif filtered_offername_list == swoffers:
                log.debug('Showing native segwit orderbook')
                filtered_offername_list = sw0offers
            self.send_response(302)
            self.send_header('Location', self.get_url_base() + '/')
            self.end_headers()

class HTTPDThread(threading.Thread):
    def __init__(self, taker, hostport):
        threading.Thread.__init__(self, name='HTTPDThread')
        self.daemon = True
        self.taker = taker
        self.hostport = hostport

    def run(self):
        # hostport = ('localhost', 62601)
        try:
            httpd = http.server.HTTPServer(self.hostport,
                                          OrderbookPageRequestHeader)
        except Exception as e:
            print("Failed to start HTTP server: " + str(e))
            os._exit(EXIT_FAILURE)
        httpd.taker = self.taker
        print('\nstarted http server, visit http://{0}:{1}/\n'.format(
                *self.hostport))
        httpd.serve_forever()


class ObBasic(OrderbookWatch):
    """Dummy orderbook watch class
    with hooks for triggering orderbook request"""
    def __init__(self, msgchan, hostport):
        self.hostport = hostport
        self.set_msgchan(msgchan)
        # in client-server, this is passed by client
        # in INIT message. Here, we have no Joinmarket client,
        # but we have access to the client config in this script:
        self.dust_threshold = jm_single().DUST_THRESHOLD

    def on_welcome(self):
        """TODO: It will probably be a bit
        simpler, and more consistent, to use
        a twisted http server here instead
        of a thread."""
        HTTPDThread(self, self.hostport).start()
        self.request_orderbook()

    def request_orderbook(self):
        self.msgchan.request_orderbook()


"""An override for MessageChannel classes,
to allow receipt of privmsgs without the
verification hooks in client-daemon communication."""
def on_privmsg(inst, nick, message):
    if len(message) < 2:
        return

    if message[0] != COMMAND_PREFIX:
        log.debug('message not a cmd')
        return
    cmd_string = message[1:].split(' ')[0]
    if cmd_string not in offername_list:
        log.debug('non-offer ignored')
        return
    #Ignore sigs (TODO better to include check)
    sig = message[1:].split(' ')[-2:]
    #reconstruct original message without cmd pref
    rawmessage = ' '.join(message[1:].split(' ')[:-2])
    for command in rawmessage.split(COMMAND_PREFIX):
        _chunks = command.split(" ")
        try:
            inst.check_for_orders(nick, _chunks)
            inst.check_for_fidelity_bond(nick, _chunks)
        except:
            pass

def get_dummy_nick():
    """In Joinmarket-CS nick creation is negotiated
    between client and server/daemon so as to allow
    client to sign for messages; here we only ever publish
    an orderbook request, so no such need, but for better
    privacy, a conformant nick is created based on a random
    pseudo-pubkey."""
    nick_pkh_raw = hashlib.sha256(os.urandom(10)).digest()[:NICK_HASH_LENGTH]
    nick_pkh = btc.base58.encode(nick_pkh_raw)
    #right pad to maximum possible; b58 is not fixed length.
    #Use 'O' as one of the 4 not included chars in base58.
    nick_pkh += 'O' * (NICK_MAX_ENCODED - len(nick_pkh))
    #The constructed length will be 1 + 1 + NICK_MAX_ENCODED
    nick = JOINMARKET_NICK_HEADER + str(JM_VERSION) + nick_pkh
    jm_single().nickname = nick
    return nick

def main():
    global bond_exponent
    parser = OptionParser(
            usage='usage: %prog [options]',
            description='Runs a webservice which shows the orderbook.')
    add_base_options(parser)
    parser.add_option('-H',
                      '--host',
                      action='store',
                      type='string',
                      dest='host',
                      default='localhost',
                      help='hostname or IP to bind to, default=localhost')
    parser.add_option('-p',
                      '--port',
                      action='store',
                      type='int',
                      dest='port',
                      help='port to listen on, default=62601',
                      default=62601)
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    # needed to display notional units of FB valuation
    bond_exponent = jm_single().config.get("POLICY", "bond_value_exponent")
    try:
        float(bond_exponent)
    except ValueError:
        log.error("Invalid entry for bond_value_exponent, should be decimal "
                  "number: {}".format(bond_exponent))
        sys.exit(EXIT_FAILURE)
    check_and_start_tor()
    hostport = (options.host, options.port)
    mcs = []
    chan_configs = get_mchannels(mode="PASSIVE")
    for c in chan_configs:
        if "type" in c and c["type"] == "onion":
            mcs.append(OnionMessageChannel(c))
        else:
            # default is IRC; TODO allow others
            mcs.append(IRCMessageChannel(c))
    IRCMessageChannel.on_privmsg = on_privmsg
    OnionMessageChannel.on_privmsg = on_privmsg
    mcc = MessageChannelCollection(mcs)
    mcc.set_nick(get_dummy_nick())
    taker = ObBasic(mcc, hostport)
    log.info("Starting ob-watcher")
    mcc.run()



if __name__ == "__main__":
    main()
    reactor.run()
    print('done')
