#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2023-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.

"""
Create offers

{
    "min_seconds_between_offers": Add a random delay between creating offers between min and max, default 60.
    "max_seconds_between_offers": ^, default "min_seconds_between_offers" * 4
    "min_seconds_between_bids": Add a random delay between creating bids between min and max, default 60.
    "max_seconds_between_bids": ^, default "min_seconds_between_bids" * 4
    "wallet_port_override": Used for testing.
    "offers": [
        {
            "name": Offer template name, eg "Offer 0", will be automatically renamed if not unique.
            "coin_from": Coin you send.
            "coin_to": Coin you receive.
            "amount": Amount to create the offer for.
            "minrate": Rate below which the offer won't drop.
            "ratetweakpercent": modify the offer rate from the fetched value, can be negative.
            "amount_variable": bool, bidder can set a different amount
            "address": Address offer is sent from, default will generate a new address per offer.
            "min_coin_from_amt": Won't generate offers if the wallet would drop below min_coin_from_amt.
            "offer_valid_seconds": Seconds that the generated offers will be valid for.

            # Optional
            "enabled": Set to false to ignore offer template.
            "swap_type": Type of swap, defaults to "adaptor_sig"
            "min_swap_amount": Sets "amt_bid_min" on the offer, minimum valid bid when offer amount is variable.
            "amount_step": If set offers will be created for amount values between "amount" and "min_coin_from_amt" in decrements of "amount_step".
        },
        ...
    ],
    "bids": [
        {
            "name": Bid template name, must be unique, eg "Bid 0", will be automatically renamed if not unique.
            "coin_from": Coin you receive.
            "coin_to": Coin you send.
            "amount": amount to bid.
            "max_rate": Maximum rate for bids.
            "min_coin_to_balance": Won't send bids if wallet amount of "coin_to" would drop below.

            # Optional
            "enabled": Set to false to ignore bid template.
            "max_concurrent": Maximum number of bids to have active at once, default 1.
            "amount_variable": Can send bids below the set "amount" where possible if true.
            "max_coin_from_balance": Won't send bids if wallet amount of "coin_from" would be above.
            "address": Address offer is sent from, default will generate a new address per bid.
        },
        ...
    ]
}

"""

__version__ = "0.2"

import os
import json
import time
import random
import shutil
import signal
import urllib
import logging
import argparse
import threading
from urllib.request import urlopen

delay_event = threading.Event()

DEFAULT_CONFIG_FILE: str = "createoffers.json"
DEFAULT_STATE_FILE: str = "createoffers_state.json"


def post_req(url: str, json_data=None):
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    if json_data:
        req.add_header("Content-Type", "application/json; charset=utf-8")
        post_bytes = json.dumps(json_data).encode("utf-8")
        req.add_header("Content-Length", len(post_bytes))
    else:
        post_bytes = None
    return urlopen(req, data=post_bytes, timeout=300).read()


def make_json_api_func(host: str, port: int):
    host = host
    port = port

    def api_func(path=None, json_data=None, timeout=300):
        nonlocal host, port
        url = f"http://{host}:{port}/json"
        if path is not None:
            url += "/" + path
        if json_data is not None:
            return json.loads(post_req(url, json_data))
        response = urlopen(url, timeout=300).read()
        return json.loads(response)

    return api_func


def signal_handler(sig, frame) -> None:
    logging.info("Signal {} detected.".format(sig))
    delay_event.set()


def findCoin(coin: str, known_coins) -> str:
    for known_coin in known_coins:
        if (
            known_coin["name"].lower() == coin.lower()
            or known_coin["ticker"].lower() == coin.lower()
        ):
            if known_coin["active"] is False:
                raise ValueError(f"Inactive coin {coin}")
            return known_coin["name"]
    raise ValueError(f"Unknown coin {coin}")


def readConfig(args, known_coins):
    config_path: str = args.configfile
    num_changes: int = 0
    with open(config_path) as fs:
        config = json.load(fs)

    if "offers" not in config:
        config["offers"] = []
    if "bids" not in config:
        config["bids"] = []

    if "min_seconds_between_offers" not in config:
        config["min_seconds_between_offers"] = 60
        print("Set min_seconds_between_offers", config["min_seconds_between_offers"])
        num_changes += 1
    if "max_seconds_between_offers" not in config:
        config["max_seconds_between_offers"] = config["min_seconds_between_offers"] * 4
        print("Set max_seconds_between_offers", config["max_seconds_between_offers"])
        num_changes += 1

    if "min_seconds_between_bids" not in config:
        config["min_seconds_between_bids"] = 60
        print("Set min_seconds_between_bids", config["min_seconds_between_bids"])
        num_changes += 1
    if "max_seconds_between_bids" not in config:
        config["max_seconds_between_bids"] = config["min_seconds_between_bids"] * 4
        print("Set max_seconds_between_bids", config["max_seconds_between_bids"])
        num_changes += 1

    offer_templates = config["offers"]
    offer_templates_map = {}
    num_enabled = 0
    for i, offer_template in enumerate(offer_templates):
        num_enabled += 1 if offer_template.get("enabled", True) else 0
        if "name" not in offer_template:
            print("Naming offer template", i)
            offer_template["name"] = f"Offer {i}"
            num_changes += 1
        if offer_template["name"] in offer_templates_map:
            print("Renaming offer template", offer_template["name"])
            original_name = offer_template["name"]
            offset = 2
            while f"{original_name}_{offset}" in offer_templates_map:
                offset += 1
            offer_template["name"] = f"{original_name}_{offset}"
            num_changes += 1
        offer_templates_map[offer_template["name"]] = offer_template

        if "amount_step" not in offer_template:
            if offer_template.get("min_coin_from_amt", 0) < offer_template["amount"]:
                print("Setting min_coin_from_amt for", offer_template["name"])
                offer_template["min_coin_from_amt"] = offer_template["amount"]
                num_changes += 1
        else:
            if "min_coin_from_amt" not in offer_template:
                print("Setting min_coin_from_amt for", offer_template["name"])
                offer_template["min_coin_from_amt"] = 0
                num_changes += 1

        if "address" not in offer_template:
            print("Setting address to auto for offer", offer_template["name"])
            offer_template["address"] = "auto"
            num_changes += 1
        if "ratetweakpercent" not in offer_template:
            print("Setting ratetweakpercent to 0 for offer", offer_template["name"])
            offer_template["ratetweakpercent"] = 0
            num_changes += 1
        if "amount_variable" not in offer_template:
            print("Setting amount_variable to True for offer", offer_template["name"])
            offer_template["amount_variable"] = True
            num_changes += 1

        if offer_template.get("enabled", True) is False:
            continue
        offer_template["coin_from"] = findCoin(offer_template["coin_from"], known_coins)
        offer_template["coin_to"] = findCoin(offer_template["coin_to"], known_coins)
    config["num_enabled_offers"] = num_enabled

    bid_templates = config["bids"]
    bid_templates_map = {}
    num_enabled = 0
    for i, bid_template in enumerate(bid_templates):
        num_enabled += 1 if bid_template.get("enabled", True) else 0
        if "name" not in bid_template:
            print("Naming bid template", i)
            bid_template["name"] = f"Bid {i}"
            num_changes += 1
        if bid_template["name"] in bid_templates_map:
            print("Renaming bid template", bid_template["name"])
            original_name = bid_template["name"]
            offset = 2
            while f"{original_name}_{offset}" in bid_templates_map:
                offset += 1
            bid_template["name"] = f"{original_name}_{offset}"
            num_changes += 1
        bid_templates_map[bid_template["name"]] = bid_template

        if bid_template.get("min_swap_amount", 0.0) < 0.001:
            print("Setting min_swap_amount for bid template", bid_template["name"])
            bid_template["min_swap_amount"] = 0.001

        if "address" not in bid_template:
            print("Setting address to auto for bid", bid_template["name"])
            bid_template["address"] = "auto"
            num_changes += 1

        if bid_template.get("enabled", True) is False:
            continue
        bid_template["coin_from"] = findCoin(bid_template["coin_from"], known_coins)
        bid_template["coin_to"] = findCoin(bid_template["coin_to"], known_coins)
    config["num_enabled_bids"] = num_enabled

    if num_changes > 0:
        shutil.copyfile(config_path, config_path + ".last")
        with open(config_path, "w") as fp:
            json.dump(config, fp, indent=4)

    return config


def write_state(statefile, script_state):
    if os.path.exists(statefile):
        shutil.copyfile(statefile, statefile + ".last")
    with open(statefile, "w") as fp:
        json.dump(script_state, fp, indent=4)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "-v",
        "--version",
        action="version",
        version="%(prog)s {version}".format(version=__version__),
    )
    parser.add_argument(
        "--host",
        dest="host",
        help="RPC host (default=127.0.0.1)",
        type=str,
        default="127.0.0.1",
        required=False,
    )
    parser.add_argument(
        "--port",
        dest="port",
        help="RPC port (default=12700)",
        type=int,
        default=12700,
        required=False,
    )
    parser.add_argument(
        "--oneshot",
        dest="oneshot",
        help="Exit after one iteration (default=false)",
        required=False,
        action="store_true",
    )
    parser.add_argument(
        "--debug",
        dest="debug",
        help="Print extra debug messages (default=false)",
        required=False,
        action="store_true",
    )
    parser.add_argument(
        "--configfile",
        dest="configfile",
        help=f"config file path (default={DEFAULT_CONFIG_FILE})",
        type=str,
        default=DEFAULT_CONFIG_FILE,
        required=False,
    )
    parser.add_argument(
        "--statefile",
        dest="statefile",
        help=f"state file path (default={DEFAULT_STATE_FILE})",
        type=str,
        default=DEFAULT_STATE_FILE,
        required=False,
    )
    args = parser.parse_args()

    read_json_api = make_json_api_func(args.host, args.port)

    if not os.path.exists(args.configfile):
        raise ValueError(f'Config file "{args.configfile}" not found.')

    known_coins = read_json_api("coins")
    coins_map = {}
    for known_coin in known_coins:
        coins_map[known_coin["name"]] = known_coin

    script_state = {}
    if os.path.exists(args.statefile):
        with open(args.statefile) as fs:
            script_state = json.load(fs)

    signal.signal(signal.SIGINT, signal_handler)
    while not delay_event.is_set():
        # Read config each iteration so they can be modified without restarting
        config = readConfig(args, known_coins)
        offer_templates = config["offers"]
        random.shuffle(offer_templates)

        bid_templates = config["bids"]
        random.shuffle(bid_templates)

        # override wallet api calls for testing
        if "wallet_port_override" in config:
            wallet_api_port = int(config["wallet_port_override"])
            print(f"Overriding wallet api port: {wallet_api_port}")
            read_json_api_wallet = make_json_api_func(args.host, wallet_api_port)
        else:
            read_json_api_wallet = read_json_api

        try:
            sent_offers = read_json_api("sentoffers", {"active": "active"})

            if args.debug and len(offer_templates) > 0:
                print(
                    "Processing {} offer template{}".format(
                        config["num_enabled_offers"],
                        "s" if config["num_enabled_offers"] != 1 else "",
                    )
                )
            for offer_template in offer_templates:
                if offer_template.get("enabled", True) is False:
                    continue
                offers_found = 0

                coin_from_data = coins_map[offer_template["coin_from"]]
                coin_to_data = coins_map[offer_template["coin_to"]]

                wallet_from = read_json_api_wallet(
                    "wallets/{}".format(coin_from_data["ticker"])
                )
                coin_ticker = coin_from_data["ticker"]
                if coin_ticker == "PART" and "variant" in coin_from_data:
                    coin_variant = coin_from_data["variant"]
                    if coin_variant == "Anon":
                        coin_from_data_name = "PART_ANON"
                        wallet_balance: float = float(wallet_from["anon_balance"])
                    elif coin_variant == "Blind":
                        coin_from_data_name = "PART_BLIND"
                        wallet_balance: float = float(wallet_from["blind_balance"])
                    else:
                        raise ValueError(
                            f"{coin_ticker} variant {coin_variant} not handled"
                        )
                else:
                    coin_from_data_name = coin_ticker
                    wallet_balance: float = float(wallet_from["balance"])

                for offer in sent_offers:
                    created_offers = script_state.get("offers", {})
                    prev_template_offers = created_offers.get(
                        offer_template["name"], {}
                    )

                    if next(
                        (
                            x
                            for x in prev_template_offers
                            if x["offer_id"] == offer["offer_id"]
                        ),
                        None,
                    ):
                        offers_found += 1
                        if wallet_balance <= float(offer_template["min_coin_from_amt"]):
                            offer_id = offer["offer_id"]
                            print(
                                "Revoking offer {}, wallet from balance below minimum".format(
                                    offer_id
                                )
                            )
                            result = read_json_api(f"revokeoffer/{offer_id}")
                            print("revokeoffer", result)

                if offers_found > 0:
                    continue

                max_offer_amount: float = offer_template["amount"]
                min_offer_amount: float = offer_template.get(
                    "amount_step", max_offer_amount
                )

                min_wallet_from_amount: float = float(
                    offer_template["min_coin_from_amt"]
                )
                if wallet_balance - min_offer_amount <= min_wallet_from_amount:
                    print(
                        "Skipping template {}, wallet from balance below minimum".format(
                            offer_template["name"]
                        )
                    )
                    continue

                offer_amount: float = max_offer_amount
                if wallet_balance - max_offer_amount <= min_wallet_from_amount:
                    available_balance: float = wallet_balance - min_wallet_from_amount
                    min_steps: int = available_balance // min_offer_amount
                    assert min_steps > 0  # Should not be possible, checked above
                    offer_amount = min_offer_amount * min_steps

                delay_next_offer_before = script_state.get("delay_next_offer_before", 0)
                if delay_next_offer_before > int(time.time()):
                    print("Delaying offers until {}".format(delay_next_offer_before))
                    break

                """
                received_offers = read_json_api(args.port, 'offers', {'active': 'active', 'include_sent': False, 'coin_from': coin_from_data['id'], 'coin_to': coin_to_data['id']})
                print('received_offers', received_offers)

                TODO - adjust rates based on existing offers
                """

                rates = read_json_api(
                    "rates",
                    {"coin_from": coin_from_data["id"], "coin_to": coin_to_data["id"]},
                )
                print("Rates", rates)
                coingecko_rate = float(rates["coingecko"]["rate_inferred"])
                use_rate = coingecko_rate

                if offer_template["ratetweakpercent"] != 0:
                    print(
                        "Adjusting rate {} by {}%.".format(
                            use_rate, offer_template["ratetweakpercent"]
                        )
                    )
                    tweak = offer_template["ratetweakpercent"] / 100.0
                    use_rate += use_rate * tweak

                if use_rate < offer_template["minrate"]:
                    print("Warning: Clamping rate to minimum.")
                    use_rate = offer_template["minrate"]

                print(
                    "Creating offer for: {} at rate: {}".format(
                        offer_template, use_rate
                    )
                )
                template_from_addr = offer_template["address"]
                offer_data = {
                    "addr_from": (
                        -1 if template_from_addr == "auto" else template_from_addr
                    ),
                    "coin_from": coin_from_data_name,
                    "coin_to": coin_to_data["ticker"],
                    "amt_from": offer_amount,
                    "amt_var": offer_template["amount_variable"],
                    "valid_for_seconds": offer_template.get(
                        "offer_valid_seconds", config.get("offer_valid_seconds", 3600)
                    ),
                    "rate": use_rate,
                    "swap_type": offer_template.get("swap_type", "adaptor_sig"),
                    "lockhrs": "24",
                    "automation_strat_id": 1,
                }
                if "min_swap_amount" in offer_template:
                    offer_data["amt_bid_min"] = offer_template["min_swap_amount"]
                if args.debug:
                    print("offer data {}".format(offer_data))
                new_offer = read_json_api("offers/new", offer_data)
                if "error" in new_offer:
                    raise ValueError(
                        "Server failed to create offer: {}".format(new_offer["error"])
                    )
                print("New offer: {}".format(new_offer["offer_id"]))
                if "offers" not in script_state:
                    script_state["offers"] = {}
                template_name = offer_template["name"]
                if template_name not in script_state["offers"]:
                    script_state["offers"][template_name] = []
                script_state["offers"][template_name].append(
                    {"offer_id": new_offer["offer_id"], "time": int(time.time())}
                )
                max_seconds_between_offers = config["max_seconds_between_offers"]
                min_seconds_between_offers = config["min_seconds_between_offers"]
                time_between_offers = min_seconds_between_offers
                if max_seconds_between_offers > min_seconds_between_offers:
                    time_between_offers = random.randint(
                        min_seconds_between_offers, max_seconds_between_offers
                    )

                script_state["delay_next_offer_before"] = (
                    int(time.time()) + time_between_offers
                )
                write_state(args.statefile, script_state)

            if args.debug and len(bid_templates) > 0:
                print(
                    "Processing {} bid template{}".format(
                        config["num_enabled_bids"],
                        "s" if config["num_enabled_bids"] != 1 else "",
                    )
                )
            for bid_template in bid_templates:
                if bid_template.get("enabled", True) is False:
                    continue
                delay_next_bid_before = script_state.get("delay_next_bid_before", 0)
                if delay_next_bid_before > int(time.time()):
                    print("Delaying bids until {}".format(delay_next_bid_before))
                    break

                # Check bids in progress
                max_concurrent = bid_template.get("max_concurrent", 1)
                if "bids" not in script_state:
                    script_state["bids"] = {}
                template_name = bid_template["name"]
                if template_name not in script_state["bids"]:
                    script_state["bids"][template_name] = []
                previous_bids = script_state["bids"][template_name]

                bids_in_progress: int = 0
                for previous_bid in previous_bids:
                    if not previous_bid["active"]:
                        continue
                    previous_bid_id = previous_bid["bid_id"]
                    previous_bid_info = read_json_api(f"bids/{previous_bid_id}")
                    bid_state = previous_bid_info["bid_state"]
                    if bid_state in (
                        "Completed",
                        "Timed-out",
                        "Abandoned",
                        "Error",
                        "Rejected",
                    ):
                        print(
                            f"Marking bid inactive {previous_bid_id}, state {bid_state}"
                        )
                        previous_bid["active"] = False
                        write_state(args.statefile, script_state)
                        continue
                    if bid_state in ("Sent", "Received") and previous_bid_info[
                        "expired_at"
                    ] < int(time.time()):
                        print(f"Marking bid inactive {previous_bid_id}, expired")
                        previous_bid["active"] = False
                        write_state(args.statefile, script_state)
                        continue
                    bids_in_progress += 1

                if bids_in_progress >= max_concurrent:
                    print("Max concurrent bids reached for template")
                    continue

                # Bidder sends coin_to and receives coin_from
                coin_from_data = coins_map[bid_template["coin_from"]]
                coin_to_data = coins_map[bid_template["coin_to"]]

                page_limit: int = 25
                offers_options = {
                    "active": "active",
                    "include_sent": False,
                    "coin_from": coin_from_data["id"],
                    "coin_to": coin_to_data["id"],
                    "with_extra_info": True,
                    "sort_by": "rate",
                    "sort_dir": "asc",
                    "offset": 0,
                    "limit": page_limit,
                }

                received_offers = []
                for i in range(1000000):  # for i in itertools.count()
                    page_offers = read_json_api("offers", offers_options)
                    if len(page_offers) < 1:
                        break
                    received_offers += page_offers
                    offers_options["offset"] = offers_options["offset"] + page_limit
                    if i > 100:
                        print(f"Warning: Broke offers loop at: {i}")
                        break

                if args.debug:
                    print("Received Offers", received_offers)

                for offer in received_offers:
                    offer_id = offer["offer_id"]
                    offer_amount = float(offer["amount_from"])
                    offer_rate = float(offer["rate"])
                    bid_amount = bid_template["amount"]

                    min_swap_amount = bid_template.get(
                        "min_swap_amount", 0.01
                    )  # TODO: Make default vary per coin
                    can_adjust_offer_amount: bool = offer["amount_negotiable"]
                    can_adjust_bid_amount: bool = bid_template.get(
                        "amount_variable", True
                    )
                    can_adjust_amount: bool = (
                        can_adjust_offer_amount and can_adjust_bid_amount
                    )

                    if offer_amount < min_swap_amount:
                        if args.debug:
                            print(f"Offer amount below min swap amount bid {offer_id}")
                        continue

                    if can_adjust_offer_amount is False and offer_amount > bid_amount:
                        if args.debug:
                            print(f"Bid amount too low for offer {offer_id}")
                        continue

                    if bid_amount > offer_amount:
                        if can_adjust_bid_amount:
                            bid_amount = offer_amount
                        else:
                            if args.debug:
                                print(f"Bid amount too high for offer {offer_id}")
                            continue

                    if offer_rate > bid_template["maxrate"]:
                        if args.debug:
                            print(f"Bid rate too low for offer {offer_id}")
                        continue

                    sent_bids = read_json_api(
                        "sentbids",
                        {
                            "offer_id": offer["offer_id"],
                            "with_available_or_active": True,
                        },
                    )
                    if len(sent_bids) > 0:
                        if args.debug:
                            print(f"Already bidding on offer {offer_id}")
                        continue

                    offer_identity = read_json_api(
                        "identities/{}".format(offer["addr_from"])
                    )
                    if len(offer_identity) > 0:
                        id_offer_from = offer_identity[0]
                        automation_override = id_offer_from["automation_override"]
                        if automation_override == 2:
                            if args.debug:
                                print(
                                    f"Not bidding on offer {offer_id}, automation_override ({automation_override})."
                                )
                            continue
                        if automation_override == 1:
                            if args.debug:
                                print(
                                    "Offer address from {}, set to always accept.".format(
                                        offer["addr_from"]
                                    )
                                )
                        else:
                            successful_sent_bids = id_offer_from[
                                "num_sent_bids_successful"
                            ]
                            failed_sent_bids = id_offer_from["num_sent_bids_failed"]
                            if (
                                failed_sent_bids > 3
                                and failed_sent_bids > successful_sent_bids
                            ):
                                if args.debug:
                                    print(
                                        f"Not bidding on offer {offer_id}, too many failed bids ({failed_sent_bids})."
                                    )
                                continue

                    validateamount: bool = False
                    max_coin_from_balance = bid_template.get(
                        "max_coin_from_balance", -1
                    )
                    if max_coin_from_balance > 0:
                        wallet_from = read_json_api_wallet(
                            "wallets/{}".format(coin_from_data["ticker"])
                        )
                        total_balance_from = float(wallet_from["balance"]) + float(
                            wallet_from["unconfirmed"]
                        )
                        if args.debug:
                            print(f"Total coin from balance {total_balance_from}")
                        if total_balance_from + bid_amount > max_coin_from_balance:
                            if (
                                can_adjust_amount
                                and max_coin_from_balance - total_balance_from
                                > min_swap_amount
                            ):
                                bid_amount = max_coin_from_balance - total_balance_from
                                validateamount = True
                                print(f"Reduced bid amount to {bid_amount}")
                            else:
                                if args.debug:
                                    print(
                                        f"Bid amount would exceed maximum wallet total for offer {offer_id}"
                                    )
                                continue

                    min_coin_to_balance = bid_template["min_coin_to_balance"]
                    if min_coin_to_balance > 0:
                        wallet_to = read_json_api_wallet(
                            "wallets/{}".format(coin_to_data["ticker"])
                        )

                        total_balance_to = float(wallet_to["balance"]) + float(
                            wallet_to["unconfirmed"]
                        )
                        if args.debug:
                            print(f"Total coin to balance {total_balance_to}")

                        swap_amount_to = bid_amount * offer_rate
                        if total_balance_to - swap_amount_to < min_coin_to_balance:
                            if can_adjust_amount:
                                adjusted_swap_amount_to = (
                                    total_balance_to - min_coin_to_balance
                                )
                                adjusted_bid_amount = (
                                    adjusted_swap_amount_to / offer_rate
                                )

                                if adjusted_bid_amount > min_swap_amount:
                                    bid_amount = adjusted_bid_amount
                                    validateamount = True
                                    print(f"Reduced bid amount to {bid_amount}")
                                    swap_amount_to = adjusted_bid_amount * offer_rate

                        if total_balance_to - swap_amount_to < min_coin_to_balance:
                            if args.debug:
                                print(
                                    f"Bid amount would exceed minimum coin to wallet total for offer {offer_id}"
                                )
                            continue

                    if validateamount:
                        bid_amount = read_json_api(
                            "validateamount",
                            {
                                "coin": coin_from_data["ticker"],
                                "amount": bid_amount,
                                "method": "rounddown",
                            },
                        )
                    bid_data = {
                        "offer_id": offer["offer_id"],
                        "amount_from": bid_amount,
                    }

                    if "address" in bid_template:
                        addr_from = bid_template["address"]
                        if addr_from != -1 and addr_from != "auto":
                            bid_data["addr_from"] = addr_from

                    if config.get("test_mode", False):
                        print("Would create bid: {}".format(bid_data))
                        bid_id = "simulated"
                    else:
                        if args.debug:
                            print("Creating bid: {}".format(bid_data))
                        new_bid = read_json_api("bids/new", bid_data)
                        if "error" in new_bid:
                            raise ValueError(
                                "Server failed to create bid: {}".format(
                                    new_bid["error"]
                                )
                            )
                        print(
                            "New bid: {} on offer {}".format(
                                new_bid["bid_id"], offer["offer_id"]
                            )
                        )
                        bid_id = new_bid["bid_id"]

                    script_state["bids"][template_name].append(
                        {"bid_id": bid_id, "time": int(time.time()), "active": True}
                    )

                    max_seconds_between_bids = config["max_seconds_between_bids"]
                    min_seconds_between_bids = config["min_seconds_between_bids"]
                    if max_seconds_between_bids > min_seconds_between_bids:
                        time_between_bids = random.randint(
                            min_seconds_between_bids, max_seconds_between_bids
                        )
                    else:
                        time_between_bids = min_seconds_between_bids
                    script_state["delay_next_bid_before"] = (
                        int(time.time()) + time_between_bids
                    )
                    write_state(args.statefile, script_state)
                    break  # Create max one bid per iteration

        except Exception as e:
            print(f"Error: {e}.")

        if args.oneshot:
            break
        print("Looping indefinitely, ctrl+c to exit.")
        delay_event.wait(60)

    print("Done.")


if __name__ == "__main__":
    main()