# app.py

# Monkey patching must occur before other imports
import eventlet
eventlet.monkey_patch()  # noqa: E402

# Standard library imports
import logging  # noqa: E402
import os  # noqa: E402
import json  # noqa: E402
import subprocess  # noqa: E402
import xml.etree.ElementTree as ET  # noqa: E402

# Third-party imports
from flask import Flask, render_template, request, redirect, jsonify, session, flash  # noqa: E402
from flask_cors import CORS  # noqa: E402
from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect  # noqa: E402
from email_validator import validate_email, EmailNotValidError  # noqa: E402

# Local application imports
from BrighterTrades import BrighterTrades  # noqa: E402
from utils import sanitize_for_json  # noqa: E402
from wallet import WalletBackgroundJobs  # noqa: E402

# Set up logging
log_level_name = os.getenv('BRIGHTER_LOG_LEVEL', 'INFO').upper()
log_level = getattr(logging, log_level_name, logging.INFO)

# Debug file logger for execution loop
_loop_debug = logging.getLogger('loop_debug')
_loop_debug.setLevel(logging.DEBUG)
_loop_handler = logging.FileHandler('/home/rob/PycharmProjects/BrighterTrading/trade_debug.log', mode='a')
_loop_handler.setFormatter(logging.Formatter('%(asctime)s - [LOOP] %(message)s'))
_loop_debug.addHandler(_loop_handler)
logging.basicConfig(level=log_level)
logging.getLogger('ccxt.base.exchange').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logger = logging.getLogger(__name__)

# Create a Flask object named app that serves the html.
app = Flask(__name__)
# Create a socket in order to receive requests.
socketio = SocketIO(app, async_mode='eventlet')


# Custom Jinja2 filter to format small balances properly (avoid scientific notation)
@app.template_filter('format_balance')
def format_balance(value):
    """Format balance to avoid scientific notation for small values."""
    if value is None:
        return '0'
    try:
        val = float(value)
        if val == 0:
            return '0'
        elif abs(val) < 0.00001:
            return f'{val:.8f}'.rstrip('0').rstrip('.')
        elif abs(val) < 1:
            return f'{val:.6f}'.rstrip('0').rstrip('.')
        elif abs(val) < 1000:
            return f'{val:.4f}'.rstrip('0').rstrip('.')
        else:
            return f'{val:,.2f}'
    except (ValueError, TypeError):
        return str(value)

# Create a BrighterTrades object. This the main application that maintains access to the server, local storage,
# and manages objects that process trade data.
brighter_trades = BrighterTrades(socketio)

# Set server configuration globals.
CORS_HEADERS = 'Content-Type'

# Socket ID to authenticated user_id mapping
# This is the source of truth for WebSocket authentication - never trust client payloads
socket_user_mapping = {}  # request.sid -> user_id
wallet_jobs = None
_wallet_jobs_started = False

# Set the app directly with the globals.
app.config.from_object(__name__)
app.secret_key = '1_BAD_secrete_KEY_is_not_2'

# Enable cross-origin resource sharing for specific endpoints
cors = CORS(app, supports_credentials=True,
            resources={
                r"/api/indicator_init": {"origins": ["http://127.0.0.1:5000", "http://localhost:5000"]}
            },
            allow_headers=['Content-Type'])  # Change from headers to allow_headers


@app.after_request
def add_cors_headers(response):
    response.headers['Access-Control-Allow-Credentials'] = 'true'
    return response


# =============================================================================
# Strategy Execution Loop (Background Task)
# =============================================================================
# This runs in the background and periodically executes active live/paper strategies

STRATEGY_LOOP_INTERVAL = 10  # seconds between strategy ticks
_strategy_loop_running = False

def strategy_execution_loop():
    """
    Background loop that executes active strategies periodically.

    For live trading, strategies need to be triggered server-side,
    not just when the client sends candle data.
    """
    global _strategy_loop_running
    _strategy_loop_running = True
    logger = logging.getLogger('strategy_loop')
    logger.info("Strategy execution loop started")

    while _strategy_loop_running:
        try:
            # Check if there are any active strategy instances
            active_count = brighter_trades.strategies.get_active_count()

            # Log every iteration for debugging
            if not hasattr(strategy_execution_loop, '_iter_count'):
                strategy_execution_loop._iter_count = 0
            strategy_execution_loop._iter_count += 1
            logger.info(f"Execution loop iteration {strategy_execution_loop._iter_count}, active_count={active_count}")

            if active_count > 0:
                logger.info(f"Executing {active_count} active strategies...")

                # Iterate directly over active instances
                instance_keys = list(brighter_trades.strategies.active_instances.keys())

                for instance_key in instance_keys:
                    try:
                        user_id, strategy_id, mode = instance_key
                        instance = brighter_trades.strategies.active_instances.get(instance_key)

                        if instance is None:
                            continue

                        # For live strategies, get current price as candle data
                        # Default to BTC/USDT if no symbol specified
                        symbol = getattr(instance, 'symbol', 'BTC/USDT')

                        try:
                            price = brighter_trades.exchanges.get_price(symbol)
                            if price:
                                import time
                                candle_data = {
                                    'symbol': symbol,
                                    'close': price,
                                    'open': price,
                                    'high': price,
                                    'low': price,
                                    'volume': 0,
                                    'time': int(time.time())
                                }

                                # Execute strategy tick
                                events = instance.tick(candle_data)

                                if events:
                                    logger.info(f"Strategy {strategy_id} generated {len(events)} events: {events}")
                                    # Emit events to the user's room
                                    user_name = brighter_trades.users.get_username(user_id=user_id)
                                    logger.info(f"Emitting to user_id={user_id}, user_name={user_name}")
                                    if user_name:
                                        socketio.emit('strategy_events', sanitize_for_json({
                                            'strategy_id': strategy_id,
                                            'mode': mode,
                                            'events': events
                                        }), room=user_name)
                                        logger.info(f"Emitted strategy_events to room={user_name}")
                                    else:
                                        logger.warning(f"Could not find username for user_id={user_id}")

                        except Exception as e:
                            logger.warning(f"Could not get price for {symbol}: {e}")

                    except Exception as e:
                        logger.error(f"Error executing strategy {instance_key}: {e}", exc_info=True)

            # === BROKER UPDATES (single owner - happens here only) ===
            # This is the only place where brokers are polled for order fills
            if brighter_trades.manual_broker_manager:
                try:
                    # Collect prices for broker updates
                    # Paper trades use symbol-only keys (single synthetic market)
                    # Live trades use exchange:symbol keys
                    broker_price_updates = {}
                    for trade in brighter_trades.trades.active_trades.values():
                        if trade.broker_order_id:  # Only broker-managed trades
                            try:
                                is_paper = trade.broker_kind == 'paper'
                                exchange = getattr(trade, 'exchange', None) or trade.target

                                if is_paper:
                                    # Paper trades: single synthetic market, use first available exchange
                                    price = brighter_trades.exchanges.get_price(trade.symbol)
                                    if price:
                                        # Paper uses symbol-only key
                                        broker_price_updates[trade.symbol] = price
                                else:
                                    # Live trades: use specific exchange
                                    price = brighter_trades.exchanges.get_price(trade.symbol, exchange)
                                    if price:
                                        # Live uses exchange:symbol key
                                        price_key = f"{exchange.lower()}:{trade.symbol}"
                                        broker_price_updates[price_key] = price
                                        # Also add symbol-only as fallback
                                        broker_price_updates[trade.symbol] = price
                            except Exception:
                                pass

                    # Update all brokers and get fill events
                    fill_events = brighter_trades.manual_broker_manager.update_all_brokers(broker_price_updates)

                    for event in fill_events:
                        event_type = event.get('type', 'fill')

                        if event_type == 'sltp_triggered':
                            # SL/TP triggered - find related trade and notify user
                            symbol = event.get('symbol')
                            user_id = event.get('user_id')

                            # Find trades for this symbol to get the user
                            related_trade = None
                            for trade in brighter_trades.trades.active_trades.values():
                                if trade.symbol == symbol and (trade.is_paper or trade.broker_kind == 'paper'):
                                    related_trade = trade
                                    user_id = user_id or trade.creator
                                    break

                            if user_id:
                                user_name = brighter_trades.users.get_username(user_id=user_id)
                                if user_name:
                                    socketio.emit('message', {
                                        'reply': 'sltp_triggered',
                                        'data': sanitize_for_json({
                                            'trigger': event.get('trigger'),
                                            'symbol': symbol,
                                            'trigger_price': event.get('trigger_price'),
                                            'size': event.get('size'),
                                            'pnl': event.get('pnl'),
                                            'trade_id': related_trade.unique_id if related_trade else None
                                        })
                                    }, room=user_name)
                                    _loop_debug.debug(f"Emitted sltp_triggered to room={user_name}")
                            continue

                        # Find trade by broker_order_id and update
                        trade = brighter_trades.trades.find_trade_by_broker_order_id(event.get('order_id'))
                        if trade:
                            trade.trade_filled(
                                qty=event.get('filled_qty', event.get('size', 0)),
                                price=event.get('filled_price', event.get('price', 0))
                            )
                            # Note: trade_filled() sets status to 'filled' or 'part-filled' appropriately
                            # Do NOT override it here - that would lose partial-fill state
                            brighter_trades.trades._save_trade(trade)

                            # Emit fill event to user through existing message pattern
                            # Uses the 'message' event with 'reply' field that Comms understands
                            user_id = event.get('user_id') or trade.creator
                            if user_id:
                                user_name = brighter_trades.users.get_username(user_id=user_id)
                                if user_name:
                                    socketio.emit('message', {
                                        'reply': 'order_filled',
                                        'data': sanitize_for_json({
                                            'order_id': event.get('order_id'),
                                            'trade_id': trade.unique_id,
                                            'symbol': trade.symbol,
                                            'side': trade.side,
                                            'filled_qty': event.get('filled_qty', event.get('size', 0)),
                                            'filled_price': event.get('filled_price', event.get('price', 0)),
                                            'status': trade.status,  # 'filled' or 'part-filled'
                                            'broker_kind': event.get('broker_kind'),
                                            'broker_key': event.get('broker_key')
                                        })
                                    }, room=user_name)
                                    _loop_debug.debug(f"Emitted order_filled to room={user_name}")

                except Exception as e:
                    _loop_debug.debug(f"Exception in broker update: {e}")
                    logger.error(f"Error updating brokers: {e}", exc_info=True)

            # Update active trades (runs every iteration, regardless of active strategies)
            _loop_debug.debug(f"Checking active_trades: {len(brighter_trades.trades.active_trades)} trades")
            if brighter_trades.trades.active_trades:
                _loop_debug.debug(f"Has active trades, getting prices...")
                try:
                    # Group trades by (exchange, symbol) for per-exchange price fetching
                    exchange_symbols = set()
                    for trade in brighter_trades.trades.active_trades.values():
                        exchange = getattr(trade, 'exchange', None) or trade.target
                        # Normalize exchange name (skip test_exchange placeholder)
                        if exchange and exchange.lower() not in ['test_exchange', 'paper']:
                            exchange_symbols.add((exchange.lower(), trade.symbol))
                        else:
                            # Fall back to default exchange for legacy trades
                            exchange_symbols.add((None, trade.symbol))

                    _loop_debug.debug(f"Exchange+symbols to fetch: {exchange_symbols}")
                    price_updates = {}
                    for exchange, symbol in exchange_symbols:
                        try:
                            price = brighter_trades.exchanges.get_price(symbol, exchange_name=exchange)
                            _loop_debug.debug(f"Got price for {exchange}:{symbol}: {price}")
                            if price:
                                # Use exchange:symbol key for exchange-specific prices
                                if exchange:
                                    price_updates[f"{exchange}:{symbol}"] = price
                                else:
                                    price_updates[symbol] = price
                        except Exception as e:
                            _loop_debug.debug(f"Failed to get price for {exchange}:{symbol}: {e}")
                            logger.warning(f"Could not get price for {symbol} from {exchange}: {e}")

                    _loop_debug.debug(f"price_updates: {price_updates}")
                    if price_updates:
                        _loop_debug.debug(f"Calling brighter_trades.trades.update_prices_only()")
                        # Use update_prices_only to avoid duplicate broker polling
                        trade_updates = brighter_trades.trades.update_prices_only(price_updates)
                        _loop_debug.debug(f"trade_updates returned: {trade_updates}")
                        if trade_updates:
                            logger.debug(f"Trade updates (no active strategies): {trade_updates}")
                            for update in trade_updates:
                                trade_id = update.get('id')
                                trade = brighter_trades.trades.active_trades.get(trade_id)
                                _loop_debug.debug(f"Emitting update for trade_id={trade_id}, creator={trade.creator if trade else None}")
                                if trade and trade.creator:
                                    user_name = brighter_trades.users.get_username(user_id=trade.creator)
                                    if user_name:
                                        socketio.emit('trade_update', sanitize_for_json(update), room=user_name)
                                        _loop_debug.debug(f"Emitted trade_update to room={user_name}")
                except Exception as e:
                    _loop_debug.debug(f"Exception in trade update: {e}")
                    logger.error(f"Error updating trades (no strategies): {e}", exc_info=True)

        except Exception as e:
            logger.error(f"Strategy execution loop error: {e}")

        # Sleep before next iteration
        eventlet.sleep(STRATEGY_LOOP_INTERVAL)

    logger.info("Strategy execution loop stopped")


_strategy_loop_started = False


def start_strategy_loop():
    """
    Start the strategy execution loop in a background greenlet.

    This supports both `python src/app.py` and WSGI imports.
    The loop is only started once per process.
    """
    global _strategy_loop_started

    if _strategy_loop_started:
        return

    if app.config.get('TESTING') or os.getenv('PYTEST_CURRENT_TEST'):
        return

    if os.getenv('BRIGHTER_DISABLE_STRATEGY_LOOP', '').lower() in ('1', 'true', 'yes'):
        logger.info("Strategy execution loop disabled by BRIGHTER_DISABLE_STRATEGY_LOOP")
        return

    eventlet.spawn(strategy_execution_loop)
    _strategy_loop_started = True
    logger.info("Strategy execution loop started")


# Start the loop when the app starts (will be called from main block or before_request)


def start_wallet_background_jobs():
    """
    Start wallet background jobs once per process.

    This supports both `python src/app.py` and WSGI imports.
    Jobs are skipped in test contexts.
    """
    global wallet_jobs, _wallet_jobs_started

    if _wallet_jobs_started:
        return

    if app.config.get('TESTING') or os.getenv('PYTEST_CURRENT_TEST'):
        return

    if os.getenv('BRIGHTER_DISABLE_WALLET_JOBS', '').lower() in ('1', 'true', 'yes'):
        logging.info("Wallet background jobs disabled by BRIGHTER_DISABLE_WALLET_JOBS")
        return

    if not getattr(brighter_trades, 'wallet_manager', None):
        logging.warning("Wallet manager not initialized - background jobs disabled")
        return

    wallet_jobs = WalletBackgroundJobs(brighter_trades.wallet_manager, socketio)
    wallet_jobs.start_all_jobs()
    _wallet_jobs_started = True
    logging.info("Wallet background jobs started")


@app.before_request
def ensure_background_jobs_started():
    """Ensure background jobs are running in non-`__main__` deployments."""
    start_strategy_loop()
    start_wallet_background_jobs()


def _coerce_user_id(user_id):
    if user_id is None or user_id == '':
        return None
    try:
        return int(user_id)
    except (TypeError, ValueError):
        return None


def resolve_user_name(payload: dict | None) -> str | None:
    """
    Resolve a username from payload fields, accepting both legacy and migrated key shapes.
    """
    if not isinstance(payload, dict):
        return None

    user_name = payload.get('user_name') or payload.get('user')
    if user_name:
        return user_name

    user_id = _coerce_user_id(payload.get('user_id') or payload.get('userId'))
    if user_id is None:
        return None

    try:
        return brighter_trades.users.get_username(user_id=user_id)
    except Exception:
        logging.warning(f"Unable to resolve user_name from user id '{user_id}'.")
        return None


@app.route('/')
def welcome():
    """
    Serves the welcome/landing page.
    """
    return render_template('welcome.html')


@app.route('/app')
# @cross_origin(supports_credentials=True)
def index():
    """
        Fetches data from brighter_trades and inject it into an HTML template.
        Renders the html template and serves the web application.
    """
    # session.clear() # only for debugging!!!!
    try:
        # Log the user in.
        user_name = brighter_trades.users.load_or_create_user(username=session.get('user'))
    except ValueError as e:
        if str(e) != 'GuestLimitExceeded!':
            raise
        else:
            flash('The system is too busy for another user.')
            return redirect('/signup')

    # Ensure client session is assigned a user_name.
    session['user'] = user_name

    print('[SERVING INDEX] (USERNAME):', user_name)

    default_exchange = 'binance'
    default_keys = None  # No keys needed for public market data

    # Ensure that a valid connection with an exchange exists
    result = brighter_trades.connect_user_to_exchange(user_name=user_name,
                                                      default_exchange=default_exchange,
                                                      default_keys=default_keys)
    if not result:
        raise ValueError("Couldn't connect to the default exchange.")

    # A dict of data required to build the html of the user app.
    # Dynamic content like options and titles and balances to display.
    rendered_data = brighter_trades.get_rendered_data(user_name)
    # Jsom initialization data to be passed into the web application.
    js_data = brighter_trades.get_js_init_data(user_name)
    # serve the landing page.
    return render_template('index.html',
                           title=rendered_data['title'],
                           user_name=user_name,
                           my_balances=rendered_data['my_balances'],
                           symbols=rendered_data['symbols'],
                           intervals=rendered_data['intervals'],
                           interval_state=rendered_data['chart_interval'],
                           exchanges=rendered_data['available_exchanges'],
                           cond_exchanges=rendered_data['chart_exchanges'],
                           confd_exchanges=rendered_data['configured_exchanges'],
                           selected_exchange=rendered_data['selected_exchange'],
                           indicator_types=rendered_data['indicator_types'],
                           indicator_list=rendered_data['indicator_list'],
                           checked=rendered_data['enabled_indicators'],
                           ma_vals=rendered_data['ma_vals'],
                           js_data=js_data,
                           active_trades=rendered_data['active_trades'],
                           open_orders=rendered_data['open_orders'])


@socketio.on('connect')
def handle_connect():
    """
    Handle WebSocket connection.

    Security: User identity is determined from Flask session (set during HTTP login),
    NOT from query parameters. This prevents identity spoofing.
    """
    # Get user from Flask session - this is set during HTTP login (/login route)
    session_user = session.get('user')

    if not session_user:
        # No session user - reject connection
        emit('message', {'reply': 'error', 'data': 'User not authenticated - no session'})
        disconnect()
        return

    # Verify user is logged in
    if not brighter_trades.get_user_info(user_name=session_user, info='Is logged in?'):
        emit('message', {'reply': 'error', 'data': 'User not logged in'})
        disconnect()
        return

    # Get user_id from username
    try:
        user_id = brighter_trades.get_user_info(user_name=session_user, info='User_id')
    except Exception:
        emit('message', {'reply': 'error', 'data': 'Could not resolve user identity'})
        disconnect()
        return

    # Store the authenticated user_id for this socket - THIS IS THE SOURCE OF TRUTH
    socket_user_mapping[request.sid] = {
        'user_id': user_id,
        'user_name': session_user
    }

    # Join a room specific to the user for targeted messaging
    join_room(session_user)
    emit('message', {'reply': 'connected', 'data': 'Connection established'})


@socketio.on('disconnect')
def handle_disconnect():
    """Clean up socket mapping on disconnect."""
    socket_user_mapping.pop(request.sid, None)


@socketio.on('message')
def handle_message(data):
    """
    Handle incoming JSON messages with authentication.

    Security: User identity is determined from socket_user_mapping (set at connect time),
    NOT from message payload. This prevents identity spoofing attacks.
    """
    # Validate input
    if 'message_type' not in data or 'data' not in data:
        emit('message', {"success": False, "message": "Invalid message format."})
        return

    msg_type, msg_data = data['message_type'], data['data']

    # Get authenticated user from our mapping - THIS IS THE SOURCE OF TRUTH
    # DO NOT trust msg_data.get('user_name') or msg_data.get('user_id')
    auth_info = socket_user_mapping.get(request.sid)
    if not auth_info:
        emit('message', {"success": False, "message": "Not authenticated"})
        return

    # Use server-verified identity, ignoring any identity claims in payload
    authenticated_user_id = auth_info['user_id']
    authenticated_user_name = auth_info['user_name']

    # Inject authenticated identity into msg_data (overwriting any client-provided values)
    msg_data['user_name'] = authenticated_user_name
    msg_data['user'] = authenticated_user_name
    msg_data['user_id'] = authenticated_user_id
    msg_data['userId'] = authenticated_user_id

    # Process the incoming message with server-verified user identity
    resp = brighter_trades.process_incoming_message(
        msg_type=msg_type,
        msg_data=msg_data,
        socket_conn_id=request.sid,
        authenticated_user_id=authenticated_user_id
    )

    # Send the response back to the client
    if resp:
        emit('message', resp)


@app.route('/settings', methods=['POST'])
def settings():
    """
    This route is used to change settings that require a browser refresh.

    Options are:
    Data source options for the chart display:
        timeframe: For setting the time frame the of the viewport.
        trading_pair: For setting the market displayed in the viewport.
        exchange_name: For setting the exchange_name displayed in the viewport.
    Controls of the indicators:
        new_indicator: Creates a new indicator and stores it.
        edit_indicator: Edits the properties of specific indicators.
        toggle_indicator: Enables or disables display of indicators in the viewport.

    :return: None - Redirects to index, or returns JSON for async requests.
    """
    # Request the user_name from the client session.
    if not (user_name := session.get('user')):
        return jsonify({"success": False, "message": "Not logged in"}), 401

    # Return if the user is not logged in.
    if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'):
        return jsonify({"success": False, "message": "User not logged in"}), 401

    # Get the setting that the application wants to change.
    try:
        # Handle JSON or form submissions
        if request.is_json:
            data = request.json
            setting = data.get('setting')
            params = data.get('indicator', {})
        else:
            setting = request.form.get('setting')
            # Use to_dict(flat=False) to get lists for multi-value fields (like checkboxes)
            params = request.form.to_dict(flat=False)
            # Convert single-value lists back to single values, except for 'indicator'
            for key, value in params.items():
                if key != 'indicator' and isinstance(value, list) and len(value) == 1:
                    params[key] = value[0]

        if not setting:
            return jsonify({"success": False, "message": "No setting provided"}), 400

        # Change the setting.
        brighter_trades.adjust_setting(user_name=user_name, setting=setting, params=params)

        # Return success as JSON if called via an async request
        if request.is_json:
            return jsonify({"success": True}), 200

        # Redirect if this is a form submission (non-async request)
        return redirect('/app')

    except Exception as e:
        return jsonify({"success": False, "message": str(e)}), 500


@app.route('/docs')
def docs():
    """
    Documentation page - placeholder for now.
    """
    # TODO: Link to actual documentation when available
    flash('Documentation coming soon! Redirecting to the app.')
    return redirect('/app')


@app.route('/signup')
def signup():
    return render_template('sign_up.html', title='title')


@app.route('/signout')
def signout():
    if not (user_name := session.get('user')):
        return redirect('/')

    if brighter_trades.log_user_in_out(user_name=user_name, cmd='logout'):
        # Disconnect any active WebSocket connections for this user
        # to prevent continued access after logout
        sids_to_remove = []
        for sid, auth_info in list(socket_user_mapping.items()):
            if auth_info.get('user_name') == user_name:
                sids_to_remove.append(sid)

        for sid in sids_to_remove:
            socket_user_mapping.pop(sid, None)
            try:
                # Disconnect the socket (they'll need to re-authenticate)
                socketio.server.disconnect(sid)
            except Exception:
                pass  # Socket may already be closed

        # If the user was logged out successfully delete the session var.
        del session['user']

    return redirect('/')


@app.route('/login', methods=['GET'])
def login_page():
    """Display the login page."""
    return render_template('login_page.html', title='Login')


@app.route('/login', methods=['POST'])
def login():
    # Get the user_name and password from the form data
    username = request.form.get('username')
    password = request.form.get('password')

    # Validate the input
    if not username or not password:
        flash('Please provide both username and password.', 'login_error')
        return redirect('/login')

    # Attempt to log in the user
    success = brighter_trades.log_user_in_out(user_name=username, cmd='login', password=password)

    if success:
        # Store the user_name in the session
        session['user'] = username
        flash('Login successful!', 'success')
        return redirect('/app')
    else:
        flash('Invalid username or password. Please try again.', 'login_error')
        return redirect('/login')


@app.route('/signup_submit', methods=['POST'])
def signup_submit():
    email = request.form.get('email')
    username = request.form.get('user_name')
    password = request.form.get('password')

    # Validate email format
    try:
        validate_email(email)
    except EmailNotValidError as e:
        flash(f"Invalid email format: {e}", 'signup_error')
        return redirect('/signup')

    # Validate user_name and password
    if not username or not password:
        flash("Please provide both username and password.", 'signup_error')
        return redirect('/signup')

    # Create a new user
    success = brighter_trades.create_new_user(email=email, username=username, password=password)
    if success:
        session['user'] = username
        flash("Signup successful! You are now logged in.", 'success')
        return redirect('/app')
    else:
        flash("Username or email already exists. Please try a different one.", 'signup_error')
        return redirect('/signup')



@app.route('/api/indicator_init', methods=['POST', 'GET'])
def indicator_init():
    """
    Initializes the indicators and returns the data for a given symbol and timeframe.
    """
    try:
        data = request.get_json()
        username = resolve_user_name(data)

        if not username:
            return jsonify({'error': 'Invalid user name.'}), 400

        # Check if the user is logged in
        if not brighter_trades.get_user_info(user_name=username, info='Is logged in?'):
            return jsonify({'error': 'User is not logged in.'}), 401

        # Get the chart view for the user
        chart_view = brighter_trades.get_user_info(user_name=username, info='Chart View')

        # Get the indicator data
        source = {'user_name': username, 'market': chart_view}
        data = brighter_trades.get_indicator_data(user_name=username, source=source, start_ts=None, num_results=1000)
        # indicators={'EMA 5': {'visible': true, 'type': 'EMA', 'color': 'red' },
        # 'vol': {'visible': true, 'type': 'Volume'},'New Indicator': {'visible': true, 'type': 'nothing'}}
        return jsonify(data), 200
    except Exception as e:
        import traceback
        traceback.print_exc()
        return jsonify({'error': str(e)}), 500


@app.route('/api/edm-config', methods=['GET'])
def edm_config():
    """
    Returns the EDM (Exchange Data Manager) configuration for the frontend.
    This allows the frontend to connect directly to EDM for candle data.
    """
    edm_settings = brighter_trades.config.get_setting('edm') or {}
    return jsonify({
        'rest_url': edm_settings.get('rest_url', 'http://localhost:8080'),
        'ws_url': edm_settings.get('ws_url', 'ws://localhost:8080/ws'),
        'enabled': edm_settings.get('enabled', True),
    }), 200


@app.route('/api/chart-view', methods=['POST'])
def get_chart_view():
    """
    Returns the current chart view settings for a user.
    Used by the frontend to get exchange, market, and timeframe for EDM requests.
    """
    data = request.get_json() or {}
    user_name = data.get('user_name')

    if not user_name:
        return jsonify({'error': 'user_name is required'}), 400

    try:
        chart_view = brighter_trades.users.get_chart_view(user_name=user_name)
        if chart_view:
            return jsonify({
                'exchange': chart_view.get('exchange', 'binance'),
                'market': chart_view.get('market', 'BTC/USDT'),
                'timeframe': chart_view.get('timeframe', '1h'),
            }), 200
        else:
            # Return defaults if no chart view is set
            return jsonify({
                'exchange': 'binance',
                'market': 'BTC/USDT',
                'timeframe': '1h',
            }), 200
    except Exception as e:
        logging.error(f"Error getting chart view for {user_name}: {e}")
        return jsonify({
            'exchange': 'binance',
            'market': 'BTC/USDT',
            'timeframe': '1h',
        }), 200


@app.route('/api/generate-strategy', methods=['POST'])
def generate_strategy():
    """
    Generate a Blockly strategy from natural language description using AI.
    Calls the CmdForge strategy-builder tool.
    """
    data = request.get_json() or {}
    description = data.get('description', '').strip()
    indicators = data.get('indicators', [])
    signals = data.get('signals', [])
    default_source = data.get('default_source', {
        'exchange': 'binance',
        'market': 'BTC/USDT',
        'timeframe': '5m'
    })

    if not description:
        return jsonify({'error': 'Description is required'}), 400

    try:
        # Build input for the strategy-builder tool
        tool_input = json.dumps({
            'description': description,
            'indicators': indicators,
            'signals': signals,
            'default_source': default_source
        })

        # Call CmdForge strategy-builder tool
        result = subprocess.run(
            ['strategy-builder'],
            input=tool_input,
            capture_output=True,
            text=True,
            timeout=120  # 2 minute timeout for AI generation
        )

        if result.returncode != 0:
            error_msg = result.stderr.strip() or 'Strategy generation failed'
            logging.error(f"strategy-builder failed: {error_msg}")
            return jsonify({'error': error_msg}), 500

        workspace_xml = result.stdout.strip()

        # Validate the generated XML
        if not _validate_blockly_xml(workspace_xml):
            logging.error(f"Invalid Blockly XML generated: {workspace_xml[:200]}")
            return jsonify({'error': 'Generated strategy is invalid'}), 500

        return jsonify({
            'success': True,
            'workspace_xml': workspace_xml
        }), 200

    except subprocess.TimeoutExpired:
        logging.error("strategy-builder timed out")
        return jsonify({'error': 'Strategy generation timed out'}), 504
    except FileNotFoundError:
        logging.error("strategy-builder tool not found")
        return jsonify({'error': 'Strategy builder tool not installed'}), 500
    except Exception as e:
        logging.error(f"Error generating strategy: {e}")
        return jsonify({'error': str(e)}), 500


def _validate_blockly_xml(xml_string: str) -> bool:
    """Validate that the string is valid Blockly XML."""
    try:
        root = ET.fromstring(xml_string)
        # Check it's a Blockly XML document (handle namespace prefixed tags)
        # Tag can be 'xml' or '{namespace}xml'
        tag_name = root.tag.split('}')[-1] if '}' in root.tag else root.tag
        if tag_name != 'xml' and 'blockly' not in root.tag.lower():
            return False
        # Check it has at least one block using namespace-aware search
        # Use .//* to find all descendants, then filter by local name
        blocks = [elem for elem in root.iter()
                  if (elem.tag.split('}')[-1] if '}' in elem.tag else elem.tag) == 'block']
        return len(blocks) > 0
    except ET.ParseError:
        return False
    except Exception:
        return False


# =============================================================================
# Wallet API Routes
# =============================================================================

def _get_current_user_id():
    """Get user_id from session. Returns None if not logged in."""
    user_name = session.get('user')
    if not user_name:
        return None
    return brighter_trades.users.get_id(user_name)


# === User Profile APIs ===

@app.route('/api/user/profile', methods=['GET'])
def get_user_profile():
    """Get current user's profile info."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    user = brighter_trades.users.get_user_by_id(user_id)
    if user:
        return jsonify({
            'success': True,
            'profile': {
                'username': user.get('user_name'),
                'email': user.get('email', '')
            }
        })
    return jsonify({'success': False, 'error': 'User not found'}), 404


@app.route('/api/user/email', methods=['POST'])
def update_user_email():
    """Update current user's email address."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    data = request.get_json() or {}
    email = data.get('email', '').strip()

    if not email:
        return jsonify({'success': False, 'error': 'Email is required'}), 400

    # Basic email validation
    if '@' not in email or '.' not in email:
        return jsonify({'success': False, 'error': 'Invalid email format'}), 400

    result = brighter_trades.users.update_email(user_id, email)
    if result:
        return jsonify({'success': True})
    return jsonify({'success': False, 'error': 'Failed to update email'}), 500


@app.route('/api/user/password', methods=['POST'])
def update_user_password():
    """Update current user's password."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    data = request.get_json() or {}
    current_password = data.get('current_password', '')
    new_password = data.get('new_password', '')

    if not current_password or not new_password:
        return jsonify({'success': False, 'error': 'Both current and new password are required'}), 400

    if len(new_password) < 6:
        return jsonify({'success': False, 'error': 'Password must be at least 6 characters'}), 400

    result = brighter_trades.users.update_password(user_id, current_password, new_password)
    if result.get('success'):
        return jsonify({'success': True})
    return jsonify({'success': False, 'error': result.get('error', 'Failed to update password')}), 400


# === Wallet APIs ===

@app.route('/api/wallet', methods=['GET'])
def get_wallet():
    """Get current user's wallet info (without private key)."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    wallet = brighter_trades.get_user_wallet(user_id)
    if wallet:
        return jsonify({'success': True, 'wallet': wallet})
    return jsonify({'success': True, 'wallet': None})


@app.route('/api/wallet/create', methods=['POST'])
def create_wallet():
    """Create a new wallet for the current user (two-address model)."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    data = request.get_json() or {}
    user_sweep_address = data.get('user_sweep_address')  # Optional user-provided sweep address

    result = brighter_trades.create_user_wallet(user_id, user_sweep_address=user_sweep_address)
    return jsonify(result)


@app.route('/api/wallet/credits', methods=['GET'])
def get_credits():
    """Get user's spendable credits balance (from ledger)."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    balance = brighter_trades.get_credits_balance(user_id)
    return jsonify({'success': True, 'balance': balance})


@app.route('/api/wallet/withdraw', methods=['POST'])
def request_withdrawal():
    """Request BTC withdrawal (processed async)."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    data = request.get_json() or {}
    amount = data.get('amount_satoshis')
    destination = data.get('destination_address')

    if not amount or not destination:
        return jsonify({'success': False, 'error': 'Missing amount or destination'}), 400

    try:
        amount = int(amount)
    except (TypeError, ValueError):
        return jsonify({'success': False, 'error': 'Invalid amount'}), 400

    # SECURITY: Prevent negative withdrawal (would credit balance)
    if amount <= 0:
        return jsonify({'success': False, 'error': 'Amount must be positive'}), 400

    result = brighter_trades.request_withdrawal(user_id, amount, destination)
    return jsonify(result)


@app.route('/api/wallet/transactions', methods=['GET'])
def get_transactions():
    """Get user's recent transaction history."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    limit = request.args.get('limit', 20, type=int)
    transactions = brighter_trades.get_transaction_history(user_id, limit)
    return jsonify({'success': True, 'transactions': transactions})


@app.route('/api/wallet/keys', methods=['GET'])
def get_wallet_keys():
    """
    Get fee address keys (for viewing after wallet creation).
    Note: Sweep address private key is NOT stored and cannot be retrieved.
    """
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    # Keep key export disabled by default; can be opt-in for controlled POC sessions.
    wallet_cfg = brighter_trades.config.get_setting('wallet') or {}
    if not wallet_cfg.get('allow_key_export', False):
        return jsonify({'success': False, 'error': 'Key export is disabled'}), 403

    keys = brighter_trades.wallet_manager.get_wallet_keys(user_id)
    if keys:
        return jsonify({'success': True, **keys})
    return jsonify({'success': False, 'error': 'Wallet not found or keys unavailable'})


@app.route('/api/admin/credit', methods=['POST'])
def admin_credit_user():
    """
    Admin endpoint to manually credit a user's wallet.
    For POC testing until deposit detection is implemented.

    SECURITY: In POC mode, users can only credit themselves.
    In production, add proper admin role checking.
    """
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    data = request.get_json() or {}
    target_user_id = data.get('user_id')
    amount = data.get('amount_satoshis', 10000)  # Default 10k sats
    reason = data.get('reason', 'POC test credit')

    # SECURITY: In POC mode, only allow crediting yourself
    # Remove this restriction when proper admin auth is implemented
    if target_user_id is not None:
        try:
            if int(target_user_id) != user_id:
                return jsonify({'success': False, 'error': 'Can only credit your own account in POC mode'}), 403
        except (TypeError, ValueError):
            return jsonify({'success': False, 'error': 'Invalid user_id'}), 400

    target_user_id = user_id  # Force to self

    try:
        amount = int(amount)
    except (TypeError, ValueError):
        return jsonify({'success': False, 'error': 'Invalid amount'}), 400

    # SECURITY: Prevent negative credits
    if amount <= 0:
        return jsonify({'success': False, 'error': 'Amount must be positive'}), 400

    # SECURITY: Limit POC credits to prevent abuse
    MAX_POC_CREDIT = 100000  # 0.001 BTC max per credit
    if amount > MAX_POC_CREDIT:
        return jsonify({'success': False, 'error': f'POC credit limited to {MAX_POC_CREDIT} satoshis'}), 400

    result = brighter_trades.wallet_manager.admin_credit(
        user_id=target_user_id,
        amount_satoshis=amount,
        reason=reason
    )
    return jsonify(result)


# =============================================================================
# Manual Trading API Routes (Position-First)
# =============================================================================

@app.route('/api/manual/orders', methods=['GET'])
def get_manual_open_orders():
    """Get all open orders for the current user across all brokers."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not authenticated'}), 401

    try:
        orders = brighter_trades.manual_broker_manager.get_all_open_orders(user_id)
        return jsonify({'success': True, 'orders': orders})
    except Exception as e:
        logger.error(f"Error getting open orders: {e}", exc_info=True)
        return jsonify({'success': False, 'message': str(e)}), 500


@app.route('/api/manual/orders/<order_id>/cancel', methods=['POST'])
def cancel_manual_order(order_id):
    """Cancel a specific open order."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not authenticated'}), 401

    data = request.get_json() or {}
    broker_key = data.get('broker_key', 'paper')

    try:
        # First try to find the trade by broker_order_id
        trade = brighter_trades.trades.find_trade_by_broker_order_id(order_id)
        if trade:
            result = brighter_trades.trades.cancel_order(trade.unique_id)
        else:
            # Fall back to broker manager direct cancel
            result = brighter_trades.manual_broker_manager.cancel_order(user_id, order_id, broker_key)

        return jsonify(result)
    except Exception as e:
        logger.error(f"Error cancelling order {order_id}: {e}", exc_info=True)
        return jsonify({'success': False, 'message': str(e)}), 500


@app.route('/api/manual/positions', methods=['GET'])
def get_manual_positions():
    """Get all positions for the current user across all brokers."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not authenticated'}), 401

    try:
        positions = brighter_trades.manual_broker_manager.get_all_positions(user_id)
        return jsonify({'success': True, 'positions': positions})
    except Exception as e:
        logger.error(f"Error getting positions: {e}", exc_info=True)
        return jsonify({'success': False, 'message': str(e)}), 500


@app.route('/api/manual/positions/<path:symbol>/close', methods=['POST'])
def close_manual_position(symbol):
    """
    Close entire position for a symbol (position-first operation).

    This is the preferred close method - it closes the entire position
    rather than individual trades.
    """
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not authenticated'}), 401

    data = request.get_json() or {}
    broker_key = data.get('broker_key', 'paper')

    try:
        result = brighter_trades.trades.close_position(user_id, symbol, broker_key)
        return jsonify(result)
    except Exception as e:
        logger.error(f"Error closing position {symbol}: {e}", exc_info=True)
        return jsonify({'success': False, 'message': str(e)}), 500


@app.route('/api/manual/balance', methods=['GET'])
def get_manual_balance():
    """Get balance for a specific broker."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not authenticated'}), 401

    broker_key = request.args.get('broker_key', 'paper')

    try:
        total = brighter_trades.manual_broker_manager.get_broker_balance(user_id, broker_key)
        available = brighter_trades.manual_broker_manager.get_available_balance(user_id, broker_key)
        return jsonify({
            'success': True,
            'total': total,
            'available': available,
            'broker_key': broker_key
        })
    except Exception as e:
        logger.error(f"Error getting balance: {e}", exc_info=True)
        return jsonify({'success': False, 'message': str(e)}), 500


@app.route('/api/manual/orders/symbol/<path:symbol>/cancel', methods=['POST'])
def cancel_orders_for_symbol(symbol):
    """Cancel all resting orders for a symbol."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not authenticated'}), 401

    data = request.get_json() or {}
    broker_key = data.get('broker_key', 'paper')

    try:
        result = brighter_trades.trades.cancel_orders_for_symbol(user_id, symbol, broker_key)
        return jsonify(result)
    except Exception as e:
        logger.error(f"Error cancelling orders for {symbol}: {e}", exc_info=True)
        return jsonify({'success': False, 'message': str(e)}), 500


@app.route('/api/manual/history', methods=['GET'])
def get_trade_history():
    """Get trade history for current user."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not authenticated'}), 401

    limit = request.args.get('limit', 50, type=int)

    try:
        history = brighter_trades.trades.get_trade_history(user_id, limit)
        return jsonify({'success': True, 'history': history})
    except Exception as e:
        logger.error(f"Error getting trade history: {e}", exc_info=True)
        return jsonify({'success': False, 'message': str(e)}), 500


# =============================================================================
# External Sources API Routes
# =============================================================================

@app.route('/api/external_sources/test', methods=['POST'])
def test_external_source():
    """Test an external source API connection without saving."""
    data = request.get_json() or {}
    url = data.get('url')
    auth_header = data.get('auth_header', '')
    auth_key = data.get('auth_key', '')
    json_path = data.get('json_path', '$.data[0].value')

    if not url:
        return jsonify({'success': False, 'error': 'URL is required'})

    from ExternalSources import ExternalSource
    source = ExternalSource(
        name='test',
        url=url,
        auth_header=auth_header,
        auth_key=auth_key,
        json_path=json_path
    )

    result = brighter_trades.external_sources._fetch_value(source)
    return jsonify(result)


@app.route('/api/external_sources/create', methods=['POST'])
def create_external_source():
    """Create a new external source."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not logged in'}), 401

    data = request.get_json() or {}
    name = data.get('name')
    url = data.get('url')
    auth_header = data.get('auth_header', '')
    auth_key = data.get('auth_key', '')
    json_path = data.get('json_path', '$.data[0].value')
    refresh_interval = data.get('refresh_interval', 300)

    if not name or not url:
        return jsonify({'success': False, 'message': 'Name and URL are required'})

    result = brighter_trades.external_sources.create_source(
        name=name,
        url=url,
        auth_header=auth_header,
        auth_key=auth_key,
        json_path=json_path,
        refresh_interval=refresh_interval,
        creator_id=user_id
    )
    return jsonify(result)


@app.route('/api/external_sources/list', methods=['GET'])
def list_external_sources():
    """List all external sources for the current user."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    sources = brighter_trades.external_sources.get_sources_for_user(user_id)
    return jsonify({'success': True, 'sources': sources})


@app.route('/api/external_sources/refresh/<tbl_key>', methods=['POST'])
def refresh_external_source(tbl_key):
    """Refresh a specific external source."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    result = brighter_trades.external_sources.refresh_source(tbl_key)
    return jsonify(result)


@app.route('/api/external_sources/delete/<tbl_key>', methods=['DELETE'])
def delete_external_source(tbl_key):
    """Delete an external source."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not logged in'}), 401

    result = brighter_trades.external_sources.delete_source(tbl_key, user_id)
    return jsonify(result)


# =============================================================================
# External Indicators API Routes (Historical data for backtesting)
# =============================================================================

@app.route('/api/external_indicators/test', methods=['POST'])
def test_external_indicator():
    """Test an external indicator API configuration without saving."""
    data = request.get_json() or {}

    historical_url = data.get('historical_url')
    auth_header = data.get('auth_header', '')
    auth_key = data.get('auth_key', '')
    value_jsonpath = data.get('value_jsonpath', '$.data[*].value')
    timestamp_jsonpath = data.get('timestamp_jsonpath', '$.data[*].timestamp')
    date_format = data.get('date_format', 'ISO')
    date_param_format = data.get('date_param_format', 'ISO')

    if not historical_url:
        return jsonify({'success': False, 'error': 'Historical URL is required'})

    result = brighter_trades.external_indicators.test_indicator(
        historical_url=historical_url,
        auth_header=auth_header,
        auth_key=auth_key,
        value_jsonpath=value_jsonpath,
        timestamp_jsonpath=timestamp_jsonpath,
        date_format=date_format,
        date_param_format=date_param_format
    )
    return jsonify(result)


@app.route('/api/external_indicators/create', methods=['POST'])
def create_external_indicator():
    """Create a new external indicator."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not logged in'}), 401

    data = request.get_json() or {}
    name = data.get('name')
    historical_url = data.get('historical_url')
    auth_header = data.get('auth_header', '')
    auth_key = data.get('auth_key', '')
    value_jsonpath = data.get('value_jsonpath', '$.data[*].value')
    timestamp_jsonpath = data.get('timestamp_jsonpath', '$.data[*].timestamp')
    date_format = data.get('date_format', 'ISO')
    date_param_format = data.get('date_param_format', 'ISO')

    if not name or not historical_url:
        return jsonify({'success': False, 'message': 'Name and Historical URL are required'})

    result = brighter_trades.external_indicators.create_indicator(
        name=name,
        historical_url=historical_url,
        auth_header=auth_header,
        auth_key=auth_key,
        value_jsonpath=value_jsonpath,
        timestamp_jsonpath=timestamp_jsonpath,
        date_format=date_format,
        date_param_format=date_param_format,
        creator_id=user_id
    )
    return jsonify(result)


@app.route('/api/external_indicators/list', methods=['GET'])
def list_external_indicators():
    """List all external indicators for the current user."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'error': 'Not logged in'}), 401

    indicators = brighter_trades.external_indicators.get_indicators_for_user(user_id)
    return jsonify({'success': True, 'indicators': indicators})


@app.route('/api/external_indicators/delete/<tbl_key>', methods=['DELETE'])
def delete_external_indicator(tbl_key):
    """Delete an external indicator."""
    user_id = _get_current_user_id()
    if not user_id:
        return jsonify({'success': False, 'message': 'Not logged in'}), 401

    result = brighter_trades.external_indicators.delete_indicator(tbl_key, user_id)
    return jsonify(result)


# =============================================================================
# Health Check Routes
# =============================================================================

# =============================================================================
# Chart Analysis API Routes
# =============================================================================

@app.route('/api/detect_patterns', methods=['POST'])
def detect_patterns():
    """
    Detect all candlestick patterns in a given time range.

    This endpoint runs ALL CDL_* pattern indicators on the candle data
    within the specified time range and returns any detected patterns.

    Request body:
        user_name: str - The authenticated user
        exchange: str - Exchange name (e.g., 'binance')
        symbol: str - Trading pair (e.g., 'BTC/USDT')
        timeframe: str - Candle timeframe (e.g., '1h')
        start_time: int - Start timestamp (seconds)
        end_time: int - End timestamp (seconds)

    Returns:
        patterns: list - List of detected patterns with name and value
    """
    from indicators import indicators_registry, CandlestickPattern
    import pandas as pd
    import numpy as np

    data = request.get_json() or {}
    user_name = data.get('user_name')
    exchange = data.get('exchange', 'binance')
    symbol = data.get('symbol', 'BTC/USDT')
    timeframe = data.get('timeframe', '1h')
    start_time = data.get('start_time')
    end_time = data.get('end_time')

    if not user_name:
        return jsonify({'success': False, 'error': 'user_name is required'}), 400

    if not start_time or not end_time:
        return jsonify({'success': False, 'error': 'start_time and end_time are required'}), 400

    try:
        # Get candle data - fetch enough to cover historical chart data
        # Charts typically show ~500 candles, so fetch more to ensure coverage
        num_candles = 1000

        candles_df = brighter_trades.candles.get_last_n_candles(
            num_candles=num_candles,
            asset=symbol,
            timeframe=timeframe,
            exchange=exchange,
            user_name=user_name
        )

        if candles_df is None or candles_df.empty:
            return jsonify({'success': True, 'patterns': []})

        # Calculate context window based on timeframe
        # Pattern detection needs ~10 candles before for context (3-5 candle patterns)
        timeframe_seconds = {
            '1m': 60, '3m': 180, '5m': 300, '15m': 900, '30m': 1800,
            '1h': 3600, '2h': 7200, '4h': 14400, '6h': 21600,
            '12h': 43200, '1d': 86400, '1w': 604800
        }
        tf_secs = timeframe_seconds.get(timeframe, 3600)
        context_before = tf_secs * 10  # 10 candles of context

        # Filter candles to ONLY the clicked range plus minimal context
        context_candles = candles_df[
            (candles_df['time'] >= start_time - context_before) &
            (candles_df['time'] <= end_time)
        ].copy()

        if context_candles.empty:
            return jsonify({'success': True, 'patterns': []})

        # Get all CDL_* pattern indicators
        cdl_patterns = {
            name: cls for name, cls in indicators_registry.items()
            if name.startswith('CDL_')
        }

        detected_patterns = []

        # Extract OHLC data
        opens = context_candles['open'].to_numpy(dtype='float')
        highs = context_candles['high'].to_numpy(dtype='float')
        lows = context_candles['low'].to_numpy(dtype='float')
        closes = context_candles['close'].to_numpy(dtype='float')
        times = context_candles['time'].to_numpy()

        # Run each pattern detector
        for pattern_name, pattern_class in cdl_patterns.items():
            try:
                # Instantiate the pattern indicator
                pattern_indicator = pattern_class(
                    name=pattern_name,
                    indicator_type=pattern_name,
                    properties={}
                )

                # Run detection
                pattern_values = pattern_indicator.detect(opens, highs, lows, closes)

                # Check if any patterns were detected in the target range
                # Only keep one instance per pattern name (the most recent)
                pattern_found = False
                for i, (time_val, value) in enumerate(zip(times, pattern_values)):
                    if time_val >= start_time and time_val <= end_time and value != 0:
                        if not pattern_found:
                            # Format pattern name nicely
                            display_name = pattern_name.replace('CDL_', '').replace('_', ' ').title()
                            detected_patterns.append({
                                'name': display_name,
                                'value': int(value),  # 100 = bullish, -100 = bearish
                            })
                            pattern_found = True
                            break  # Only one per pattern type
            except Exception as e:
                logging.warning(f"Error detecting pattern {pattern_name}: {e}")
                continue

        # Remove duplicates by pattern name (keep first occurrence)
        seen = set()
        unique_patterns = []
        for p in detected_patterns:
            key = p['name']
            if key not in seen:
                seen.add(key)
                unique_patterns.append(p)

        return jsonify({
            'success': True,
            'patterns': unique_patterns
        })

    except Exception as e:
        logging.error(f"Error detecting patterns: {e}")
        return jsonify({'success': False, 'error': str(e)}), 500


# =============================================================================
# Health Check Routes
# =============================================================================

@app.route('/health/edm', methods=['GET'])
def edm_health():
    """
    Health check endpoint for EDM connectivity.
    """
    if brighter_trades.edm_client is None:
        return jsonify({'status': 'disabled', 'message': 'EDM client not initialized'}), 200

    try:
        healthy = brighter_trades.edm_client.health_check_sync()
        if healthy:
            return jsonify({'status': 'healthy'}), 200
        else:
            return jsonify({'status': 'unhealthy', 'message': 'EDM health check failed'}), 503
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 503


if __name__ == '__main__':
    # Start the strategy execution loop in the background
    start_strategy_loop()
    logging.info("Strategy execution loop started in background")

    # Start wallet background jobs (auto-sweep, deposit detection, withdrawal processing)
    start_wallet_background_jobs()

    socketio.run(app, host='127.0.0.1', port=5002, debug=False, use_reloader=False)
