"""
core/mt5_utils.py — Thread-sicherer MT5-Zugriff + Trading-Hilfsfunktionen
==========================================================================
Globaler Lock für die MetaTrader5-Lib (nicht thread-safe) sowie
alle kleinen Hilfsfunktionen die von TradeManager, TrailingManager
und MT5Data gemeinsam genutzt werden.
"""

from __future__ import annotations
import threading
from contextlib import contextmanager

import MetaTrader5 as mt5

from core.config import (
    get_margin_buffer, get_manual_margin,
    SL_TF, SL_LOOKBACK, SL_WINDOW, SL_BUFFER_TICKS,
)
from core.logger import get_logger

log = get_logger("mt5")

# ── Globaler MT5-Lock ──────────────────────────
# Die MT5 Python-Lib ist NICHT thread-safe. Alle MT5-Calls laufen
# über diesen Lock. Mit Timeout, damit ein hängender order_send
# (Server antwortet nicht) nicht alle anderen Threads blockiert.
_mt5_lock         = threading.Lock()
MT5_LOCK_TIMEOUT_S = 5


@contextmanager
def mt5_lock(timeout: float = MT5_LOCK_TIMEOUT_S):
    """
    Context-Manager für den globalen MT5-Lock mit Timeout.

      with mt5_lock() as got:
          if not got:
              return   # Lock nicht erhalten → Tick überspringen
          ... MT5-Calls ...
    """
    acquired = _mt5_lock.acquire(timeout=timeout)
    if not acquired:
        try:
            yield False
        finally:
            pass
    else:
        try:
            yield True
        finally:
            _mt5_lock.release()


# ── Tick / Symbol-Helpers ─────────────────────

def get_tick(sym: str):
    t = mt5.symbol_info_tick(sym)
    return t if (t and t.bid > 0 and t.ask > 0) else None


def get_filling(sym: str) -> int:
    si = mt5.symbol_info(sym)
    if si is None:
        return mt5.ORDER_FILLING_IOC
    for m in (mt5.ORDER_FILLING_FOK, mt5.ORDER_FILLING_IOC,
              mt5.ORDER_FILLING_RETURN):
        if si.filling_mode & m:
            return m
    return mt5.ORDER_FILLING_RETURN


def calc_lots(sym: str, price: float, otype: int) -> float:
    si, acc = mt5.symbol_info(sym), mt5.account_info()
    if not si or not acc:
        return 0.0
    mpl = mt5.order_calc_margin(otype, sym, 1.0, price)
    if not mpl or mpl <= 0:
        return 0.0
    step = si.volume_step
    # ── Manuell vorgegebene Einsatz-Margin (User-Wunsch 2026-08-04) ──────────
    # Ist sie gesetzt, wird die Position auf GENAU diesen Betrag gerechnet statt
    # auf einen Prozentsatz der freien Margin. Die freie Margin bleibt Obergrenze:
    # ein zu hoher Wunschwert wird gedeckelt, nicht an den Broker durchgereicht
    # (sonst Reject mit kryptischem retcode statt sauberer Meldung).
    _man = get_manual_margin()
    if _man > 0:
        _leistbar = acc.margin_free * get_margin_buffer()
        _ziel = min(_man, _leistbar)
        lots = round(((_ziel / mpl) // step) * step, 4)
        if lots < si.volume_min:
            log.warning(f"Einsatz-Margin {_man:.2f} reicht nicht für das Mindestlot "
                        f"({si.volume_min}) — Margin je Lot {mpl:.2f}")
            return 0.0
        if _ziel < _man:
            log.warning(f"Einsatz-Margin auf {_ziel:.2f} gedeckelt "
                        f"(gewünscht {_man:.2f}, frei×Buffer {_leistbar:.2f})")
        return min(lots, si.volume_max)
    lots = round(((acc.margin_free * get_margin_buffer() / mpl) // step) * step, 4)
    # Margin-Guard: reicht die freie Margin nicht mal fürs Mindestvolumen, 0.0
    # zurückgeben → Caller meldet sauber „Lot-Fehler" statt Broker-Reject mit
    # kryptischem retcode (früher: max(volume_min, …) erzwang unbezahlbare Größe).
    if lots < si.volume_min:
        return 0.0
    return min(lots, si.volume_max)


def calc_lots_risk(sym: str, price: float, otype: int,
                   sl_distance: float | None, risk_frac: float) -> float:
    """Risiko-basierte Lot-Größe: Verlust beim Initial-SL ≈ risk_frac × Equity.

    Ersetzt das All-in-auf-freie-Margin von `calc_lots`. Die freie Margin bleibt
    Obergrenze (Deckel), damit Mini-Konten nicht über die Margin hinaus ordern.
    Gibt 0.0 zurück, wenn Daten fehlen → Caller fällt auf `calc_lots` zurück.
    """
    si, acc = mt5.symbol_info(sym), mt5.account_info()
    if not si or not acc or not sl_distance or sl_distance <= 0:
        return 0.0
    step       = si.volume_step or 0.01
    tick_size  = si.trade_tick_size or si.point
    tick_value = si.trade_tick_value
    if not tick_size or not tick_value:
        return 0.0
    val_per_price = tick_value / tick_size              # € je 1.0 Preis je 1 Lot
    equity = acc.equity or acc.balance or 0.0
    if equity <= 0:
        return 0.0
    raw  = (equity * risk_frac) / (sl_distance * val_per_price)
    lots = (raw // step) * step
    # Margin-Deckel: nie mehr als die freie Margin (× Buffer) zulässt
    mpl = mt5.order_calc_margin(otype, sym, 1.0, price)
    if mpl and mpl > 0:
        max_aff = ((acc.margin_free * get_margin_buffer() / mpl) // step) * step
        lots = min(lots, max_aff)
    lots = min(round(lots, 4), si.volume_max)
    # Ergibt die Risiko-Rechnung WENIGER als das Mindestlot, NICHT auf volume_min
    # aufrunden (das überschritte still das gewollte Risiko) → 0.0, Caller lehnt ab
    # (Fix 2026-07-19; vorher max(volume_min, …)).
    if lots < si.volume_min:
        return 0.0
    return lots


def atr_value(sym: str, tf: int = SL_TF, period: int = 14) -> float | None:
    """ATR (Wilder-vereinfacht: Mittel der True Ranges) auf `tf`."""
    r = mt5.copy_rates_from_pos(sym, tf, 0, period + 1)
    if r is None or len(r) < period + 1:
        return None
    trs = []
    for i in range(1, len(r)):
        h, l, pc = float(r[i]["high"]), float(r[i]["low"]), float(r[i - 1]["close"])
        trs.append(max(h - l, abs(h - pc), abs(l - pc)))
    return sum(trs) / len(trs) if trs else None


def pivot_low(sym: str, max_above: float):
    r = mt5.copy_rates_from_pos(sym, SL_TF, 0, SL_LOOKBACK)
    if r is None or len(r) < 2 * SL_WINDOW + 1:
        return None
    lows = [float(x["low"]) for x in r]
    for i in range(len(lows) - SL_WINDOW - 1, SL_WINDOW - 1, -1):
        c = lows[i]
        if (c < max_above
                and c < min(lows[i - SL_WINDOW:i])
                and c < min(lows[i + 1:i + 1 + SL_WINDOW])):
            return c
    return None


def pivot_high(sym: str, min_below: float):
    r = mt5.copy_rates_from_pos(sym, SL_TF, 0, SL_LOOKBACK)
    if r is None or len(r) < 2 * SL_WINDOW + 1:
        return None
    highs = [float(x["high"]) for x in r]
    for i in range(len(highs) - SL_WINDOW - 1, SL_WINDOW - 1, -1):
        c = highs[i]
        if (c > min_below
                and c > max(highs[i - SL_WINDOW:i])
                and c > max(highs[i + 1:i + 1 + SL_WINDOW])):
            return c
    return None
