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

# Copyright (c) 2023-2024 tecnovert
# Copyright (c) 2024-2025 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.
    "prune_state_delay": Seconds between pruning old state data, set to 0 to disable pruning.
    "main_loop_delay": Seconds between main loop iterations.
    "prune_state_after_seconds": Seconds to keep old state data for.
    "auth": Basicswap API auth string, e.g., "admin:password". Ignored if client auth is not enabled.
    "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.
            "automation_strategy": Auto accept bids setting - "accept_all", "accept_known", or "none" (default: "accept_all")
            "adjust_rates_based_on_market": Per-offer setting to adjust rates based on existing market offers (default: false)
            "amount_step": REQUIRED - Offer size increment for privacy. Must be between 0.001 and "amount".
                          This prevents revealing exact wallet balance by creating stepped offers.
                          Example: 150 LTC balance, 100 LTC offer, 5 LTC increment, 76 LTC min balance = creates 70 LTC offer.
                          Without this, partial fills would reveal your exact remaining balance.

            # 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 purchase quantity when offer amount is variable.
        },
        ...
    ],
    "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.
            "offers_to_bid_on": Which offers to bid on - "all", "auto_accept_only", or "known_only" (default: "all")

            # 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.
            "use_balance_bidding": If true, calculates bid amount as (wallet_balance - offer_min_amount) instead of using template amount.
        },
        ...
    ]
}

"""

__version__ = "0.4"

import argparse
import json
import os
import random
import shutil
import signal
import sys
import threading
import time
import traceback
import urllib
import urllib.error
import base64
from urllib.request import urlopen

delay_event = threading.Event()
shutdown_in_progress = False
coins_map = {}
read_json_api = None
read_json_api_wallet = None

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


def post_req(url: str, json_data=None, auth_header_val=None):
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    if auth_header_val:
        req.add_header("Authorization", auth_header_val)
    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)


def make_json_api_func(host: str, port: int, auth_string: str = None):
    host = host
    port = port
    _auth_header_val = None
    _auth_required_confirmed = False
    if auth_string:
        try:
            if auth_string and ":" in auth_string:
                try:
                    auth_bytes = auth_string.encode("utf-8")
                    _auth_header_val = "Basic " + base64.b64encode(auth_bytes).decode(
                        "ascii"
                    )
                except Exception as e:
                    print(
                        f"Warning: Could not process auth string '{auth_string}': {e}"
                    )
                    _auth_header_val = None
            elif auth_string:
                print(
                    "Warning: Auth string is not in 'username:password' format. Ignoring."
                )
        except Exception as e:
            print(f"Error processing authentication: {e}")

    def api_func(path=None, json_data=None, timeout=300):
        nonlocal _auth_required_confirmed
        url = f"http://{host}:{port}/json"
        if path is not None:
            url += "/" + path

        current_auth_header = _auth_header_val if _auth_required_confirmed else None

        try:
            if json_data is not None:
                response_obj = post_req(
                    url, json_data, auth_header_val=current_auth_header
                )
            else:
                headers = {"User-Agent": "Mozilla/5.0"}
                if current_auth_header:
                    headers["Authorization"] = current_auth_header
                req = urllib.request.Request(url, headers=headers)
                response_obj = urlopen(req, timeout=timeout)

            response_bytes = response_obj.read()
            return json.loads(response_bytes)

        except urllib.error.HTTPError as e:
            if e.code == 401 and not _auth_required_confirmed:
                if _auth_header_val:
                    print(
                        "Server requires authentication, retrying with credentials..."
                    )
                    _auth_required_confirmed = True
                    try:
                        if json_data is not None:
                            response_obj = post_req(
                                url, json_data, auth_header_val=_auth_header_val
                            )
                        else:
                            headers = {
                                "User-Agent": "Mozilla/5.0",
                                "Authorization": _auth_header_val,
                            }
                            req = urllib.request.Request(url, headers=headers)
                            response_obj = urlopen(req, timeout=timeout)
                        response_bytes = response_obj.read()
                        return json.loads(response_bytes)
                    except urllib.error.HTTPError as retry_e:
                        if retry_e.code == 401:
                            raise ValueError(
                                "Authentication failed: Invalid credentials provided in 'auth' key."
                            )
                        else:
                            print(f"Error during authenticated API request: {retry_e}")
                            raise retry_e
                    except Exception as retry_e:
                        print(f"Error during authenticated API request: {retry_e}")
                        raise retry_e
                else:
                    raise ValueError(
                        "Server requires authentication (401), but no 'auth' key found or properly formatted in config file."
                    )
            else:
                if e.code == 401 and _auth_required_confirmed:
                    raise ValueError(
                        "Authentication failed: Invalid credentials provided in 'auth' key."
                    )
                else:
                    raise e
        except Exception as e:
            print(f"Error during API connection: {e}")
            raise e

    return api_func


def signal_handler(sig, _) -> None:
    global shutdown_in_progress
    os.write(
        sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
    )
    shutdown_in_progress = True
    delay_event.set()


def findCoin(coin: str, known_coins) -> str:
    if coin.lower() in ["particl anon", "particl_anon", "part_anon"]:
        return "Particl Anon"
    if coin.lower() in ["particl blind", "particl_blind", "part_blind"]:
        return "Particl Blind"
    if coin.lower() in ["particl", "part"]:
        return "Particl"

    # Regular coin lookup
    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"]

    print(f"Warning: Unknown coin {coin}, using as-is")
    return coin


def readConfig(args, known_coins):
    config_path: str = args.configfile
    num_changes: int = 0
    min_swap_size = 0.001

    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"] = 15
        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"] = 15
        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 "enabled" not in offer_template:
            offer_template["enabled"] = True
            num_changes += 1
        elif offer_template["enabled"] is not True:
            continue
        if "amount" not in offer_template:
            offer_template["amount"] = 0
            print(f"{offer_template['name']} Offer amount missing. Skipping offer")
            num_changes += 1
            continue
        elif (
            offer_template["amount"] is None
            or float(offer_template["amount"]) < min_swap_size
        ):
            print(f"{offer_template['name']} Offer amount invalid. Skipping offer")
            continue
        amount = float(offer_template["amount"])
        if "minrate" not in offer_template or offer_template["minrate"] is None:
            offer_template["minrate"] = 0
            print(f"{offer_template['name']} Offer minrate missing. Setting to 0")
            num_changes += 1
        if "amount_step" not in offer_template:
            print(
                f"Adding mandatory amount_step for {offer_template['name']} (privacy feature)"
            )
            offer_template["amount_step"] = min_swap_size
            num_changes += 1
        amount_step = float(offer_template["amount_step"])
        if amount_step < min_swap_size:
            print(
                f"Invalid amount_step for {offer_template['name']}: must be >= {min_swap_size}, setting to {min_swap_size}"
            )
            offer_template["amount_step"] = min_swap_size
            num_changes += 1
        elif amount_step > amount:
            print(
                f"Invalid amount_step for {offer_template['name']}: must be <= amount ({amount}), setting to {amount}"
            )
            offer_template["amount_step"] = amount
            num_changes += 1
        if (
            "min_swap_amount" not in offer_template
            or float(offer_template["min_swap_amount"]) < min_swap_size
        ):
            print("Setting min_swap_amount for", offer_template["name"])
            offer_template["min_swap_amount"] = min_swap_size
            num_changes += 1
        elif float(offer_template["min_swap_amount"]) > amount:
            print(
                f"min_swap_amount cannot be larger than offer amount ({amount}) Setting min_swap_amount for",
                offer_template["name"],
                f"to {amount}",
            )
            offer_template["min_swap_amount"] = amount
            num_changes += 1
        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) < min_swap_size:
            print("Setting min_swap_amount for bid template", bid_template["name"])
            bid_template["min_swap_amount"] = min_swap_size

        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

    config["main_loop_delay"] = config.get("main_loop_delay", 60)
    if config["main_loop_delay"] < 10:
        print("Setting main_loop_delay to 10")
        config["main_loop_delay"] = 10
        num_changes += 1
    if config["main_loop_delay"] > 1000:
        print("Setting main_loop_delay to 1000")
        config["main_loop_delay"] = 1000
        num_changes += 1
    config["prune_state_delay"] = config.get("prune_state_delay", 120)

    # Add market-based rate adjustment option (default: false)
    # When enabled, the script will analyze existing offers on the orderbook
    # and adjust rates based on your ratetweakpercent
    if "adjust_rates_based_on_market" not in config:
        config["adjust_rates_based_on_market"] = False
        print("Setting global adjust_rates_based_on_market to False")
        num_changes += 1

    seconds_in_day: int = 86400
    config["prune_state_after_seconds"] = config.get(
        "prune_state_after_seconds", seconds_in_day * 7
    )
    if config["prune_state_after_seconds"] < seconds_in_day:
        print(f"Setting prune_state_after_seconds to {seconds_in_day}")
        config["prune_state_after_seconds"] = seconds_in_day
        num_changes += 1

    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 attempt_pre_offer_bids(
    offer_template, use_rate, amount_to_offer, coin_from_data, coin_to_data, debug=False
):
    if not offer_template.get("attempt_bids_first", False):
        return amount_to_offer

    if debug:
        print(
            f"Attempting to fill existing offers before creating {offer_template['name']}"
        )

    try:
        # Find existing offers we could bid on (reverse direction)
        existing_offers = read_json_api(
            "offers",
            {
                "active": "active",
                "include_sent": False,
                "coin_from": coin_to_data["id"],  # We want to buy what we're offering
                "coin_to": coin_from_data["id"],  # We want to sell what we're buying
                "with_extra_info": True,
            },
        )

        if not isinstance(existing_offers, list):
            if debug:
                print(f"Invalid offers response for bidding: {existing_offers}")
            return amount_to_offer

        if not existing_offers:
            if debug:
                print("No existing offers found to bid on")
            return amount_to_offer

        # Filter offers based on bidding strategy
        biddable_offers = filter_biddable_offers(
            existing_offers, offer_template, use_rate, debug
        )

        if not biddable_offers:
            if debug:
                print("No suitable offers found for bidding")
            return amount_to_offer

        # Calculate and execute bids
        total_filled = execute_bid_plan(
            biddable_offers, offer_template, amount_to_offer, use_rate, debug
        )

        remaining_amount = max(0, amount_to_offer - total_filled)

        if debug:
            print(
                f"Filled {total_filled} via bids, remaining for AMM offer: {remaining_amount}"
            )

        return remaining_amount

    except Exception as e:
        if debug:
            print(f"Error in pre-offer bidding: {e}")
        return amount_to_offer


def filter_biddable_offers(offers, offer_template, our_rate, debug=False):
    """Filter offers based on bidding criteria"""
    strategy = offer_template.get("bid_strategy", "balanced")
    rate_tolerance = offer_template.get("bid_rate_tolerance", 2.0)

    filtered = []
    for offer in offers:
        try:
            offer_rate = float(offer["rate"])

            # Check if offer rate is better than or close to our rate
            max_acceptable_rate = our_rate * (1 + rate_tolerance / 100)

            if offer_rate <= max_acceptable_rate:
                # Apply strategy-specific filtering
                if should_bid_on_offer(offer, strategy):
                    filtered.append(offer)

        except (ValueError, KeyError) as e:
            if debug:
                print(f"Error processing offer for bidding: {e}")
            continue

    # Sort by best rate first (lowest rate = best for us when buying)
    filtered.sort(key=lambda x: float(x["rate"]))

    if debug:
        print(f"Found {len(filtered)} biddable offers using {strategy} strategy")

    return filtered


def execute_bid_plan(offers, offer_template, total_amount, our_rate, debug=False):
    """Calculate and execute optimal bid amounts"""
    max_bid_percentage = offer_template.get("max_bid_percentage", 50)
    max_bid_amount = total_amount * (max_bid_percentage / 100)

    total_filled = 0
    remaining = min(max_bid_amount, total_amount)

    for offer in offers:
        if remaining <= 0:
            break

        try:
            offer_amount = float(offer.get("amount_from", 0))
            offer_rate = float(offer["rate"])
            min_bid_amount = offer_template.get("min_bid_amount", 0.001)

            # Calculate how much we can bid on this offer
            # We're buying coin_to, so we need to calculate based on their offer_amount
            max_we_can_buy = remaining
            max_they_offer = offer_amount

            bid_amount = min(max_we_can_buy, max_they_offer)

            if bid_amount >= min_bid_amount:
                # Attempt to place the bid
                if place_bid_on_offer(offer, bid_amount, offer_template, debug):
                    total_filled += bid_amount
                    remaining -= bid_amount
                    if debug:
                        print(
                            f"Successfully bid {bid_amount} on offer {offer.get('offer_id', 'unknown')} at rate {offer_rate}"
                        )
                else:
                    if debug:
                        print(
                            f"Failed to place bid on offer {offer.get('offer_id', 'unknown')}"
                        )

        except (ValueError, KeyError) as e:
            if debug:
                print(f"Error calculating bid amount: {e}")
            continue

    return total_filled


def place_bid_on_offer(offer, bid_amount, offer_template, debug=False):
    """Place a bid on a specific offer"""
    try:
        bid_data = {
            "offer_id": offer.get("offer_id"),
            "amount_from": str(bid_amount),
            "bid_rate": offer["rate"],  # Use the offer's rate
        }

        # Add optional bid settings
        if "bid_valid_seconds" in offer_template:
            bid_data["valid_for_seconds"] = str(offer_template["bid_valid_seconds"])

        response = read_json_api("bids", bid_data)

        if response and response.get("bid_id"):
            return True
        else:
            if debug:
                print(f"Bid placement failed: {response}")
            return False

    except Exception as e:
        if debug:
            print(f"Error placing bid: {e}")
        return False


def should_bid_on_offer(offer, strategy):
    """Determine if we should bid on this offer based on strategy"""
    if strategy == "aggressive":
        # Bid on all compatible offers
        return True
    elif strategy == "conservative":
        # Only bid on offers with auto-accept enabled
        return offer.get("automation_strat_id") in [1, 2]
    elif strategy == "auto_accept_only":
        # Only bid on auto-accept offers (best rates from auto-accept only)
        return offer.get("automation_strat_id") in [1, 2]
    elif strategy == "balanced":
        # Bid on auto-accept offers and some manual offers
        return True  # For now same as aggressive (TODO)

    return False


def process_offers(args, config, script_state) -> None:
    if shutdown_in_progress:
        return

    offer_templates = config["offers"]
    if len(offer_templates) < 1:
        return
    if args.debug:
        print(
            "Processing {} offer template{}".format(
                config["num_enabled_offers"],
                "s" if config["num_enabled_offers"] != 1 else "",
            )
        )

    random.shuffle(offer_templates)

    try:
        sent_offers = read_json_api("sentoffers", {"active": "active"})
        if not isinstance(sent_offers, list):
            if args.debug:
                print(
                    f"Invalid sent offers response type: {type(sent_offers)}, content: {sent_offers}"
                )
            sent_offers = []
    except Exception as e:
        if args.debug:
            print(f"Error getting sent offers: {e}")
        sent_offers = []

    for offer_template in offer_templates:
        if offer_template.get("enabled", True) is False:
            continue
        offers_found = 0

        try:
            coin_from_data = coins_map[offer_template["coin_from"]]
            coin_to_data = coins_map[offer_template["coin_to"]]
        except KeyError as e:
            print(f"Skipping {offer_template['name']} - coin not available")
            if args.debug:
                print(f"Error: {e}")
            continue

        coin_ticker = coin_from_data["ticker"]
        coin_from_data_name = offer_template["coin_from"]

        try:
            wallet_from = read_json_api_wallet(f"wallets/{coin_ticker}")
            if coin_ticker == "PART":
                if "variant" in coin_from_data:
                    coin_variant = coin_from_data["variant"]
                    if coin_variant == "Anon":
                        wallet_balance = float(wallet_from["anon_balance"])
                        if args.debug:
                            print(f"Using anon balance: {wallet_balance}")
                    elif coin_variant == "Blind":
                        wallet_balance = float(wallet_from["blind_balance"])
                        if args.debug:
                            print(f"Using blind balance: {wallet_balance}")
                    else:
                        raise ValueError(
                            f"{coin_ticker} variant {coin_variant} not handled"
                        )
                else:
                    wallet_balance = float(wallet_from["balance"])
                    if args.debug:
                        print(f"Using regular balance: {wallet_balance}")
            else:
                wallet_balance = float(wallet_from["balance"])
                if args.debug:
                    print(f"Using balance for {coin_ticker}: {wallet_balance}")
        except (KeyError, TypeError, ValueError) as e:
            print(f"Skipping {offer_template['name']} - wallet balance unavailable")
            if args.debug:
                print(f"coin_ticker {coin_ticker}, error: {e}")
                print(f"Wallet data: {wallet_from}")
            continue

        created_offers = script_state.get("offers", {})
        prev_template_offers = created_offers.get(offer_template["name"], [])

        template_offer_ids = set()
        for prev_offer in prev_template_offers:
            if "offer_id" in prev_offer:
                template_offer_ids.add(prev_offer["offer_id"])

        matching_sent_offers = []
        for offer in sent_offers:
            offer_id = offer.get("offer_id")
            if not offer_id:
                continue

            if offer_id in template_offer_ids:
                matching_sent_offers.append(offer)
                offers_found += 1

                offer_amount_from = float(offer.get("amount_from", 0))
                min_coin_from_amt = float(offer_template.get("min_coin_from_amt", 0))

                if offer_amount_from > wallet_balance:
                    print(
                        f"Revoking offer {offer_id}, offer amount {offer_amount_from:.8f} > wallet balance {wallet_balance:.8f}"
                    )
                    result = read_json_api(f"revokeoffer/{offer_id}")
                    if args.debug:
                        print("revokeoffer", result)
                    else:
                        print("Offer revoked, will repost with accurate amount")
                    for i, prev_offer in enumerate(prev_template_offers):
                        if prev_offer.get("offer_id") == offer_id:
                            del prev_template_offers[i]
                            break
                    write_state(args.statefile, script_state)
                    offers_found -= 1
                elif wallet_balance <= min_coin_from_amt:
                    print(
                        "Revoking offer {}, wallet from balance below minimum".format(
                            offer_id
                        )
                    )
                    result = read_json_api(f"revokeoffer/{offer_id}")
                    if args.debug:
                        print("revokeoffer", result)
                    else:
                        print("Offer revoked successfully")
            else:
                coin_from_match = offer.get("coin_from") == coin_from_data["id"]
                coin_to_match = offer.get("coin_to") == coin_to_data["id"]

                if coin_from_match and coin_to_match:
                    if args.debug:
                        print(
                            f"Found untracked offer {offer_id} matching template {offer_template['name']} coins"
                        )
                    matching_sent_offers.append(offer)
                    offers_found += 1

        if len(matching_sent_offers) > 1:
            print(
                f"WARNING: Found {len(matching_sent_offers)} active offers for template '{offer_template['name']}'"
            )
            if args.debug:
                print(f"Offer IDs: {[o.get('offer_id') for o in matching_sent_offers]}")

            matching_sent_offers.sort(
                key=lambda x: x.get("created_at", 0), reverse=True
            )
            newest_offer = matching_sent_offers[0]

            for old_offer in matching_sent_offers[1:]:
                old_offer_id = old_offer.get("offer_id")
                print(f"Revoking duplicate offer {old_offer_id}")
                try:
                    result = read_json_api(f"revokeoffer/{old_offer_id}")
                    if args.debug:
                        print(f"Revoke result: {result}")

                    for i, prev_offer in enumerate(prev_template_offers):
                        if prev_offer.get("offer_id") == old_offer_id:
                            del prev_template_offers[i]
                            break
                except Exception as e:
                    print(f"Error revoking duplicate offer {old_offer_id}: {e}")

            offers_found = 1

            if newest_offer.get("offer_id") not in template_offer_ids:
                if "offers" not in script_state:
                    script_state["offers"] = {}
                if offer_template["name"] not in script_state["offers"]:
                    script_state["offers"][offer_template["name"]] = []
                script_state["offers"][offer_template["name"]].append(
                    {"offer_id": newest_offer["offer_id"], "time": int(time.time())}
                )
                write_state(args.statefile, script_state)
                print(f"Added untracked offer {newest_offer['offer_id']} to state")

        if offers_found > 0:
            continue

        try:
            max_offer_amount: float = float(offer_template["amount"])
            min_offer_amount: float = 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
        except (TypeError, ValueError) as e:
            print(f"Error processing amounts for {offer_template['name']}: {e}")
            print("Skipping template due to invalid amount values")
            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
            try:
                min_steps: int = int(available_balance / min_offer_amount)
                if min_steps <= 0:
                    min_steps = 1
                offer_amount = min_offer_amount * min_steps
            except (TypeError, ValueError) as e:
                print(f"Error calculating steps: {e}. Using max available amount.")
                offer_amount = min(max_offer_amount, available_balance)

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

        coin_from_id_for_rates = coin_from_data["id"]
        coin_to_id_for_rates = coin_to_data["id"]

        if (
            "ticker" in coin_from_data
            and coin_from_data["ticker"] == "PART"
            and "variant" in coin_from_data
        ):
            coin_from_id_for_rates = 1  # PART coin ID
            print(
                f"Using base PART (ID: {coin_from_id_for_rates}) for rate lookup instead of {coin_from_data_name}"
            )

        if (
            "ticker" in coin_to_data
            and coin_to_data["ticker"] == "PART"
            and "variant" in coin_to_data
        ):
            coin_to_id_for_rates = 1  # PART coin ID
            print(
                f"Using base PART (ID: {coin_to_id_for_rates}) for rate lookup instead of {coin_to_data['ticker']}"
            )

        is_part_to_part = False

        if "ticker" in coin_from_data and "ticker" in coin_to_data:
            if coin_from_data["ticker"] == "PART" and coin_to_data["ticker"] == "PART":
                is_part_to_part = True

        if coin_from_id_for_rates == 1 and coin_to_id_for_rates == 1:  # Both PART
            is_part_to_part = True

        # Get adjust_rates_based_on_market setting (WIP)
        adjust_rates_value = offer_template.get("adjust_rates_based_on_market", "false")

        if isinstance(adjust_rates_value, bool):
            adjust_rates_value = "true" if adjust_rates_value else "false"
        elif adjust_rates_value is True:
            adjust_rates_value = "true"
        elif adjust_rates_value is False:
            adjust_rates_value = "false"

        # Fallback to global config
        if "adjust_rates_based_on_market" not in offer_template:
            global_setting = config.get("adjust_rates_based_on_market", False)
            if isinstance(global_setting, bool):
                adjust_rates_value = "true" if global_setting else "false"
            else:
                adjust_rates_value = str(global_setting)

        if args.debug:
            print(
                f"Adjust rates mode for {offer_template['name']}: {adjust_rates_value}"
            )

        coingecko_rate = None
        # Get CoinGecko rates if needed (for "true", "false", "all", and unknown modes)
        if is_part_to_part:
            use_rate = 1.0
            print("Using fixed rate 1.0 for PART to PART (or variants)")
            offer_template["adjust_rates_based_on_market"] = "static"

        elif adjust_rates_value not in ["only", "minrate", "static"]:
            try:
                rates = read_json_api(
                    "rates",
                    {
                        "coin_from": coin_from_id_for_rates,
                        "coin_to": coin_to_id_for_rates,
                    },
                )

                if not isinstance(rates, dict):
                    if args.debug:
                        print(
                            f"Invalid rates response type: {type(rates)}, content: {rates}"
                        )
                    else:
                        print(
                            f"Skipping {offer_template['name']} - invalid rates response"
                        )

                if args.debug:
                    print("Rates response:", rates)

                use_rate = None

                if (
                    "coingecko" in rates
                    and isinstance(rates["coingecko"], dict)
                    and "rate_inferred" in rates["coingecko"]
                ):
                    coingecko_rate = float(rates["coingecko"]["rate_inferred"])
                    use_rate = coingecko_rate
                    print(f"Using CoinGecko rate: {use_rate}")
                else:
                    print(
                        f"No CoinGecko rate available for {coin_from_data_name} to {coin_to_data['ticker']}"
                    )
            except Exception as e:
                if args.debug:
                    print(
                        f"Error getting rates for {coin_from_data_name} to {coin_to_data['ticker']}: {e}"
                    )

        # Fetch Orderbook data if needed (for "true", "only", "all", and "minrate" modes)
        market_rate = None
        if adjust_rates_value in ["true", "only", "all", "minrate"]:
            try:
                if args.debug:
                    print(
                        f"Fetching offers with coin_from_id: {coin_from_data['id']}, coin_to_id: {coin_to_data['id']}"
                    )

                received_offers = read_json_api(
                    "offers",
                    {
                        "active": "active",
                        "include_sent": False,
                        "coin_from": coin_from_data["id"],
                        "coin_to": coin_to_data["id"],
                        "with_extra_info": True,
                    },
                )

                if not isinstance(received_offers, list):
                    if args.debug:
                        print(
                            f"Invalid market offers response type: {type(received_offers)}, content: {received_offers}"
                        )
                    received_offers = []

            except Exception as e:
                if args.debug:
                    print(f"Error getting market offers: {e}")
                received_offers = []

            if args.debug:
                coin_from_name = offer_template["coin_from"]
                coin_to_name = offer_template["coin_to"]
                print(
                    f"Found {len(received_offers)} existing offers for {coin_from_name} to {coin_to_name}"
                )

            if adjust_rates_value in ["true", "only", "all", "minrate"]:
                original_count = len(received_offers)
                auto_accept_offers = []

                if "automation_strategy" not in offer_template:
                    template_strategy = "accept_all"
                else:
                    template_strategy = offer_template["automation_strategy"]

                for offer in received_offers:
                    try:
                        offer_id = offer.get("offer_id", "unknown")
                        offer_strat_id = offer.get("automation_strat_id", 0)
                        if args.debug:
                            print(
                                f"Checking offer {offer_id} from {offer['addr_from']}"
                            )
                            print(f"Automation strategy ID: {offer_strat_id}")

                        if template_strategy == "accept_all" and offer_strat_id == 1:
                            auto_accept_offers.append(offer)
                            if args.debug:
                                print(
                                    f"Using {template_strategy}. Offer strategy = {offer_strat_id}. Added offer: {offer_id}"
                                )
                        elif template_strategy == "accept_known" and offer_strat_id in [
                            1,
                            2,
                        ]:
                            auto_accept_offers.append(offer)
                            if args.debug:
                                print(
                                    f"Using {template_strategy}. Offer strategy = {offer_strat_id}. Added offer: {offer_id}"
                                )
                        elif template_strategy == "none" or not template_strategy:
                            auto_accept_offers.append(offer)
                            if args.debug:
                                print(
                                    f"Automation_strategy is disabled. Adding all offers. offer: {offer_id}"
                                )
                        elif template_strategy in ["accept_all", "accept_known"]:
                            if args.debug:
                                print(
                                    f"Offer not a match for {template_strategy}. Skipping offer: {offer_id}"
                                )

                    except Exception as e:
                        if args.debug:
                            print(
                                f"Error checking automation strategy for offer {offer.get('offer_id', 'unknown')}: {e}"
                            )
                        continue

                received_offers = auto_accept_offers
                if args.debug:
                    print(
                        f"Filtered to {len(received_offers)} automation_strategy: '{template_strategy}' offers (from {original_count} total)"
                    )

            # Calculate market rate if offers are available
            if received_offers:
                market_rates = [float(offer["rate"]) for offer in received_offers]
                if market_rates:
                    min_market_rate = min(market_rates)
                    max_market_rate = max(market_rates)
                    avg_market_rate = sum(market_rates) / len(market_rates)

                    # Set market rate to the best rate found
                    market_rate = min_market_rate

                    # Log market statistics
                    print(
                        f"Market statistics ({template_strategy}) - Count: {len(market_rates)}, Avg: {avg_market_rate:.8f}, Min: {min_market_rate:.8f}, Max: {max_market_rate:.8f}"
                    )
                else:
                    print("No valid market rates found in existing offers")

        if adjust_rates_value == "false":
            #  Use CoinGecko only, fail if unavailable
            if is_part_to_part:
                use_rate = 1
            elif coingecko_rate:
                print(f"Using CoinGecko rate only: {coingecko_rate}")
                use_rate = coingecko_rate
            else:
                print(f"CoinGecko rate unavailable. Skipping {offer_template['name']}")
                continue

        elif adjust_rates_value == "true":
            # Use higher of CoinGecko + orderbook, fail if both unavailable
            if all((coingecko_rate, market_rate)):
                # Use the higher rate between CoinGecko and market
                use_rate = max(coingecko_rate, market_rate)
                print(
                    f"Using higher base rate - CoinGecko: {coingecko_rate}, Market: {market_rate}, Selected: {use_rate}"
                )
            elif market_rate:
                # Fallback to Orderbook data if no CoinGecko
                use_rate = market_rate
                print(f"CoinGecko unavailable. using orderbook: {use_rate}")
            elif coingecko_rate:
                # Fallback to CoinGecko if no Orderbook data
                use_rate = coingecko_rate
                print(f"Using CoinGecko rate only: {coingecko_rate}")
            else:
                print(f"Rates unavailable. Skipping {offer_template['name']}")
                continue

        elif adjust_rates_value == "all":
            # Use higher of CoinGecko + orderbook, fail if either unavailable
            if all((coingecko_rate, market_rate)):
                # Use the higher rate between CoinGecko and market
                use_rate = max(coingecko_rate, market_rate)
                print(
                    f"Using higher rate - CoinGecko: {coingecko_rate}, Market: {market_rate}, Selected: {use_rate}"
                )
            else:
                print(
                    f"ERROR: Failed to obtain market data available in 'all' mode for {offer_template['name']}"
                )
                print(
                    f"Skipping {offer_template['name']} - 'all' mode requires external rates from all sources"
                )
                continue

        elif adjust_rates_value == "only":
            # Use orderbook only, fail if no market rates
            if market_rate:
                use_rate = market_rate
                print(f"Using market rate only: {use_rate}")
            else:
                print(
                    f"ERROR: No market data available for 'only' mode for {offer_template['name']}"
                )
                print(
                    f"Skipping {offer_template['name']} - market-only mode requires existing offers"
                )
                continue

        elif adjust_rates_value == "minrate":
            # Use orderbook, fallback to minrate if no market rates
            if market_rate:
                use_rate = market_rate
                print(f"Using market rate only: {use_rate}")
            else:
                use_rate = offer_template["minrate"]
                print(f"No market data available. Using minrate: {use_rate}")

        elif adjust_rates_value == "static":
            # Use static / fixed rate + tweak
            use_rate = offer_template["minrate"]
            print(f"Using minrate as base: {use_rate}")

        else:
            # Unknown mode, default to CoinGecko
            print(
                f"Unknown adjust_rates_based_on_market value: {adjust_rates_value}, defaulting to CoinGecko"
            )
            if coingecko_rate:
                use_rate = coingecko_rate
                print(f"Using CoinGecko rate only: {coingecko_rate}")
            else:
                print(f"CoinGecko rate unavailable. Skipping {offer_template['name']}")
                continue

        # Apply ratetweakpercent
        tweak = (offer_template["ratetweakpercent"] / 100.0) + 1.0
        print(
            f"Using ratetweakpercent {offer_template['ratetweakpercent']}% for adjustment, tweak factor: {tweak}"
        )
        # tweak market_rate, coingecko_rate and static type only. Dont tweak minrate when it's a fallback
        if use_rate in [coingecko_rate, market_rate] or adjust_rates_value == "static":
            use_rate = use_rate * tweak
        if market_rate:
            market_rate = market_rate * tweak
            print(
                f"Calculated market rate: {market_rate} (min market: {min_market_rate}, tweak: {tweak})"
            )
        if coingecko_rate:
            coingecko_rate = coingecko_rate * tweak
            print(f"Calculated CoinGecko rate: {coingecko_rate}, tweak: {tweak}")

        # Ensure we don't go below minimum rate
        if use_rate < offer_template["minrate"]:
            print(
                f"Calculated rate {use_rate} is below minimum rate {offer_template['minrate']}, using minimum"
            )
            use_rate = offer_template["minrate"]

        if args.debug:
            print(
                "Creating offer for: {} at rate: {}".format(
                    offer_template["name"], use_rate
                )
            )
        else:
            print(
                "Creating offer for: {} {} -> {} at rate: {}".format(
                    offer_amount,
                    coin_from_data["ticker"],
                    coin_to_data["ticker"],
                    use_rate,
                )
            )
        template_from_addr = offer_template["address"]

        coin_from_id = coin_from_data["id"]
        coin_to_id = coin_to_data["id"]

        if args.debug:
            print(f"Using coin IDs for API: {coin_from_id} -> {coin_to_id}")

        # Get automation strategy (auto accept bids setting)
        automation_strategy = offer_template.get(
            "automation_strategy", "accept_all"
        )  # accept_all, accept_known, none
        automation_strat_id = 1  # Default to "Accept All" strategy (ID 1)
        if automation_strategy == "accept_all":
            automation_strat_id = 1  # Accept All strategy (ID 1)
        elif automation_strategy == "accept_known":
            automation_strat_id = 2  # Accept Known strategy (ID 2)
        elif automation_strategy == "none":
            automation_strat_id = -1  # No automation

        offer_data = {
            "addr_from": (-1 if template_from_addr == "auto" else template_from_addr),
            "coin_from": coin_from_id,
            "coin_to": coin_to_id,
            "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": automation_strat_id,
        }

        if "min_swap_amount" in offer_template:
            offer_data["amt_bid_min"] = offer_template["min_swap_amount"]

        if args.debug:
            print(
                f"Offer data: {offer_amount} {coin_from_data['ticker']} -> {coin_to_data['ticker']} at {use_rate}"
            )

        # Attempt to fill existing offers before creating AMM offer
        final_offer_amount = attempt_pre_offer_bids(
            offer_template,
            use_rate,
            offer_amount,
            coin_from_data,
            coin_to_data,
            args.debug,
        )

        # Check if we still need to create an offer after bidding
        min_remaining_offer = offer_template.get("min_remaining_offer", 0.001)
        if final_offer_amount < min_remaining_offer:
            print(
                f"Fully filled via bids, skipping AMM offer creation for {offer_template['name']}"
            )
            continue

        # Update offer amount if it changed due to bidding
        if final_offer_amount != offer_amount:
            offer_data["amt_from"] = final_offer_amount
            print(f"Adjusted offer amount to {final_offer_amount} after bidding")

        try:
            new_offer = read_json_api("offers/new", offer_data)

            if not isinstance(new_offer, dict):
                if args.debug:
                    print(
                        f"Invalid new offer response type: {type(new_offer)}, content: {new_offer}"
                    )
                else:
                    print(
                        f"Error creating offer for {offer_template['name']} - invalid response"
                    )
                continue

            if "error" in new_offer:
                raise ValueError(f"Server failed to create offer: {new_offer['error']}")

            if "offer_id" not in new_offer:
                if args.debug:
                    print(f"New offer response missing offer_id: {new_offer}")
                else:
                    print(
                        f"Error creating offer for {offer_template['name']} - missing offer ID"
                    )
                continue

            print(f"New offer created with ID: {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())}
            )
        except Exception as e:
            print(f"Error creating offer: {e}")
            continue

        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
            )

        next_offer_time = int(time.time()) + time_between_offers
        script_state["delay_next_offer_before"] = next_offer_time

        if args.debug:
            print(
                f"Next offer will be created after {time_between_offers} seconds (at {time.ctime(next_offer_time)})"
            )

        write_state(args.statefile, script_state)


def process_bids(args, config, script_state) -> None:
    if shutdown_in_progress:
        return

    bid_templates = config["bids"]
    if len(bid_templates) < 1:
        return
    random.shuffle(bid_templates)

    if args.debug:
        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()):
            if args.debug:
                print(
                    "Delaying bids until {}".format(time.ctime(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:
            if args.debug:
                print("Max concurrent bids reached for template")
            continue

        # Bidder sends coin_to and receives coin_from
        try:
            coin_from_data = coins_map[bid_template["coin_from"]]
            coin_to_data = coins_map[bid_template["coin_to"]]
        except KeyError as e:
            if args.debug:
                print(f"Coin not found in coins_map for bid: {e}")
            else:
                print(f"Skipping {bid_template['name']} - coin not available")
            continue

        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()
            try:
                page_offers = read_json_api("offers", offers_options)

                if not isinstance(page_offers, list):
                    if args.debug:
                        print(
                            f"Invalid page offers response type: {type(page_offers)}, content: {page_offers}"
                        )
                    break

                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
            except Exception as e:
                if args.debug:
                    print(f"Error getting page offers: {e}")
                break

        if args.debug:
            print(f"Found {len(received_offers)} offers for bidding analysis")

        for offer in received_offers:
            try:
                if not isinstance(offer, dict):
                    if args.debug:
                        print(f"Invalid offer type: {type(offer)}, content: {offer}")
                    continue

                if (
                    "offer_id" not in offer
                    or "amount_from" not in offer
                    or "rate" not in offer
                ):
                    if args.debug:
                        print(f"Offer missing required fields: {offer}")
                    continue

                offer_id = offer["offer_id"]
                offer_amount = float(offer["amount_from"])
                offer_rate = float(offer["rate"])

                # Check if we should use balance-based bidding
                use_balance_bidding = bid_template.get("use_balance_bidding", False)

                if use_balance_bidding:
                    # Calculate bid amount based on available balance minus minimum amount
                    # This follows the same pattern as offers: balance - min_amount
                    try:
                        # Get wallet balance for the coin we're bidding with (coin_from in the offer)
                        wallet_from = read_json_api_wallet(
                            "wallets/{}".format(coin_from_data["ticker"])
                        )

                        wallet_balance = float(wallet_from["balance"]) + float(
                            wallet_from["unconfirmed"]
                        )

                        # Get minimum amount from the offer
                        offer_min_amount = float(offer.get("amount_bid_min", 0.001))

                        # Calculate available amount: balance - min_amount
                        available_amount = wallet_balance - offer_min_amount

                        if available_amount > 0:
                            bid_amount = available_amount
                            if args.debug:
                                print(
                                    f"Using balance-based bidding: {bid_amount} (balance: {wallet_balance} - min: {offer_min_amount})"
                                )
                        else:
                            # Fallback to template amount if calculation fails
                            bid_amount = bid_template["amount"]
                            if args.debug:
                                print(
                                    f"Insufficient balance for balance-based bid, using template amount: {bid_amount}"
                                )

                    except Exception as balance_error:
                        # Fallback to template amount if balance calculation fails
                        bid_amount = bid_template["amount"]
                        if args.debug:
                            print(
                                f"Error calculating balance-based bid amount: {balance_error}, using template amount: {bid_amount}"
                            )
                else:
                    # Use traditional template amount
                    bid_amount = bid_template["amount"]
                    if args.debug:
                        print(f"Using template bid amount: {bid_amount}")
            except (KeyError, TypeError, ValueError) as e:
                if args.debug:
                    print(f"Error processing offer data: {e}, offer: {offer}")
                continue

            min_swap_amount = bid_template.get(
                "min_swap_amount", 0.001
            )  # 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

            try:
                sent_bids = read_json_api(
                    "sentbids",
                    {
                        "offer_id": offer["offer_id"],
                        "with_available_or_active": True,
                    },
                )

                if not isinstance(sent_bids, list):
                    if args.debug:
                        print(
                            f"Invalid sent bids response type: {type(sent_bids)}, content: {sent_bids}"
                        )
                    sent_bids = []

            except Exception as e:
                if args.debug:
                    print(f"Error getting sent bids for offer {offer_id}: {e}")
                sent_bids = []

            if len(sent_bids) > 0:
                if args.debug:
                    print(f"Already bidding on offer {offer_id}")
                continue

            # Check if we should bid on offers based on their auto-accept settings
            offers_to_bid_on = bid_template.get(
                "offers_to_bid_on", "auto_accept_only"
            )  # all, auto_accept_only, known_only

            try:
                offer_identity = read_json_api(
                    "identities/{}".format(offer["addr_from"])
                )

                if not isinstance(offer_identity, dict):
                    if args.debug:
                        print(
                            f"Invalid offer identity response type: {type(offer_identity)}, content: {offer_identity}"
                        )
                    offer_identity = {}

            except Exception as e:
                if args.debug:
                    print(f"Error getting offer identity for {offer['addr_from']}: {e}")
                offer_identity = {}

            if "address" in offer_identity:
                id_offer_from = offer_identity
                automation_override = id_offer_from["automation_override"]

                # Check offers_to_bid_on setting
                if offers_to_bid_on == "auto_accept_only" and automation_override != 1:
                    if args.debug:
                        print(
                            f"Not bidding on offer {offer_id}, offers_to_bid_on is auto_accept_only but offer doesn't have auto-accept enabled."
                        )
                    continue

                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:
                    # For known_only setting, check identity stats
                    if offers_to_bid_on == "known_only":
                        successful_sent_bids = id_offer_from["num_sent_bids_successful"]
                        if successful_sent_bids < 1:
                            if args.debug:
                                print(
                                    f"Not bidding on offer {offer_id}, offers_to_bid_on is known_only but no successful swaps with this identity."
                                )
                                continue

                    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:
                try:
                    wallet_from = read_json_api_wallet(
                        "wallets/{}".format(coin_from_data["ticker"])
                    )
                    total_balance_from = float(wallet_from.get("balance", 0)) + float(
                        wallet_from.get("unconfirmed", 0)
                    )
                    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
                except (KeyError, TypeError, ValueError) as e:
                    if args.debug:
                        print(
                            f"Error getting wallet from balance for bid {offer_id}: {e}"
                        )
                    else:
                        print(
                            f"Skipping bid {offer_id} - wallet from balance unavailable"
                        )
                    continue

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

                    total_balance_to = float(wallet_to.get("balance", 0)) + float(
                        wallet_to.get("unconfirmed", 0)
                    )
                    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
                except (KeyError, TypeError, ValueError) as e:
                    if args.debug:
                        print(
                            f"Error getting wallet to balance for bid {offer_id}: {e}"
                        )
                    else:
                        print(
                            f"Skipping bid {offer_id} - wallet to balance unavailable"
                        )
                    continue

            if validateamount:
                try:
                    validated_amount = read_json_api(
                        "validateamount",
                        {
                            "coin": coin_from_data["ticker"],
                            "amount": bid_amount,
                            "method": "rounddown",
                        },
                    )

                    if isinstance(validated_amount, (int, float, str)):
                        bid_amount = float(validated_amount)
                    else:
                        if args.debug:
                            print(
                                f"Invalid validateamount response: {validated_amount}"
                            )

                except Exception as e:
                    if args.debug:
                        print(f"Error validating amount: {e}")
            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(
                        f"Creating bid: {bid_amount} {coin_to_data['ticker']} for offer {offer_id}"
                    )
                try:
                    new_bid = read_json_api("bids/new", bid_data)

                    if not isinstance(new_bid, dict):
                        if args.debug:
                            print(
                                f"Invalid new bid response type: {type(new_bid)}, content: {new_bid}"
                            )
                        else:
                            print(
                                f"Error creating bid on offer {offer['offer_id']} - invalid response"
                            )
                        continue

                    if "error" in new_bid:
                        raise ValueError(
                            "Server failed to create bid: {}".format(new_bid["error"])
                        )

                    if "bid_id" not in new_bid:
                        if args.debug:
                            print(f"New bid response missing bid_id: {new_bid}")
                        else:
                            print(
                                f"Error creating bid on offer {offer['offer_id']} - missing bid ID"
                            )
                        continue

                    print(
                        "New bid created with ID: {} on offer {}".format(
                            new_bid["bid_id"], offer["offer_id"]
                        )
                    )
                    bid_id = new_bid["bid_id"]
                except Exception as e:
                    if args.debug:
                        print(f"Error creating bid: {e}")
                    else:
                        print(f"Failed to create bid on offer {offer['offer_id']}")
                    continue

            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
            next_bid_time = int(time.time()) + time_between_bids
            script_state["delay_next_bid_before"] = next_bid_time

            if args.debug:
                print(
                    f"Next bid will be created after {time_between_bids} seconds (at {time.ctime(next_bid_time)})"
                )

            write_state(args.statefile, script_state)
            break


def prune_script_state(now, args, config, script_state):
    if shutdown_in_progress:
        return

    if args.debug:
        print("Pruning script state.")

    removed_offers: int = 0
    removed_bids: int = 0

    max_ttl: int = config["prune_state_after_seconds"]
    if "offers" in script_state:
        for template_name, template_group in script_state["offers"].items():
            offers_to_remove = []
            for offer in template_group:
                if now - offer["time"] > max_ttl:
                    offers_to_remove.append(offer["offer_id"])

            for offer_id in offers_to_remove:
                for i, offer in enumerate(template_group):
                    if offer_id == offer["offer_id"]:
                        del template_group[i]
                        removed_offers += 1
                        break

    if "bids" in script_state:
        for template_name, template_group in script_state["bids"].items():
            bids_to_remove = []
            for bid in template_group:
                if now - bid["time"] > max_ttl:
                    bids_to_remove.append(bid["bid_id"])

            for bid_id in bids_to_remove:
                for i, bid in enumerate(template_group):
                    if bid_id == bid["bid_id"]:
                        del template_group[i]
                        removed_bids += 1
                        break

    if removed_offers > 0 or removed_bids > 0:
        print(
            "Pruned {} offer{} and {} bid{} from script state.".format(
                removed_offers,
                "s" if removed_offers != 1 else "",
                removed_bids,
                "s" if removed_bids != 1 else "",
            )
        )
    script_state["time_last_pruned_state"] = now
    write_state(args.statefile, script_state)


def main():
    global read_json_api, read_json_api_wallet
    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()

    if not os.path.exists(args.configfile):
        print(f'Error: Config file "{args.configfile}" not found.')
        return 1

    try:
        with open(args.configfile) as fs:
            initial_config = json.load(fs)
    except Exception as e:
        print(f"Error reading config file {args.configfile}: {e}")
        return 1

    auth_info = initial_config.get("auth")

    read_json_api = make_json_api_func(args.host, args.port, auth_info)
    wallet_api_port_override = initial_config.get("wallet_port_override")
    if wallet_api_port_override:
        read_json_api_wallet_auth = make_json_api_func(
            args.host, int(wallet_api_port_override), auth_info
        )
    else:
        read_json_api_wallet_auth = read_json_api_wallet

    try:
        print("Checking API connection...")
        known_coins = read_json_api("coins")

        if not isinstance(known_coins, list):
            print(f"Error: Invalid coins response type: {type(known_coins)}")
            print("Please ensure BasicSwap is running and accessible.")
            return 1

        for known_coin in known_coins:
            if isinstance(known_coin, dict) and "name" in known_coin:
                coins_map[known_coin["name"]] = known_coin
            elif args.debug:
                print(f"Skipping invalid coin data: {known_coin}")

        print("API connection successful.")
    except ValueError as e:
        print(f"\nError: {e}")
        print(
            'Please ensure the \'auth\' key in your config file is correct (e.g., "auth": "username:password")'
        )
        return 1
    except urllib.error.URLError as e:
        print(
            f"\nError: Could not connect to Basicswap API at http://{args.host}:{args.port}"
        )
        print(f"Reason: {e.reason}")
        print("Please ensure Basicswap is running and accessible.")
        return 1
    except Exception as e:
        print(f"\nError during initial API connection: {e}")
        if args.debug:
            traceback.print_exc()
        return 1

    script_state = {}
    if os.path.exists(args.statefile):
        try:
            with open(args.statefile) as fs:
                script_state = json.load(fs)
        except Exception as e:
            print(f"Error reading state file {args.statefile}: {e}")
            print("Starting with empty state.")
            script_state = {}

    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)

    last_summary_time = 0
    summary_interval = 600

    try:
        while not delay_event.is_set():
            # Read config each iteration so it can be modified without restarting
            config = readConfig(args, known_coins)

            # 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 = read_json_api_wallet_auth
            else:
                read_json_api_wallet = read_json_api

            # Skip processing if shutdown is in progress
            if not shutdown_in_progress:
                try:
                    process_offers(args, config, script_state)

                    process_bids(args, config, script_state)

                    now = int(time.time())
                    prune_state_delay = config["prune_state_delay"]
                    if prune_state_delay > 0:
                        if (
                            now - script_state.get("time_last_pruned_state", 0)
                            > prune_state_delay
                        ):
                            prune_script_state(now, args, config, script_state)

                except Exception as e:
                    error_msg = str(e)

                    if (
                        any(
                            keyword in error_msg.lower()
                            for keyword in ["balance", "insufficient", "not enough"]
                        )
                        and not args.debug
                    ):

                        current_time = int(time.time())
                        if (
                            not hasattr(process_offers, "_last_balance_error")
                            or current_time
                            - getattr(process_offers, "_last_balance_error", 0)
                            > 3600
                        ):
                            print(
                                "AMM Info: Insufficient balance for some offers (use --debug for details)"
                            )
                            process_offers._last_balance_error = current_time
                    else:
                        print(f"AMM Error: {error_msg}")

                    if args.debug:
                        print(f"Full error details: {e}")
                        traceback.print_exc()

            if args.oneshot or shutdown_in_progress:
                break

            current_time = int(time.time())
            if args.debug and current_time - last_summary_time > summary_interval:
                active_offers = sum(
                    len(template_group)
                    for template_group in script_state.get("offers", {}).values()
                )
                active_bids = sum(
                    len(template_group)
                    for template_group in script_state.get("bids", {}).values()
                )
                print(
                    f"AMM Summary: {active_offers} active offers, {active_bids} active bids, next check in {config['main_loop_delay']}s"
                )
                last_summary_time = current_time

            delay_event.wait(config["main_loop_delay"])

        print("Done.")
        return 0
    except KeyboardInterrupt:
        print("\nExiting due to keyboard interrupt.")
        return 0
    except Exception as e:
        print(f"Fatal error: {e}")
        if args.debug:
            traceback.print_exc()
        return 1


if __name__ == "__main__":
    main()
