#!/usr/bin/python3

import sys
import locale
import requests
import json
import math
import time
import datetime, time
import codecs, grpc, os
from pathlib import Path

from blitzpy import RaspiBlitzConfig

from lndlibs import rpc_pb2 as lnrpc
from lndlibs import rpc_pb2_grpc as rpcstub

# display config script info
if len(sys.argv) <= 1 or sys.argv[1] == "-h" or sys.argv[1] == "help":
    print("# manage ip2tor subscriptions for raspiblitz")
    print("# blitz.ip2tor.py menu")
    sys.exit(1)

# basic settings
cfg = RaspiBlitzConfig()
if Path("/mnt/hdd/raspiblitz.conf").is_file():
    print("# blitz.ip2tor.py")
    cfg.reload()
    DEFAULT_SHOPURL="shopdeu2vdhazvmllyfagdcvlpflzdyt5gwftmn4hjj3zw2oyelksaid.onion"
    LND_IP="127.0.0.1"
    LND_ADMIN_MACAROON_PATH="/mnt/hdd/app-data/lnd/data/chain/{0}/{1}net/admin.macaroon"
    LND_TLS_PATH="/mnt/hdd/app-data/lnd/tls.cert"
else:
    print("# blitz.ip2tor.py (development env)")
    cfg.run_behind_tor = False
    DEFAULT_SHOPURL="shop.ip2t.org"
    LND_IP="192.168.178.95"
    LND_ADMIN_MACAROON_PATH="/Users/rotzoll/Downloads/RaspiBlitzCredentials/admin.macaroon"
    LND_TLS_PATH="/Users/rotzoll/Downloads/RaspiBlitzCredentials/tls.cert"

def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)

def parseDate(datestr):
    return datetime.datetime.strptime(datestr,"%Y-%m-%dT%H:%M:%S.%fZ")

def secondsLeft(dateObj):
    return round((dateObj - datetime.datetime.utcnow()).total_seconds())

# takes a shopurl from user input and turns it into the format needed
# also makes sure that .onion addresses run just with http not https
def normalizeShopUrl(shopurlUserInput):

    # basic checks and formats
    if len(shopurlUserInput) < 3: return
    shopurlUserInput = shopurlUserInput.lower()
    shopurlUserInput = shopurlUserInput.replace(" ", "")
    shopurlUserInput = shopurlUserInput.replace("\n", "")
    shopurlUserInput = shopurlUserInput.replace("\r", "")

    # remove protocol from the beginning (if needed)
    if shopurlUserInput.find("://") > 0:
        shopurlUserInput = shopurlUserInput[shopurlUserInput.find("://")+3:]

    # remove all path after domain 
    if shopurlUserInput.find("/") > 0:
        shopurlUserInput = shopurlUserInput[:shopurlUserInput.find("/")]

    # add correct protocol again
    if ( not shopurlUserInput.startswith("http://") and not shopurlUserInput.startswith("https://") ):
        if shopurlUserInput.endswith(".onion"):
            shopurlUserInput = "http://{0}".format(shopurlUserInput)
        else:
            shopurlUserInput = "https://{0}".format(shopurlUserInput)

    return shopurlUserInput

def apiGetHosts(session, shopurl):

    print("# apiGetHosts")
    hosts=[]

    # make HTTP request
    try:
        response = session.get("{0}/api/v1/public/hosts/".format(shopurl))
    except Exception as e:
        eprint(e)
        print("error='FAILED HTTP REQUEST'")
        return
    if response.status_code != 200:
        eprint(response.content)
        print("error='FAILED HTTP CODE ({0})'".format(response.status_code))
        return
    
    # parse & validate data
    try:
        jData = json.loads(response.content)
    except Exception as e:
        eprint(e)
        print("error='FAILED JSON PARSING'")
        return
    if not isinstance(jData, list):
        print("error='NOT A JSON LIST'")
        return    
    for idx, hostEntry in enumerate(jData):
        try:
            # ignore if not offering tor bridge
            if not hostEntry['offers_tor_bridges']: continue
            # ignore if duration is less than an hour
            if hostEntry['tor_bridge_duration'] < 3600: continue
            # add duration per hour value 
            hostEntry['tor_bridge_duration_hours'] = math.floor(hostEntry['tor_bridge_duration']/3600)
            # ignore if prices are negative or below one sat (maybe msats later)
            if hostEntry['tor_bridge_price_initial'] < 1000: continue
            if hostEntry['tor_bridge_price_extension'] < 1000: continue
            # add price in sats
            hostEntry['tor_bridge_price_initial_sats'] = math.ceil(hostEntry['tor_bridge_price_initial']/1000)
            hostEntry['tor_bridge_price_extension_sats'] = math.ceil(hostEntry['tor_bridge_price_extension']/1000)
            # ignore name is less then 3 chars
            if len(hostEntry['name']) < 3: continue
            # ignore id with zero value
            if len(hostEntry['id']) < 1: continue
            # shorten names to 20 chars max
            hostEntry['name'] = hostEntry['name'][:20]
        except Exception as e:
            eprint(e)
            print("error='PARSING HOST ENTRY'")
            return    

        print("({0}) {1} ({2} hours, first: {3} sats, next: {4} sats)".format(idx, hostEntry['name'].ljust(20), hostEntry['tor_bridge_duration_hours'], hostEntry['tor_bridge_price_initial_sats'], hostEntry['tor_bridge_price_extension_sats']))
        hosts.append(hostEntry)
    
    print("# found {0} valid torbridge hosts".format(len(hosts)))
    return hosts

def apiPlaceOrderNew(session, shopurl, hostid, toraddressWithPort):

    print("# apiPlaceOrderNew")

    try:
        postData={
            'product': "tor_bridge",
            'host_id': hostid,
            'tos_accepted': True,
            'comment': 'test',
            'target': toraddressWithPort,
            'public_key': ''
        }  
        response = session.post("{0}/api/v1/public/order/".format(shopurl), data=postData)
    except Exception as e:
        eprint(e)
        print("error='FAILED HTTP REQUEST'")
        return
    if response.status_code != 201:
        eprint(response.content)
        print("error='FAILED HTTP CODE ({0}) != 201'".format(response.status_code))
        return

    # parse & validate data
    try:
        jData = json.loads(response.content)
        if len(jData['id']) == 0:
            print("error='MISSING ID'")
            return
    except Exception as e:
        eprint(e)
        print("error='FAILED JSON PARSING'")
        return

    return jData['id']

def apiPlaceOrderExtension(session, shopurl, bridgeid):

    print("# apiPlaceOrderExtension")

    try:
        url="{0}/api/v1/public/tor_bridges/{1}/extend/".format(shopurl, bridgeid)
        response = session.post(url)
    except Exception as e:
        eprint(url)
        eprint(e)
        print("error='FAILED HTTP REQUEST'")
        return
    if response.status_code != 200 and response.status_code != 201:
        eprint(response.content)
        print("error='FAILED HTTP CODE ({0}) != 201'".format(response.status_code))
        return

    # parse & validate data
    try:
        jData = json.loads(response.content)
        if len(jData['po_id']) == 0:
            print("error='MISSING ID'")
            return
    except Exception as e:
        eprint(e)
        print("error='FAILED JSON PARSING'")
        return

    return jData['po_id']


def apiGetOrder(session, shopurl, orderid):

    print("# apiGetOrder")

    # make HTTP request
    try:
        response = session.get("{0}/api/v1/public/pos/{1}/".format(shopurl,orderid))
    except Exception as e:
        eprint(e)
        print("error='FAILED HTTP REQUEST'")
        return
    if response.status_code != 200:
        eprint(response.content)
        print("error='FAILED HTTP CODE ({0})'".format(response.status_code))
        return
    
    # parse & validate data
    try:
        jData = json.loads(response.content)
        if len(jData['item_details']) == 0:
            print("error='MISSING ITEM'")
            return
        if len(jData['ln_invoices']) > 1:
            print("error='MORE THEN ONE INVOICE'")
            return
    except Exception as e:
        eprint(e)
        print("error='FAILED JSON PARSING'")
        return

    return jData

def apiGetBridgeStatus(session, shopurl, bridgeid):

    print("# apiGetBridgeStatus")

    # make HTTP request
    try:
        response = session.get("{0}/api/v1/public/tor_bridges/{1}/".format(shopurl,bridgeid))
    except Exception as e:
        eprint(e)
        print("error='FAILED HTTP REQUEST'")
        return
    if response.status_code != 200:
        eprint(response.content)
        print("error='FAILED HTTP CODE ({0})'".format(response.status_code))
        return
    # parse & validate data
    try:
        jData = json.loads(response.content)
        if len(jData['id']) == 0:
            print("error='ID IS MISSING'")
            return
    except Exception as e:
        eprint(e)
        print("error='FAILED JSON PARSING'")
        return

    return jData

def lndDecodeInvoice(lnInvoiceString):
    try:
        # call LND GRPC API
        macaroon = codecs.encode(open(LND_ADMIN_MACAROON_PATH, 'rb').read(), 'hex')
        os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
        cert = open(LND_TLS_PATH, 'rb').read()
        ssl_creds = grpc.ssl_channel_credentials(cert)
        channel = grpc.secure_channel("{0}:10009".format(LND_IP), ssl_creds)
        stub = rpcstub.LightningStub(channel)
        request = lnrpc.PayReqString(
            pay_req=lnInvoiceString,
        )
        response = stub.DecodePayReq(request, metadata=[('macaroon', macaroon)])

        # validate results
        if response.num_msat <= 0:
            print("error='ZERO INVOICES NOT ALLOWED'")
            return  

    except Exception as e:
        print("error='FAILED LND INVOICE DECODING'")
        return

    return response

def lndPayInvoice(lnInvoiceString):
    try:
        # call LND GRPC API
        macaroon = codecs.encode(open(LND_ADMIN_MACAROON_PATH, 'rb').read(), 'hex')
        os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
        cert = open(LND_TLS_PATH, 'rb').read()
        ssl_creds = grpc.ssl_channel_credentials(cert)
        channel = grpc.secure_channel("{0}:10009".format(LND_IP), ssl_creds)
        stub = rpcstub.LightningStub(channel)
        request = lnrpc.SendRequest(
            payment_request=lnInvoiceString,
        )
        response = stub.SendPaymentSync(request, metadata=[('macaroon', macaroon)])

        # validate results
        if len(response.payment_error) > 0:
            print("error='PAYMENT FAILED'")
            print("error_detail='{}'".format(response.payment_error))
            return  

    except Exception as e:
       print("error='FAILED LND INVOICE PAYMENT'")
       return

    return response


torTarget = "qrmfuxwgyzk5jdjz.onion:80"
shopUrl = normalizeShopUrl(DEFAULT_SHOPURL)
session = requests.session()
if cfg.run_behind_tor:
    session.proxies = {'http':  'socks5://127.0.0.1:9050', 'https': 'socks5://127.0.0.1:9050'}

print("#### GET HOSTS")
hosts = apiGetHosts(session, shopUrl)
if hosts is None: sys.exit()
print(hosts)

print("#### PLACE ORDER")
hostid = hosts[0]['id']
msatsFirst=hosts[0]['tor_bridge_price_initial']
msatsNext=hosts[0]['tor_bridge_price_extension']
duration=hosts[0]['tor_bridge_duration']
orderid = apiPlaceOrderNew(session, shopUrl, hostid, torTarget)
if orderid is None: sys.exit()
print(orderid)

print("#### WAIT UNTIL INVOICE IS AVAILABLE")
while True:
    print("## Loop")
    order = apiGetOrder(session, shopUrl, orderid)
    if order is None: sys.exit()
    print(order)
    if len(order['ln_invoices']) > 0 and order['ln_invoices'][0]['payment_request'] is not None: break
    time.sleep(2)
paymentRequestStr = order['ln_invoices'][0]['payment_request']
bridge_id = order['item_details'][0]['product']['id']
bridge_ip = order['item_details'][0]['product']['host']['ip']
bridge_port = order['item_details'][0]['product']['port']

print("#### DECODE INVOICE")
print(paymentRequestStr)
paymentRequestDecoded = lndDecodeInvoice(paymentRequestStr)
if paymentRequestDecoded is None: sys.exit()

print("#### CHECK INVOICE")
print("# is invoice not more then advertised: {0}".format(msatsFirst))
print("# amount in invoice is: {0}".format(paymentRequestDecoded.num_msat))
if msatsFirst < paymentRequestDecoded.num_msat:
    print("# invoice wants more the advertised -> EXIT")
    sys.exit(1)

print("#### PAY INVOICE")
payedInvoice = lndPayInvoice(paymentRequestStr)
if payedInvoice is None: sys.exit()
print(payedInvoice)

print("#### CHECK IF BRIDGE IS READY")
while True:
    print("## Loop")
    bridge = apiGetBridgeStatus(session, shopUrl, bridge_id)
    if bridge is None: sys.exit()
    print(bridge)
    if bridge['status'] == "A": break
    time.sleep(3)
bridge_id = bridge['id']
bridge_suspendafter = bridge['suspend_after']
bridge_port = bridge['port']

print("#### CHECK IF DURATION DELIVERED AS PROMISED")
contract_broken=False
secondsDelivered=secondsLeft(parseDate(bridge_suspendafter))
print("delivered({0}) promised({1})".format(secondsDelivered, duration))
if (secondsDelivered + 600) < duration:
    print("CONTRACT BROKEN - duration delivered is too small")
    sys.exit()

print("BRIDGE READY: {0}:{1} -> {2}".format(bridge_ip, bridge_port, torTarget))

time.sleep(10)

print("#### PLACE EXTENSION ORDER")
orderid = apiPlaceOrderExtension(session, shopUrl, bridge_id)
if orderid is None: sys.exit()
print(orderid)

print("#### WAIT UNTIL INVOICE IS AVAILABLE")
while True:
    print("## Loop")
    order = apiGetOrder(session, shopUrl, orderid)
    if order is None: sys.exit()
    print(order)
    if len(order['ln_invoices']) > 0 and order['ln_invoices'][0]['payment_request'] is not None: break
    time.sleep(2)
paymentRequestStr = order['ln_invoices'][0]['payment_request']

print("#### DECODE INVOICE")
print(paymentRequestStr)
paymentRequestDecoded = lndDecodeInvoice(paymentRequestStr)
if paymentRequestDecoded is None: sys.exit()

print("#### CHECK INVOICE (EXTENSION)")
print("# is invoice not more then advertised: {0}".format(msatsNext))
print("# amount in invoice is: {0}".format(paymentRequestDecoded.num_msat))
if msatsNext < paymentRequestDecoded.num_msat:
    print("# invoice wants more the advertised -> EXIT")
    sys.exit(1)

print("#### PAY INVOICE")
payedInvoice = lndPayInvoice(paymentRequestStr)
if payedInvoice is None: sys.exit()
print(payedInvoice)

print("#### CHECK IF BRIDGE GOT EXTENDED")
while True:
    print("## Loop")
    bridge = apiGetBridgeStatus(session, shopUrl, bridge_id)
    if bridge is None: sys.exit()
    print(bridge)
    if bridge['suspend_after'] != bridge_suspendafter: break
    time.sleep(3)

print("BRIDGE GOT EXTENDED: {0} -> {1}".format(bridge_suspendafter, bridge['suspend_after']))

print("#### CHECK IF EXTENSION DURATION WAS CORRECT")
secondsLeftOld = secondsLeft(parseDate(bridge_suspendafter))
secondsLeftNew = secondsLeft(parseDate(bridge['suspend_after']))
secondsExtended = secondsLeftNew - secondsLeftOld
print("# secondsExtended({0}) promised({1})".format(secondsExtended, duration))
if secondsExtended < duration:
    print("CONTRACT BROKEN - duration delivered is too small")
    sys.exit()
else:
    print("OK")

if False: '''

###############
# MENU
###############

if sys.argv[1] == "menu":
    from dialog import Dialog
    d = Dialog(dialog="dialog",autowidgetsize=True)
    d.set_background_title("IP2TOR Subscription Service")
    code, tag = d.menu("OK, then you have two options:",
        choices=[("(1)", "Test HTTP REQUEST thru TOR PROXY"),
        ("(2)", "Make REST API - JSON request"),
        ("(3)", "TOML test"),
        ("(4)", "Working with .conf files")])
    if code == d.OK:
        if tag == "(1)":
            print("Needs: pip3 install pysocks\n")
            session = requests.session()
            session.proxies = {'http':  'socks5://127.0.0.1:9050', 'https': 'socks5://127.0.0.1:9050'}
            print("Call 'http://httpbin.org/ip' thru TOR proxy:\n")
            print(session.get("http://httpbin.org/ip").text)
            print("Call 'http://httpbin.org/ip' normal:\n")
            print(requests.get("http://httpbin.org/ip").text)
            print("Call 'https://shop.ip2t.org/api/v1/public/hosts/' thru TOR:\n")
            print(session.get("https://shop.ip2t.org/api/v1/public/hosts/").text)
        if tag == "(2)":

        if tag == "(3)":
            print ("Needs: pip3 install toml")
            import toml
            toml_string = """
            """
        if tag == "(4)":
            with open('/mnt/hdd/raspiblitz.conf', 'r') as myfile:
                data=myfile.read()
            print(data)
            import toml
            parsed_toml = toml.loads(data)
            print(parsed_toml)

    else:
        print("Cancel")
    '''