#!/usr/bin/env python3
"""
Универсальный монитор сделок на БОЕВОМ счёте — контроль TP/SL и безубыточного SL.

Логика:
  1. Каждые N минут запрашивает текущую цену тикера через T-Bank API (real).
  2. Детектирует TP исполнение по исчезновению стоп-ордера со счёта.
  3. При достижении TP — переводит SL в безубыточок (опционально).
  4. При достижении SL — закрывает позицию.
  5. Все операции выполняются на РЕАЛЬНОМ счёте.
  6. Все события пишутся в лог-файл reports/monitor_{ticker}.log.

Использование:
    python src/monitor_trade.py --ticker SBER --trade-id <trade_id> --daemon
    python src/monitor_trade.py --ticker SBER --trade-id <trade_id> --once
    python src/monitor_trade.py --ticker SBER --trade-id <trade_id> --daemon --interval 300

Зависимости:
    - T-Bank API (src.api.tbank)
    - Python 3.10+
"""

import argparse
import json
import math
import os
import sys
import time
import uuid
from datetime import datetime, time as dt_time

import requests

# Добавляем корень проекта в sys.path
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, PROJECT_ROOT)

from src.api.tbank import set_tbank_env, _find_figi, get_account_balance, get_stop_orders, cancel_stop_order, place_stop_loss

# ── Конфигурация ──────────────────────────────────────────
TRADES_FILE = os.path.join(PROJECT_ROOT, "reports", "trades.json")
LOG_DIR = os.path.join(PROJECT_ROOT, "reports", "monitor_logs")

# MOEX trading hours (MSK)
SESSION_START = dt_time(6, 50)
SESSION_END = dt_time(18, 40)

# ── Глобальные переменные API (инициализируются при старте) ──
_API_TOKEN = None
_BASE_URL = None
_ACCOUNT_ID = None


def log(ticker: str, msg: str, level: str = "INFO"):
    """Запись в лог-файл и stdout."""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{timestamp}] [{level}] {msg}"
    print(line)
    
    # Создаем директорию для логов если не существует
    os.makedirs(LOG_DIR, exist_ok=True)
    
    # Записываем в лог-файл для этого тикера
    log_file = os.path.join(LOG_DIR, f"monitor_{ticker.lower()}.log")
    with open(log_file, "a", encoding="utf-8") as f:
        f.write(line + "\n")


def _api_headers() -> dict:
    return {
        "Authorization": f"Bearer {_API_TOKEN}",
        "Content-Type": "application/json",
    }


def _api_post(endpoint: str, payload: dict) -> dict:
    """Выполнить POST-запрос к T-Bank REST API (real)."""
    url = f"{_BASE_URL}/{endpoint}"
    try:
        resp = requests.post(url, json=payload, headers=_api_headers(), timeout=15)
        resp.raise_for_status()
        return resp.json()
    except requests.exceptions.RequestException as e:
        error_body = ""
        try:
            error_body = e.response.text if e.response else ""
        except Exception:
            pass
        return {"error": str(e), "details": error_body[:400]}


def get_current_price(ticker: str) -> float | None:
    """Получить текущую цену тикера через T-Bank API (real)."""
    figi = _find_figi(ticker)
    endpoint = "tinkoff.public.invest.api.contract.v1.MarketDataService/GetLastPrices"
    result = _api_post(endpoint, {"figi": [figi]})
    prices = result.get("lastPrices", [])
    if prices:
        p = prices[0]["price"]
        return int(p["units"]) + p["nano"] / 1e9
    return None


def get_breakeven_sl(entry_price: float, quantity: int, entry_commission: float,
                     commission_pct: float = 0.3) -> float:
    """
    Рассчитать цену безубытка с учётом комиссий.
    
    TotalCost = entry_price × quantity + entry_commission
    NetProceeds(p) = p × quantity × (1 − commission_pct/100)
    Breakeven: p = TotalCost / (quantity × (1 − c))
    """
    total_cost = entry_price * quantity + entry_commission
    net_factor = quantity * (1 - commission_pct / 100)
    breakeven = total_cost / net_factor
    # Округляем вверх до 0.5 ₽ для безопасности
    return math.ceil(breakeven * 2) / 2


def is_market_open() -> bool:
    """Проверка: открыта ли торговая сессия MOEX."""
    now = datetime.now()
    if now.weekday() >= 5:  # Суббота, воскресенье
        return False
    return SESSION_START <= now.time() <= SESSION_END


def load_trade(trade_id: str) -> dict | None:
    """Загрузить сделку из trades.json."""
    if not os.path.exists(TRADES_FILE):
        log("S", "trades.json не найден", "ERROR")
        return None
    with open(TRADES_FILE, "r", encoding="utf-8") as f:
        data = json.load(f)
    trade = data.get(trade_id)
    if not trade:
        log("S", f"Сделка {trade_id[:8]} не найдена", "ERROR")
        return None
    if trade.get("status") != "OPEN":
        log("S", f"Сделка {trade_id[:8]} уже закрыта (status={trade.get('status')})", "INFO")
        return None
    return trade


def save_trade(trade: dict):
    """Сохранить обновлённую сделку в trades.json."""
    with open(TRADES_FILE, "r", encoding="utf-8") as f:
        data = json.load(f)
    data[trade["trade_id"]] = trade
    with open(TRADES_FILE, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False, default=str)


def execute_breakeven_sl(ticker: str, trade: dict, price: float):
    """
    Выполнить перевод SL в безубыточок на реальном счёте.
    """
    entry_price = trade["entry_price"]
    entry_commission = trade.get("entry_commission", 0.0)
    remaining_lots = trade["entry_quantity_lots"]
    
    # Рассчитываем безубыток
    be_sl = get_breakeven_sl(entry_price, trade["entry_quantity_shares"], entry_commission)
    
    # Отменяем старый SL
    old_sl_order_id = trade.get("sl_order_id")
    if old_sl_order_id:
        log(ticker, f"Отмена старого SL (id={old_sl_order_id[:12]})...")
        if not cancel_stop_order(old_sl_order_id):
            log(ticker, "⚠️ Не удалось отменить старый SL через API — проверьте вручную!", "ALERT")
    
    # Выставляем новый SL
    new_sl_id = place_stop_loss(
        lots=remaining_lots,
        stop_price=be_sl,
        limit_price=be_sl
    )
    
    if new_sl_id is None:
        log(ticker, "❌ КРИТИЧНО: не удалось выставить новый SL! Ручное вмешательство необходимо!", "ALERT")
        return
    
    # Обновляем trades.json
    old_sl = trade["sl_price"]
    trade["sl_price"] = be_sl
    trade["sl_be_price"] = be_sl
    trade["sl_be_triggered"] = True
    trade["sl_be_triggered_at"] = datetime.now().isoformat()
    trade["sl_be_triggered_price"] = price
    trade["sl_be_new_order_id"] = new_sl_id
    trade["sl_be_note"] = (
        f"TP достигнут при {price:.1f} ₽. "
        f"SL переведён {old_sl:.1f} → {be_sl:.1f} ₽ (безубыток + комиссии)."
    )
    save_trade(trade)
    
    log(ticker, f"🎯 TP достигнут! Цена={price:.1f} ₽. SL: {old_sl:.1f} → {be_sl:.1f} ₽ (безубыток).", "ALERT")


def check_tp_executed(ticker: str, trade: dict) -> bool:
    """
    Проверить, был ли TP исполнен, даже если текущая цена ниже TP.
    """
    tp_order_id = trade.get("tp_order_id")
    if not tp_order_id:
        return False
    
    stop_orders = get_stop_orders()
    tp_still_exists = any(
        o.get("stopOrderId") == tp_order_id for o in stop_orders
    )
    
    # TP-ордер исчез → он исполнился или был отменён
    if not tp_still_exists:
        log(ticker, f"⚠️ TP stop-order ({tp_order_id[:12]}) отсутствует на счёте — вероятно, исполнен!", "WARN")
        return True
    
    return False


def check_trade(ticker: str, trade_id: str) -> dict | None:
    """Основная логика проверки сделки. Возвращает словарь с результатами."""
    trade = load_trade(trade_id)
    if not trade:
        return None
    
    entry_price = trade["entry_price"]
    tp_price = trade.get("tp_price", 0)
    sl_price = trade["sl_price"]
    current_sl = trade.get("sl_price", sl_price)
    sl_be_triggered = trade.get("sl_be_triggered", False)
    
    price = get_current_price(ticker)
    if price is None:
        log(ticker, "Не удалось получить цену", "WARN")
        return {"status": "error", "reason": "no_price"}
    
    entry_pnl_pct = (price - entry_price) / entry_price * 100
    distance_to_tp = tp_price - price if tp_price > 0 else 0
    distance_to_sl = price - current_sl
    
    result = {
        "status": "ok",
        "price": price,
        "entry_price": entry_price,
        "pnl_pct": round(entry_pnl_pct, 2),
        "distance_to_tp": round(distance_to_tp, 2),
        "distance_to_sl": round(distance_to_sl, 2),
        "sl_be_triggered": sl_be_triggered,
    }
    
    # ── Проверка TP исполнения ──
    tp_executed = False
    
    # Метод 1: цена >= TP (прямое наблюдение)
    if tp_price > 0 and price >= tp_price:
        tp_executed = True
        log(ticker, f"🎯 TP обнаружен по цене: {price:.1f} ≥ {tp_price:.1f}", "INFO")
    
    # Метод 2: TP-ордер исчез со счёта (исполнился между проверками)
    if not tp_executed and not sl_be_triggered and tp_price > 0:
        if check_tp_executed(ticker, trade):
            tp_executed = True
            log(ticker, f"🎯 TP обнаружен по отсутствию ордера на счёте (текущая цена: {price:.1f})", "INFO")
    
    if tp_executed and not sl_be_triggered:
        execute_breakeven_sl(ticker, trade, price)
        result["action"] = "sl_to_breakeven"
        result["new_sl"] = trade.get("sl_be_price", 0)
        return result
    
    # ── Проверка SL срабатывания ──
    if price <= current_sl:
        log(ticker, f"🛑 Цена {price:.1f} ≤ SL {current_sl:.1f} — стоп-лосс сработал!", "ALERT")
        # Обновляем статус сделки
        trade["status"] = "CLOSED"
        trade["exit_price"] = price
        trade["exit_reason"] = "SL"
        trade["exit_time"] = datetime.now().isoformat()
        save_trade(trade)
        result["action"] = "sl_hit"
        return result
    
    # ── Предупреждение о приближении к SL ──
    if price <= current_sl * 1.01:  # в пределах 1% от SL
        log(ticker, f"⚠️  Цена {price:.1f} близка к SL {current_sl:.1f}! "
                   f"Расстояние: {distance_to_sl:.1f} ₽ ({distance_to_sl/current_sl*100:.1f}%)", "WARN")
        result["action"] = "sl_warning"
    
    log(ticker, f"{ticker}: {price:.1f} ₽ | PnL {entry_pnl_pct:+.2f}% | "
               f"До TP: {distance_to_tp:.1f} | До SL: {distance_to_sl:.1f} | "
               f"SL_be: {'✅' if sl_be_triggered else '⏳'}")
    result["action"] = "hold"
    return result


def verify_api_state(ticker: str):
    """Проверить и вывести текущее состояние стоп-ордеров на счёте."""
    stop_orders = get_stop_orders()
    ticker_orders = [o for o in stop_orders
                    if o.get("figi") == _find_figi(ticker) or o.get("ticker") == ticker]
    log(ticker, f"Стоп-ордеров {ticker} на счёте: {len(ticker_orders)}")
    for o in ticker_orders:
        sp = o.get("stopPrice", {})
        sp_val = int(sp.get("units", 0)) + sp.get("nano", 0) / 1e9
        lp = o.get("price", {})
        lp_val = int(lp.get("units", 0)) + lp.get("nano", 0) / 1e9
        oid = o.get("stopOrderId", "?")[:12]
        stype = o.get("stopOrderType", "?").replace("STOP_ORDER_TYPE_", "")
        lots = o.get("lotsRequested", "?")
        log(ticker, f"  {oid} | {stype} | stop={sp_val:.2f} limit={lp_val:.2f} | {lots} lots")


def init_api():
    """Инициализация API — переключение на реальный счёт."""
    global _API_TOKEN, _BASE_URL, _ACCOUNT_ID
    set_tbank_env('real')
    from src.api.tbank import API_TOKEN, BASE_URL, ACCOUNT_ID
    _API_TOKEN = API_TOKEN
    _BASE_URL = BASE_URL
    _ACCOUNT_ID = ACCOUNT_ID
    log("S", f"🔴 T-Bank API: REAL account {_ACCOUNT_ID}")


def run_daemon(ticker: str, trade_id: str, interval_sec: int = 900):
    """Фоновый режим мониторинга. Завершается при SL, TP или безубытке."""
    log(ticker, f"🟢 Монитор {ticker} (БОЕВОЙ СЧЁТ) запущен. Интервал: {interval_sec}с ({interval_sec//60} мин)")
    log(ticker, f"   SL:  {trade.get('sl_price', 'N/A')} ₽ → стоп-лосс")
    log(ticker, f"   TP:  {trade.get('tp_price', 'N/A')} ₽ → тейк-профит")
    log(ticker, f"   SL_be: {'✅' if trade.get('sl_be_triggered') else '⏳'} → безубыточный SL")
    verify_api_state(ticker)
    
    while True:
        try:
            if not is_market_open():
                now = datetime.now()
                log(ticker, f"Рынок закрыт (ждём {SESSION_START.strftime('%H:%M')}–{SESSION_END.strftime('%H:%M')} МСК, сегодня {now.strftime('%a')})", "INFO")
                time.sleep(interval_sec)
                continue
            
            result = check_trade(ticker, trade_id)
            if result is None:
                log(ticker, "Сделка не найдена или закрыта — завершение монитора", "INFO")
                break
            
            action = result.get("action", "hold")
            
            # ── Терминальные события: завершение работы ──
            if action == "sl_to_breakeven":
                verify_api_state(ticker)
                log(ticker, "🏁 TP достигнут. SL переведён в безубыток. Монитор завершает работу.", "ALERT")
                break
            
            if action == "sl_hit":
                log(ticker, "🛑 SL СРАБОТАЛ! Позиция закрыта. Монитор завершает работу.", "ALERT")
                break
            
            time.sleep(interval_sec)
            
        except KeyboardInterrupt:
            log(ticker, "⏹ Монитор остановлен пользователем")
            break
        except Exception as e:
            log(ticker, f"Ошибка в цикле: {e}", "ERROR")
            time.sleep(60)


def main():
    parser = argparse.ArgumentParser(
        description="Универсальный монитор сделок на БОЕВОМ СЧЁТЕ — TP/SL + безубыточный SL"
    )
    parser.add_argument("--ticker", required=True, help="Тикер для мониторинга")
    parser.add_argument("--trade-id", required=True, help="ID сделки из trades.json")
    parser.add_argument("--daemon", action="store_true",
                        help="Фоновый режим: проверка каждые 15 минут")
    parser.add_argument("--once", action="store_true",
                        help="Однократная проверка")
    parser.add_argument("--interval", type=int, default=900,
                        help="Интервал проверки в секундах (по умолч. 900 = 15 мин)")
    parser.add_argument("--verify", action="store_true",
                        help="Проверить состояние стоп-ордеров на счёте и выйти")
    args = parser.parse_args()
    
    init_api()
    
    # Загружаем сделку для проверки
    trade = load_trade(args.trade_id)
    if not trade:
        log(args.ticker, "❌ Сделка не найдена или закрыта")
        sys.exit(1)
    
    if args.verify:
        verify_api_state(args.ticker)
        return
    
    if args.daemon:
        run_daemon(args.ticker, args.trade_id, interval_sec=args.interval)
    else:
        log(args.ticker, f"🔍 Однократная проверка сделки {args.ticker} (БОЕВОЙ СЧЁТ)...")
        verify_api_state(args.ticker)
        result = check_trade(args.ticker, args.trade_id)
        if result is None:
            log(args.ticker, "❌ Сделка не найдена или закрыта")
            sys.exit(1)
        
        action = result.get("action", "hold")
        if action == "sl_to_breakeven":
            log(args.ticker, f"✅ SL переведён в безубыточок ({result.get('new_sl', '?')} ₽)")
        elif action == "sl_hit":
            log(args.ticker, f"🛑 SL сработал! Цена: {result.get('price', '?')} ₽")
        elif action == "sl_warning":
            log(args.ticker, f"⚠️  Цена близка к SL! До SL: {result['distance_to_sl']:.1f} ₽")
        else:
            log(args.ticker, f"Статус: {result['price']:.1f} ₽ | PnL {result['pnl_pct']:+.2f}% | "
                       f"до TP: {result['distance_to_tp']:.1f} | до SL: {result['distance_to_sl']:.1f}")


if __name__ == "__main__":
    main()