from datetime import timedelta
from django.utils import timezone
from api.lightning.node import LNNode
from django.db.models import Q

from api.models import Order, LNPayment, MarketTick, User, Currency
from api.tasks import send_message
from decouple import config

import math
import ast

FEE = float(config("FEE"))
MAKER_FEE_SPLIT = float(config("MAKER_FEE_SPLIT"))

BOND_SIZE = float(config("BOND_SIZE"))
ESCROW_USERNAME = config("ESCROW_USERNAME")
PENALTY_TIMEOUT = int(config("PENALTY_TIMEOUT"))

MIN_TRADE = int(config("MIN_TRADE"))
MAX_TRADE = int(config("MAX_TRADE"))

EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
EXP_TAKER_BOND_INVOICE = int(config("EXP_TAKER_BOND_INVOICE"))

BOND_EXPIRY = int(config("BOND_EXPIRY"))
ESCROW_EXPIRY = int(config("ESCROW_EXPIRY"))

PUBLIC_ORDER_DURATION = int(config("PUBLIC_ORDER_DURATION"))
INVOICE_AND_ESCROW_DURATION = int(config("INVOICE_AND_ESCROW_DURATION"))
FIAT_EXCHANGE_DURATION = int(config("FIAT_EXCHANGE_DURATION"))


class Logics:
    @classmethod
    def validate_already_maker_or_taker(cls, user):
        """Validates if a use is already not part of an active order"""

        active_order_status = [
            Order.Status.WFB,
            Order.Status.PUB,
            Order.Status.TAK,
            Order.Status.WF2,
            Order.Status.WFE,
            Order.Status.WFI,
            Order.Status.CHA,
            Order.Status.FSE,
            Order.Status.DIS,
            Order.Status.WFR,
        ]
        """Checks if the user is already partipant of an active order"""
        queryset = Order.objects.filter(maker=user,
                                        status__in=active_order_status)
        if queryset.exists():
            return (
                False,
                {
                    "bad_request": "You are already maker of an active order"
                },
                queryset[0],
            )

        queryset = Order.objects.filter(taker=user,
                                        status__in=active_order_status)
        if queryset.exists():
            return (
                False,
                {
                    "bad_request": "You are already taker of an active order"
                },
                queryset[0],
            )

        # Edge case when the user is in an order that is failing payment and he is the buyer
        queryset = Order.objects.filter(Q(maker=user) | Q(taker=user),
                                        status=Order.Status.FAI)
        if queryset.exists():
            order = queryset[0]
            if cls.is_buyer(order, user):
                return (
                    False,
                    {
                        "bad_request":
                        "You are still pending a payment from a recent order"
                    },
                    order,
                )

        return True, None, None

    def validate_order_size(order):
        """Validates if order is withing limits in satoshis at t0"""
        if order.t0_satoshis > MAX_TRADE:
            return False, {
                "bad_request":
                "Your order is too big. It is worth " +
                "{:,}".format(order.t0_satoshis) +
                " Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
                " Sats"
            }
        if order.t0_satoshis < MIN_TRADE:
            return False, {
                "bad_request":
                "Your order is too small. It is worth " +
                "{:,}".format(order.t0_satoshis) +
                " Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
                " Sats"
            }
        return True, None

    def user_activity_status(last_seen):
        if last_seen > (timezone.now() - timedelta(minutes=2)):
            return "Active"
        elif last_seen > (timezone.now() - timedelta(minutes=10)):
            return "Seen recently"
        else:
            return "Inactive"

    @classmethod
    def take(cls, order, user):
        is_penalized, time_out = cls.is_penalized(user)
        if is_penalized:
            return False, {
                "bad_request",
                f"You need to wait {time_out} seconds to take an order",
            }
        else:
            order.taker = user
            order.status = Order.Status.TAK
            order.expires_at = timezone.now() + timedelta(
                seconds=Order.t_to_expire[Order.Status.TAK])
            order.save()
            send_message.delay(order.id,'order_taken')
            return True, None

    def is_buyer(order, user):
        is_maker = order.maker == user
        is_taker = order.taker == user
        return (is_maker and order.type == Order.Types.BUY) or (
            is_taker and order.type == Order.Types.SELL)

    def is_seller(order, user):
        is_maker = order.maker == user
        is_taker = order.taker == user
        return (is_maker and order.type == Order.Types.SELL) or (
            is_taker and order.type == Order.Types.BUY)

    def satoshis_now(order):
        """checks trade amount in sats"""
        if order.is_explicit:
            satoshis_now = order.satoshis
        else:
            exchange_rate = float(order.currency.exchange_rate)
            premium_rate = exchange_rate * (1 + float(order.premium) / 100)
            satoshis_now = (float(order.amount) /
                            premium_rate) * 100 * 1000 * 1000

        return int(satoshis_now)

    def price_and_premium_now(order):
        """computes order price and premium with current rates"""
        exchange_rate = float(order.currency.exchange_rate)
        if not order.is_explicit:
            premium = order.premium
            price = exchange_rate * (1 + float(premium) / 100)
        else:
            order_rate = float(
                order.amount) / (float(order.satoshis) / 100000000)
            premium = order_rate / exchange_rate - 1
            premium = int(premium * 10000) / 100  # 2 decimals left
            price = order_rate

        significant_digits = 5
        price = round(
            price,
            significant_digits - int(math.floor(math.log10(abs(price)))) - 1)

        return price, premium

    @classmethod
    def order_expires(cls, order):
        """General cases when time runs out."""

        # Do not change order status if an order in any with
        # any of these status is sent to expire here
        does_not_expire = [
            Order.Status.DEL,
            Order.Status.UCA,
            Order.Status.EXP,
            Order.Status.TLD,
            Order.Status.DIS,
            Order.Status.CCA,
            Order.Status.PAY,
            Order.Status.SUC,
            Order.Status.FAI,
            Order.Status.MLD,
        ]

        if order.status in does_not_expire:
            return False

        elif order.status == Order.Status.WFB:
            order.status = Order.Status.EXP
            cls.cancel_bond(order.maker_bond)
            order.save()
            return True

        elif order.status == Order.Status.PUB:
            cls.return_bond(order.maker_bond)
            order.status = Order.Status.EXP
            order.save()
            send_message.delay(order.id,'order_expired_untaken')
            return True

        elif order.status == Order.Status.TAK:
            cls.cancel_bond(order.taker_bond)
            cls.kick_taker(order)
            send_message.delay(order.id,'taker_expired_b4bond')
            return True

        elif order.status == Order.Status.WF2:
            """Weird case where an order expires and both participants
            did not proceed with the contract. Likely the site was
            down or there was a bug. Still bonds must be charged
            to avoid service DDOS."""

            cls.settle_bond(order.maker_bond)
            cls.settle_bond(order.taker_bond)
            cls.cancel_escrow(order)
            order.status = Order.Status.EXP
            order.save()
            return True

        elif order.status == Order.Status.WFE:
            maker_is_seller = cls.is_seller(order, order.maker)
            # If maker is seller, settle the bond and order goes to expired
            if maker_is_seller:
                cls.settle_bond(order.maker_bond)
                cls.return_bond(order.taker_bond)
                # If seller is offline the escrow LNpayment does not exist
                try: 
                    cls.cancel_escrow(order)
                except:
                    pass
                order.status = Order.Status.EXP
                order.save()
                return True

            # If maker is buyer, settle the taker's bond order goes back to public
            else:
                cls.settle_bond(order.taker_bond)
                # If seller is offline the escrow LNpayment does not even exist
                try: 
                    cls.cancel_escrow(order)
                except:
                    pass
                order.taker = None
                order.taker_bond = None
                order.trade_escrow = None
                cls.publish_order(order)
                return True

        elif order.status == Order.Status.WFI:
            # The trade could happen without a buyer invoice. However, this user
            # is likely AFK; will probably desert the contract as well.

            maker_is_buyer = cls.is_buyer(order, order.maker)
            # If maker is buyer, settle the bond and order goes to expired
            if maker_is_buyer:
                cls.settle_bond(order.maker_bond)
                cls.return_bond(order.taker_bond)
                cls.return_escrow(order)
                order.status = Order.Status.EXP
                order.save()
                return True

            # If maker is seller settle the taker's bond, order goes back to public
            else:
                cls.settle_bond(order.taker_bond)
                cls.return_escrow(order)
                order.taker = None
                order.taker_bond = None
                order.trade_escrow = None
                cls.publish_order(order)
                return True

        elif order.status in [Order.Status.CHA, Order.Status.FSE]:
            # Another weird case. The time to confirm 'fiat sent or received' expired. Yet no dispute
            # was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat
            # sent", we assume this is a dispute case by default.
            cls.open_dispute(order)
            return True

    @classmethod
    def kick_taker(cls, order):
        """The taker did not lock the taker_bond. Now he has to go"""
        # Add a time out to the taker
        if order.taker:
            profile = order.taker.profile
            profile.penalty_expiration = timezone.now() + timedelta(
                seconds=PENALTY_TIMEOUT)
            profile.save()

        # Make order public again
        order.taker = None
        order.taker_bond = None
        cls.publish_order(order)
        return True

    @classmethod
    def open_dispute(cls, order, user=None):

        # Always settle escro and bonds during a dispute. Disputes 
        # can take long to resolve, it might trigger force closure 
        # for unresolve HTLCs) Dispute winner will have to submit a 
        # new invoice for value of escrow + bond.

        if not order.trade_escrow.status == LNPayment.Status.SETLED:
            cls.settle_escrow(order)
            cls.settle_bond(order.maker_bond)
            cls.settle_bond(order.taker_bond)

        order.is_disputed = True
        order.status = Order.Status.DIS
        order.expires_at = timezone.now() + timedelta(
            seconds=Order.t_to_expire[Order.Status.DIS])
        order.save()

        # User could be None if a dispute is open automatically due to weird expiration.
        if not user == None:
            profile = user.profile
            profile.num_disputes = profile.num_disputes + 1
            if profile.orders_disputes_started == None:
                profile.orders_disputes_started = [str(order.id)]
            else:
                profile.orders_disputes_started = list(
                    profile.orders_disputes_started).append(str(order.id))
            profile.save()

        return True, None

    def dispute_statement(order, user, statement):
        """Updates the dispute statements"""

        if not order.status == Order.Status.DIS:
            return False, {
                "bad_request":
                "Only orders in dispute accept dispute statements"
            }

        if len(statement) > 5000:
            return False, {
                "bad_statement": "The statement is longer than 5000 characters"
            }
        
        if len(statement) < 100:
            return False, {
                "bad_statement": "The statement is too short. Make sure to be thorough."
            }

        if order.maker == user:
            order.maker_statement = statement
        else:
            order.taker_statement = statement

        # If both statements are in, move status to wait for dispute resolution
        if order.maker_statement not in [None,""] and order.taker_statement not in [None,""]:
            order.status = Order.Status.WFR
            order.expires_at = timezone.now() + timedelta(
                seconds=Order.t_to_expire[Order.Status.WFR])

        order.save()
        return True, None

    @classmethod
    def payout_amount(cls, order, user):
        """Computes buyer invoice amount. Uses order.last_satoshis,
        that is the final trade amount set at Taker Bond time"""

        if user == order.maker:
            fee_fraction = FEE * MAKER_FEE_SPLIT
        elif user == order.taker:
            fee_fraction = FEE * (1 - MAKER_FEE_SPLIT)

        fee_sats = order.last_satoshis * fee_fraction

        reward_tip = int(config('REWARD_TIP')) if user.profile.is_referred else 0

        if cls.is_buyer(order, user):
            invoice_amount = round(order.last_satoshis - fee_sats - reward_tip)  # Trading fee to buyer is charged here.

        return True, {"invoice_amount": invoice_amount}

    @classmethod
    def escrow_amount(cls, order, user):
        """Computes escrow invoice amount. Uses order.last_satoshis,
        that is the final trade amount set at Taker Bond time"""    

        if user == order.maker:
            fee_fraction = FEE * MAKER_FEE_SPLIT
        elif user == order.taker:
            fee_fraction = FEE * (1 - MAKER_FEE_SPLIT)

        fee_sats = order.last_satoshis * fee_fraction 

        reward_tip = int(config('REWARD_TIP')) if user.profile.is_referred else 0

        if cls.is_seller(order, user):
            escrow_amount = round(order.last_satoshis + fee_sats + reward_tip)  # Trading fee to seller is charged here.

        return True, {"escrow_amount": escrow_amount}

    @classmethod
    def update_invoice(cls, order, user, invoice):

        # only the buyer can post a buyer invoice

        if not cls.is_buyer(order, user):
            return False, {
                "bad_request":
                "Only the buyer of this order can provide a buyer invoice."
            }
        if not order.taker_bond:
            return False, {"bad_request": "Wait for your order to be taken."}
        if (not (order.taker_bond.status == order.maker_bond.status ==
                 LNPayment.Status.LOCKED)
                and not order.status == Order.Status.FAI):
            return False, {
                "bad_request":
                "You cannot submit a invoice while bonds are not locked."
            }

        num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
        payout = LNNode.validate_ln_invoice(invoice, num_satoshis)

        if not payout["valid"]:
            return False, payout["context"]

        order.payout, _ = LNPayment.objects.update_or_create(
            concept=LNPayment.Concepts.PAYBUYER,
            type=LNPayment.Types.NORM,
            sender=User.objects.get(username=ESCROW_USERNAME),
            order_paid=
            order,  # In case this user has other payouts, update the one related to this order.
            receiver=user,
            # if there is a LNPayment matching these above, it updates that one with defaults below.
            defaults={
                "invoice": invoice,
                "status": LNPayment.Status.VALIDI,
                "num_satoshis": num_satoshis,
                "description": payout["description"],
                "payment_hash": payout["payment_hash"],
                "created_at": payout["created_at"],
                "expires_at": payout["expires_at"],
            },
        )

        # If the order status is 'Waiting for invoice'. Move forward to 'chat'
        if order.status == Order.Status.WFI:
            order.status = Order.Status.CHA
            order.expires_at = timezone.now() + timedelta(
                seconds=Order.t_to_expire[Order.Status.CHA])

        # If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
        if order.status == Order.Status.WF2:
            # If the escrow does not exist, or is not locked move to WFE.
            if order.trade_escrow == None:
                order.status = Order.Status.WFE
            # If the escrow is locked move to Chat.
            elif order.trade_escrow.status == LNPayment.Status.LOCKED:
                order.status = Order.Status.CHA
                order.expires_at = timezone.now() + timedelta(
                    seconds=Order.t_to_expire[Order.Status.CHA])
            else:
                order.status = Order.Status.WFE

        # If the order status is 'Failed Routing'. Retry payment.
        if order.status == Order.Status.FAI:
            if LNNode.double_check_htlc_is_settled(
                    order.trade_escrow.payment_hash):
                order.status = Order.Status.PAY
                order.payout.status = LNPayment.Status.FLIGHT
                order.payout.routing_attempts = 0
                order.payout.save()
                order.save()

        order.save()
        return True, None

    def add_profile_rating(profile, rating):
        """adds a new rating to a user profile"""

        # TODO Unsafe, does not update ratings, it adds more ratings everytime a new rating is clicked.
        profile.total_ratings += 1
        latest_ratings = profile.latest_ratings
        if latest_ratings == None:
            profile.latest_ratings = [rating]
            profile.avg_rating = rating

        else:
            latest_ratings = ast.literal_eval(latest_ratings)
            latest_ratings.append(rating)
            profile.latest_ratings = latest_ratings
            profile.avg_rating = sum(list(map(int, latest_ratings))) / len(
                latest_ratings
            )  # Just an average, but it is a list of strings. Has to be converted to int.

        profile.save()

    def is_penalized(user):
        """Checks if a user that is not participant of orders
        has a limit on taking or making a order"""

        if user.profile.penalty_expiration:
            if user.profile.penalty_expiration > timezone.now():
                time_out = (user.profile.penalty_expiration -
                            timezone.now()).seconds
                return True, time_out

        return False, None

    @classmethod
    def cancel_order(cls, order, user, state=None):

        # Do not change order status if an is in order
        # any of these status
        do_not_cancel = [
            Order.Status.DEL,
            Order.Status.UCA,
            Order.Status.EXP,
            Order.Status.TLD,
            Order.Status.DIS,
            Order.Status.CCA,
            Order.Status.PAY,
            Order.Status.SUC,
            Order.Status.FAI,
            Order.Status.MLD,
        ]

        if order.status in do_not_cancel:
            return False, {"bad_request": "You cannot cancel this order"}

        # 1) When maker cancels before bond
        """The order never shows up on the book and order 
        status becomes "cancelled" """
        if order.status == Order.Status.WFB and order.maker == user:
            cls.cancel_bond(order.maker_bond)
            order.status = Order.Status.UCA
            order.save()
            return True, None

            # 2) When maker cancels after bond
            """The order dissapears from book and goes to cancelled. If strict, maker is charged the bond 
            to prevent DDOS on the LN node and order book. If not strict, maker is returned
            the bond (more user friendly)."""
        elif order.status == Order.Status.PUB and order.maker == user:
            # Return the maker bond (Maker gets returned the bond for cancelling public order)
            if cls.return_bond(order.maker_bond):  # strict cancellation: cls.settle_bond(order.maker_bond):
                order.status = Order.Status.UCA
                order.save()
                send_message.delay(order.id,'public_order_cancelled')
                return True, None

            # 3) When taker cancels before bond
            """ The order goes back to the book as public.
            LNPayment "order.taker_bond" is deleted() """
        elif order.status == Order.Status.TAK and order.taker == user:
            # adds a timeout penalty
            cls.cancel_bond(order.taker_bond)
            cls.kick_taker(order)
            send_message.delay(order.id,'taker_canceled_b4bond')
            return True, None

            # 4) When taker or maker cancel after bond (before escrow)
            """The order goes into cancelled status if maker cancels.
            The order goes into the public book if taker cancels.
            In both cases there is a small fee."""

            # 4.a) When maker cancel after bond (before escrow)
            """The order into cancelled status if maker cancels."""
        elif (order.status in [
                Order.Status.PUB, Order.Status.TAK, Order.Status.WF2,
                Order.Status.WFE
        ] and order.maker == user):
            # Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
            valid = cls.settle_bond(order.maker_bond)
            cls.return_bond(order.taker_bond)  # returns taker bond
            if valid:
                order.status = Order.Status.UCA
                order.save()
                return True, None

            # 4.b) When taker cancel after bond (before escrow)
            """The order into cancelled status if maker cancels."""
        elif (order.status in [Order.Status.WF2, Order.Status.WFE]
              and order.taker == user):
            # Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
            valid = cls.settle_bond(order.taker_bond)
            if valid:
                order.taker = None
                cls.publish_order(order)
                return True, None

            # 5) When trade collateral has been posted (after escrow)
            """Always goes to CCA status. Collaboration is needed.
            When a user asks for cancel, 'order.m/t/aker_asked_cancel' goes True.
            When the second user asks for cancel. Order is totally cancelled.
            Must have a small cost for both parties to prevent node DDOS."""
        elif order.status in [
                Order.Status.WFI, Order.Status.CHA
        ]:

            # if the maker had asked, and now the taker does: cancel order, return everything
            if order.maker_asked_cancel and user == order.taker:
                cls.collaborative_cancel(order)
                return True, None

            # if the taker had asked, and now the maker does: cancel order, return everything
            elif order.taker_asked_cancel and user == order.maker:
                cls.collaborative_cancel(order)
                return True, None

            # Otherwise just make true the asked for cancel flags
            elif user == order.taker:
                order.taker_asked_cancel = True
                order.save()
                return True, None

            elif user == order.maker:
                order.maker_asked_cancel = True
                order.save()
                return True, None

        else:
            return False, {"bad_request": "You cannot cancel this order"}

    @classmethod
    def collaborative_cancel(cls, order):
        cls.return_bond(order.maker_bond)
        cls.return_bond(order.taker_bond)
        cls.return_escrow(order)
        order.status = Order.Status.CCA
        order.save()
        return

    def publish_order(order):
        order.status = Order.Status.PUB
        order.expires_at = order.created_at + timedelta(
            seconds=Order.t_to_expire[Order.Status.PUB])
        order.save()
        send_message.delay(order.id,'order_published')
        return

    @classmethod
    def is_maker_bond_locked(cls, order):
        if order.maker_bond.status == LNPayment.Status.LOCKED:
            return True
        elif LNNode.validate_hold_invoice_locked(order.maker_bond):
            cls.publish_order(order)
            return True
        return False

    @classmethod
    def gen_maker_hold_invoice(cls, order, user):

        # Do not gen and cancel if order is older than expiry time
        if order.expires_at < timezone.now():
            cls.order_expires(order)
            return False, {
                "bad_request":
                "Invoice expired. You did not confirm publishing the order in time. Make a new order."
            }

        # Return the previous invoice if there was one and is still unpaid
        if order.maker_bond:
            if cls.is_maker_bond_locked(order):
                return False, None
            elif order.maker_bond.status == LNPayment.Status.INVGEN:
                return True, {
                    "bond_invoice": order.maker_bond.invoice,
                    "bond_satoshis": order.maker_bond.num_satoshis,
                }

        # If there was no maker_bond object yet, generates one
        order.last_satoshis = cls.satoshis_now(order)
        bond_satoshis = int(order.last_satoshis * BOND_SIZE)

        description = f"RoboSats - Publishing '{str(order)}' - Maker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally."

        # Gen hold Invoice
        try:
            hold_payment = LNNode.gen_hold_invoice(
                bond_satoshis,
                description,
                invoice_expiry=Order.t_to_expire[Order.Status.WFB],
                cltv_expiry_secs=BOND_EXPIRY * 3600,
            )
        except Exception as e:
            print(str(e))
            if "failed to connect to all addresses" in str(e):
                return False, {
                    "bad_request":
                    "The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware."
                }
            if "wallet locked" in str(e):
                return False, {
                    "bad_request":
                    "This is weird, RoboSats' lightning wallet is locked. Check in the Telegram group, maybe the staff has died."
                }

        order.maker_bond = LNPayment.objects.create(
            concept=LNPayment.Concepts.MAKEBOND,
            type=LNPayment.Types.HOLD,
            sender=user,
            receiver=User.objects.get(username=ESCROW_USERNAME),
            invoice=hold_payment["invoice"],
            preimage=hold_payment["preimage"],
            status=LNPayment.Status.INVGEN,
            num_satoshis=bond_satoshis,
            description=description,
            payment_hash=hold_payment["payment_hash"],
            created_at=hold_payment["created_at"],
            expires_at=hold_payment["expires_at"],
            cltv_expiry=hold_payment["cltv_expiry"],
        )

        order.save()
        return True, {
            "bond_invoice": hold_payment["invoice"],
            "bond_satoshis": bond_satoshis,
        }

    @classmethod
    def finalize_contract(cls, order):
        """When the taker locks the taker_bond
        the contract is final"""

        # THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
        # (This is the last update to "last_satoshis", it becomes the escrow amount next)
        order.last_satoshis = cls.satoshis_now(order)
        order.taker_bond.status = LNPayment.Status.LOCKED
        order.taker_bond.save()

         # With the bond confirmation the order is extended 'public_order_duration' hours
        order.expires_at = timezone.now() + timedelta(
        seconds=Order.t_to_expire[Order.Status.WF2])
        order.status = Order.Status.WF2
        order.save()

        # Both users profiles are added one more contract // Unsafe can add more than once.
        order.maker.profile.total_contracts += 1
        order.taker.profile.total_contracts += 1
        order.maker.profile.save()
        order.taker.profile.save()
       
        # Log a market tick
        try:
            MarketTick.log_a_tick(order)
        except:
            pass
        return True

    @classmethod
    def is_taker_bond_locked(cls, order):
        if order.taker_bond.status == LNPayment.Status.LOCKED:
            return True
        elif LNNode.validate_hold_invoice_locked(order.taker_bond):
            cls.finalize_contract(order)
            return True
        return False

    @classmethod
    def gen_taker_hold_invoice(cls, order, user):

        # Do not gen and kick out the taker if order is older than expiry time
        if order.expires_at < timezone.now():
            cls.order_expires(order)
            return False, {
                "bad_request":
                "Invoice expired. You did not confirm taking the order in time."
            }

        # Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting.
        if order.taker_bond:
            if cls.is_taker_bond_locked(order):
                return False, None
            elif order.taker_bond.status == LNPayment.Status.INVGEN:
                return True, {
                    "bond_invoice": order.taker_bond.invoice,
                    "bond_satoshis": order.taker_bond.num_satoshis,
                }

        # If there was no taker_bond object yet, generates one
        order.last_satoshis = cls.satoshis_now(order)
        bond_satoshis = int(order.last_satoshis * BOND_SIZE)
        pos_text = "Buying" if cls.is_buyer(order, user) else "Selling"
        description = (
            f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}"
            +
            " - Taker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally."
        )

        # Gen hold Invoice
        try:
            hold_payment = LNNode.gen_hold_invoice(
                bond_satoshis,
                description,
                invoice_expiry=Order.t_to_expire[Order.Status.TAK],
                cltv_expiry_secs=BOND_EXPIRY * 3600,
            )

        except Exception as e:
            if "status = StatusCode.UNAVAILABLE" in str(e):
                return False, {
                    "bad_request":
                    "The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware."
                }

        order.taker_bond = LNPayment.objects.create(
            concept=LNPayment.Concepts.TAKEBOND,
            type=LNPayment.Types.HOLD,
            sender=user,
            receiver=User.objects.get(username=ESCROW_USERNAME),
            invoice=hold_payment["invoice"],
            preimage=hold_payment["preimage"],
            status=LNPayment.Status.INVGEN,
            num_satoshis=bond_satoshis,
            description=description,
            payment_hash=hold_payment["payment_hash"],
            created_at=hold_payment["created_at"],
            expires_at=hold_payment["expires_at"],
            cltv_expiry=hold_payment["cltv_expiry"],
        )

        order.expires_at = timezone.now() + timedelta(
            seconds=Order.t_to_expire[Order.Status.TAK])
        order.save()
        return True, {
            "bond_invoice": hold_payment["invoice"],
            "bond_satoshis": bond_satoshis,
        }

    def trade_escrow_received(order):
        """Moves the order forward"""
        # If status is 'Waiting for both' move to Waiting for invoice
        if order.status == Order.Status.WF2:
            order.status = Order.Status.WFI
        # If status is 'Waiting for invoice' move to Chat
        elif order.status == Order.Status.WFE:
            order.status = Order.Status.CHA
            order.expires_at = timezone.now() + timedelta(
                seconds=Order.t_to_expire[Order.Status.CHA])
        order.save()

    @classmethod
    def is_trade_escrow_locked(cls, order):
        if order.trade_escrow.status == LNPayment.Status.LOCKED:
            return True
        elif LNNode.validate_hold_invoice_locked(order.trade_escrow):
            cls.trade_escrow_received(order)
            return True
        return False

    @classmethod
    def gen_escrow_hold_invoice(cls, order, user):

        # Do not generate if escrow deposit time has expired
        if order.expires_at < timezone.now():
            cls.order_expires(order)
            return False, {
                "bad_request":
                "Invoice expired. You did not send the escrow in time."
            }

        # Do not gen if an escrow invoice exist. Do not return if it is already locked. Return the old one if still waiting.
        if order.trade_escrow:
            # Check if status is INVGEN and still not expired
            if cls.is_trade_escrow_locked(order):
                return False, None
            elif order.trade_escrow.status == LNPayment.Status.INVGEN:
                return True, {
                    "escrow_invoice": order.trade_escrow.invoice,
                    "escrow_satoshis": order.trade_escrow.num_satoshis,
                }

        # If there was no taker_bond object yet, generate one
        escrow_satoshis = cls.escrow_amount(order, user)[1]["escrow_amount"] # Amount was fixed when taker bond was locked, fee applied here
        description = f"RoboSats - Escrow amount for '{str(order)}' - It WILL FREEZE IN YOUR WALLET. It will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment."

        # Gen hold Invoice
        try:
            hold_payment = LNNode.gen_hold_invoice(
                escrow_satoshis,
                description,
                invoice_expiry=Order.t_to_expire[Order.Status.WF2],
                cltv_expiry_secs=ESCROW_EXPIRY * 3600,
            )

        except Exception as e:
            if "status = StatusCode.UNAVAILABLE" in str(e):
                return False, {
                    "bad_request":
                    "The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware."
                }

        order.trade_escrow = LNPayment.objects.create(
            concept=LNPayment.Concepts.TRESCROW,
            type=LNPayment.Types.HOLD,
            sender=user,
            receiver=User.objects.get(username=ESCROW_USERNAME),
            invoice=hold_payment["invoice"],
            preimage=hold_payment["preimage"],
            status=LNPayment.Status.INVGEN,
            num_satoshis=escrow_satoshis,
            description=description,
            payment_hash=hold_payment["payment_hash"],
            created_at=hold_payment["created_at"],
            expires_at=hold_payment["expires_at"],
            cltv_expiry=hold_payment["cltv_expiry"],
        )

        order.save()
        return True, {
            "escrow_invoice": hold_payment["invoice"],
            "escrow_satoshis": escrow_satoshis,
        }

    def settle_escrow(order):
        """Settles the trade escrow hold invoice"""
        # TODO ERROR HANDLING
        if LNNode.settle_hold_invoice(order.trade_escrow.preimage):
            order.trade_escrow.status = LNPayment.Status.SETLED
            order.trade_escrow.save()
            return True

    def settle_bond(bond):
        """Settles the bond hold invoice"""
        # TODO ERROR HANDLING
        if LNNode.settle_hold_invoice(bond.preimage):
            bond.status = LNPayment.Status.SETLED
            bond.save()
            return True

    def return_escrow(order):
        """returns the trade escrow"""
        if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
            order.trade_escrow.status = LNPayment.Status.RETNED
            order.trade_escrow.save()
            return True

    def cancel_escrow(order):
        """returns the trade escrow"""
        # Same as return escrow, but used when the invoice was never LOCKED
        if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
            order.trade_escrow.status = LNPayment.Status.CANCEL
            order.trade_escrow.save()
            return True

    def return_bond(bond):
        """returns a bond"""
        if bond == None:
            return
        try:
            LNNode.cancel_return_hold_invoice(bond.payment_hash)
            bond.status = LNPayment.Status.RETNED
            bond.save()
            return True
        except Exception as e:
            if "invoice already settled" in str(e):
                bond.status = LNPayment.Status.SETLED
                bond.save()
                return True
            else:
                raise e

    def cancel_bond(bond):
        """cancel a bond"""
        # Same as return bond, but used when the invoice was never LOCKED
        if bond == None:
            return True
        try:
            LNNode.cancel_return_hold_invoice(bond.payment_hash)
            bond.status = LNPayment.Status.CANCEL
            bond.save()
            return True
        except Exception as e:
            if "invoice already settled" in str(e):
                bond.status = LNPayment.Status.SETLED
                bond.save()
                return True
            else:
                raise e

    @classmethod
    def confirm_fiat(cls, order, user):
        """If Order is in the CHAT states:
        If user is buyer: fiat_sent goes to true.
        If User is seller and fiat_sent is true: settle the escrow and pay buyer invoice!"""

        if (order.status == Order.Status.CHA
                or order.status == Order.Status.FSE
            ):  # TODO Alternatively, if all collateral is locked? test out

            # If buyer, settle escrow and mark fiat sent
            if cls.is_buyer(order, user):
                order.status = Order.Status.FSE
                order.is_fiat_sent = True

            # If seller and fiat was sent, SETTLE ESCROW AND PAY BUYER INVOICE
            elif cls.is_seller(order, user):
                if not order.is_fiat_sent:
                    return False, {
                        "bad_request":
                        "You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer."
                    }

                # Make sure the trade escrow is at least as big as the buyer invoice
                if order.trade_escrow.num_satoshis <= order.payout.num_satoshis:
                    return False, {
                        "bad_request":
                        "Woah, something broke badly. Report in the public channels, or open a Github Issue."
                    }

                if cls.settle_escrow(
                        order
                ):  ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
                    order.trade_escrow.status = LNPayment.Status.SETLED

                # Double check the escrow is settled.
                if LNNode.double_check_htlc_is_settled(
                        order.trade_escrow.payment_hash):
                    # RETURN THE BONDS // Probably best also do it even if payment failed
                    cls.return_bond(order.taker_bond)
                    cls.return_bond(order.maker_bond)
                    ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
                    ##### Background process "follow_invoices" will try to pay this invoice until success
                    order.status = Order.Status.PAY
                    order.payout.status = LNPayment.Status.FLIGHT
                    order.payout.save()
                    order.save()
                    send_message.delay(order.id,'trade_successful')

                    # Add referral rewards (safe)
                    try:
                        Logics.add_rewards(order)
                    except:
                        pass

                    return True, None

        else:
            return False, {
                "bad_request":
                "You cannot confirm the fiat payment at this stage"
            }

        order.save()
        return True, None

    @classmethod
    def rate_counterparty(cls, order, user, rating):

        rating_allowed_status = [
            Order.Status.PAY,
            Order.Status.SUC,
            Order.Status.FAI,
            Order.Status.MLD,
            Order.Status.TLD,
        ]

        # If the trade is finished
        if order.status in rating_allowed_status:
            # if maker, rates taker
            if order.maker == user and order.maker_rated == False:
                cls.add_profile_rating(order.taker.profile, rating)
                order.maker_rated = True
                order.save()
            # if taker, rates maker
            if order.taker == user and order.taker_rated == False:
                cls.add_profile_rating(order.maker.profile, rating)
                order.taker_rated = True
                order.save()
        else:
            return False, {
                "bad_request": "You cannot rate your counterparty yet."
            }

        return True, None

    @classmethod
    def rate_platform(cls, user, rating):
        user.profile.platform_rating = rating
        user.profile.save()
        return True, None

    @classmethod
    def add_rewards(cls, order):
        '''
        This function is called when a trade is finished. 
        If participants of the order were referred, the reward is given to the referees.
        '''

        if order.maker.profile.is_referred:
            profile = order.maker.profile.referred_by
            profile.pending_rewards += int(config('REWARD_TIP'))
            profile.save()
            
        if order.taker.profile.is_referred:
            profile = order.taker.profile.referred_by
            profile.pending_rewards += int(config('REWARD_TIP'))
            profile.save()

        return

    @classmethod
    def withdraw_rewards(cls, user, invoice):

        # only a user with positive withdraw balance can use this

        if user.profile.earned_rewards < 1:
            return False, {"bad_invoice": "You have not earned rewards"}

        num_satoshis = user.profile.earned_rewards
        reward_payout = LNNode.validate_ln_invoice(invoice, num_satoshis)

        if not reward_payout["valid"]:
            return False, reward_payout["context"]

        lnpayment = LNPayment.objects.create(
            concept= LNPayment.Concepts.WITHREWA,
            type= LNPayment.Types.NORM,
            sender= User.objects.get(username=ESCROW_USERNAME),
            status= LNPayment.Status.VALIDI,
            receiver=user,
            invoice= invoice,
            num_satoshis= num_satoshis,
            description= reward_payout["description"],
            payment_hash= reward_payout["payment_hash"],
            created_at= reward_payout["created_at"],
            expires_at= reward_payout["expires_at"],
        )

        if LNNode.pay_invoice(lnpayment):
            user.profile.earned_rewards = 0
            user.profile.claimed_rewards += num_satoshis
            user.profile.save()

        return True, None