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

Логика:
  1. Каждые 15 минут запрашивает текущую цену PLZL через T-Bank API (real).
   2. Детектирует TP1 двумя методами:
        a) цена ≥ 2,006.5 ₽ (прямое наблюдение)
        b) TP1 stop-order исчез со счёта (исполнился между проверками)
      При срабатывании: отменяет старый SL, выставляет новый SL в безубыток
      (1,937 ₽) для оставшихся 6 лотов.
   3. При достижении ценой TP2 (2,100.0 ₽) — логирует алерт.
  4. Все операции выполняются на РЕАЛЬНОМ счёте.
  5. Все события пишутся в лог-файл reports/monitor_plzl.log.

Актуальные ордера на счёте (30.06 утро):
  - SL_be:  81c455c5... | STOP_LIMIT | stop=1937.00 | 6 lots (безубыток)
  - TP2:    259a6242... | TAKE_PROFIT | stop=2100.00 | 6 lots
  - TP1 ИСПОЛНЕН: 5 lots проданы ~2006.5, позиция сокращена 11→6

Использование:
    python src/monitor_plzl_trade.py --once       # однократная проверка
    python src/monitor_plzl_trade.py --daemon     # фоновый режим (каждые 15 мин)
    python src/monitor_plzl_trade.py --daemon --interval 300  # каждые 5 мин

Зависимости:
    - 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

# ── Конфигурация ──────────────────────────────────────────
TRADES_FILE = os.path.join(PROJECT_ROOT, "reports", "trades.json")
LOG_FILE = os.path.join(PROJECT_ROOT, "reports", "monitor_plzl.log")
TRADE_ID = "9534cea2-a315-4419-94bb-218ec4be4da2"

# Текущие stop-order ID на реальном счёте (зафиксированы 29.06)
SL_ORDER_ID = "38fc9586-7755-4614-84e6-4b6586d05480"   # SL 1886, 11 lots
TP1_ORDER_ID = "02da6147-007e-4703-8841-3d32dd3a4844"  # TP 2006.5, 5 lots
TP2_ORDER_ID = "259a6242-8143-4496-9ff6-c35c6f0f8c18"  # TP 2100, 6 lots

FIGI = "BBG000R607Y3"

# ── 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(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(os.path.dirname(LOG_FILE), exist_ok=True)
    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() -> float | None:
    """Получить текущую цену PLZL через T-Bank API (real)."""
    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_stop_orders() -> list[dict]:
    """Получить список активных стоп-ордеров на реальном счёте."""
    endpoint = "tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
    result = _api_post(endpoint, {"accountId": _ACCOUNT_ID})
    return result.get("stopOrders", [])


def cancel_stop_order(stop_order_id: str) -> bool:
    """Отменить стоп-ордер по ID."""
    endpoint = "tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
    result = _api_post(endpoint, {
        "accountId": _ACCOUNT_ID,
        "stopOrderId": stop_order_id,
    })
    if "error" in result:
        log(f"Ошибка отмены стоп-ордера {stop_order_id[:12]}: {result['error']}", "ERROR")
        return False
    log(f"✅ Стоп-ордер {stop_order_id[:12]} отменён")
    return True


def place_stop_loss(lots: int, stop_price: float, limit_price: float | None = None) -> str | None:
    """
    Выставить STOP-LOSS ордер на реальном счёте.

    Returns:
        ID нового ордера или None при ошибке.
    """
    if limit_price is None:
        limit_price = stop_price

    endpoint = "tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
    payload = {
        "figi": FIGI,
        "quantity": str(lots),
        "price": {"currency": "rub", "units": str(int(limit_price)),
                   "nano": int(round((limit_price - int(limit_price)) * 1e9))},
        "stopPrice": {"currency": "rub", "units": str(int(stop_price)),
                       "nano": int(round((stop_price - int(stop_price)) * 1e9))},
        "direction": "STOP_ORDER_DIRECTION_SELL",
        "stopOrderType": "STOP_ORDER_TYPE_STOP_LIMIT",
        "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",
        "orderId": str(uuid.uuid4()),
        "accountId": _ACCOUNT_ID,
    }
    result = _api_post(endpoint, payload)
    if "error" in result:
        log(f"Ошибка выставления SL: {result['error']} | {result.get('details','')}", "ERROR")
        return None
    new_id = result.get("stopOrderId", "")
    log(f"✅ Новый SL: {stop_price:.1f} ₽ × {lots} lots (id={new_id[:12]})")
    return new_id


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() -> dict | None:
    """Загрузить сделку PLZL из trades.json."""
    if not os.path.exists(TRADES_FILE):
        log("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(f"Сделка {TRADE_ID[:8]} не найдена", "ERROR")
        return None
    if trade.get("status") != "OPEN":
        log(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_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(trade: dict, price: float):
    """
    Выполнить перевод SL в безубыток на реальном счёте:
    1. Отменить старый SL (1,886 для 11 lots)
    2. Выставить новый SL для оставшихся 6 lots на уровне безубытка
    3. Обновить trades.json
    """
    entry_price = trade["entry_price"]
    entry_commission = trade.get("entry_commission", 0.0)
    remaining_lots = 6  # после исполнения TP1 (5 lots) остаётся 6

    # Отменяем старый SL
    log(f"Отмена старого SL (id={SL_ORDER_ID[:12]})...")
    if not cancel_stop_order(SL_ORDER_ID):
        log("⚠️ Не удалось отменить старый SL через API — проверьте вручную!", "ALERT")

    # Рассчитываем безубыток для оставшихся 6 lots
    # Важно: безубыток считаем от ВСЕЙ позиции (11 shares), т.к. комиссия входа уже уплачена
    # за все 11. Но SL ставим только на оставшиеся 6.
    be_sl = get_breakeven_sl(entry_price, 11, entry_commission)

    # Выставляем новый SL
    new_sl_id = place_stop_loss(remaining_lots, be_sl)
    if new_sl_id is None:
        log("❌ КРИТИЧНО: не удалось выставить новый 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"TP1 (2,006.5) достигнут при {price:.1f} ₽. "
        f"SL переведён {old_sl:.1f} → {be_sl:.1f} ₽ (безубыток + комиссии). "
        f"Оставшиеся 6 lots защищены."
    )
    save_trade(trade)

    log(
        f"🎯 TP1 ДОСТИГНУТ! Цена={price:.1f} ₽. "
        f"SL: {old_sl:.1f} → {be_sl:.1f} ₽ для оставшихся 6 lots (безубыток).",
        "ALERT"
    )


def check_tp1_executed() -> bool:
    """
    Проверить, был ли TP1 исполнен, даже если текущая цена ниже TP1.

    Проверяет:
      1. Исчез ли TP1 stop-order (02da6147...) из списка активных стоп-ордеров.
      2. Сократилась ли позиция (было 11 заблокировано, стало 6 свободных).

    Returns:
        True если TP1 точно был исполнен.
    """
    stop_orders = get_stop_orders()
    tp1_still_exists = any(
        o.get("stopOrderId") == TP1_ORDER_ID for o in stop_orders
    )

    # TP1-ордер исчез → он исполнился или был отменён
    if not tp1_still_exists:
        log("⚠️ TP1 stop-order (02da6147...) отсутствует на счёте — вероятно, исполнен!", "WARN")
        return True

    return False


def check_trade() -> dict | None:
    """Основная логика проверки сделки. Возвращает словарь с результатами."""
    trade = load_trade()
    if not trade:
        return None

    entry_price = trade["entry_price"]
    tp1 = trade["tp_price"]        # 2006.5
    tp2 = trade.get("tp2_price")    # 2100.0
    current_sl = trade["sl_price"]  # 1886.0 (или 1937 после безубытка)
    sl_be_triggered = trade.get("sl_be_triggered", False)

    price = get_current_price()
    if price is None:
        log("Не удалось получить цену", "WARN")
        return {"status": "error", "reason": "no_price"}

    entry_pnl_pct = (price - entry_price) / entry_price * 100
    distance_to_tp1 = tp1 - price
    distance_to_sl = price - current_sl

    result = {
        "status": "ok",
        "price": price,
        "entry_price": entry_price,
        "pnl_pct": round(entry_pnl_pct, 2),
        "distance_to_tp1": round(distance_to_tp1, 2),
        "distance_to_sl": round(distance_to_sl, 2),
        "sl_be_triggered": sl_be_triggered,
    }

    # ── Проверка TP1: два метода детекции ──────────────────
    tp1_executed = False

    # Метод 1: цена >= TP1 (прямое наблюдение)
    if price >= tp1:
        tp1_executed = True
        log(f"🎯 TP1 обнаружен по цене: {price:.1f} ≥ {tp1:.1f}", "INFO")

    # Метод 2: TP1-ордер исчез со счёта (исполнился между проверками)
    if not tp1_executed and not sl_be_triggered:
        if check_tp1_executed():
            tp1_executed = True
            log(f"🎯 TP1 обнаружен по отсутствию ордера на счёте (текущая цена: {price:.1f})", "INFO")

    if tp1_executed and not sl_be_triggered:
        execute_breakeven_sl(trade, price)
        result["action"] = "sl_to_breakeven"
        result["new_sl"] = trade.get("sl_be_price", 0)
        return result

    # ── Проверка TP2 ──
    if tp2 and price >= tp2:
        log(
            f"🏁 TP2 ({tp2:.1f}) ДОСТИГНУТ! Цена={price:.1f} ₽. "
            f"TP2 take-profit ордер должен был исполниться (6 lots @ 2,100). "
            f"Проверьте счёт.",
            "ALERT"
        )
        result["action"] = "tp2_reached"
        result["tp2_price"] = tp2
        return result

    # ── Предупреждение о приближении к SL ──
    if price <= current_sl * 1.01:  # в пределах 1% от SL
        log(
            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(
        f"PLZL: {price:.1f} ₽ | PnL {entry_pnl_pct:+.2f}% | "
        f"До TP1: {distance_to_tp1:.1f} | До SL: {distance_to_sl:.1f} | "
        f"SL_be: {'✅' if sl_be_triggered else '⏳'} | "
        f"TP2: {'🏁' if tp2 and price >= tp2 else '⏳'}"
    )
    result["action"] = "hold"
    return result


def verify_api_state():
    """Проверить и вывести текущее состояние стоп-ордеров на счёте."""
    stop_orders = get_stop_orders()
    plzl_orders = [o for o in stop_orders
                   if o.get("figi") == FIGI or o.get("ticker") == "PLZL"]
    log(f"Стоп-ордеров PLZL на счёте: {len(plzl_orders)}")
    for o in plzl_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(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(f"🔴 T-Bank API: REAL account {_ACCOUNT_ID}")


def run_daemon(interval_sec: int = 900):
    """Фоновый режим мониторинга. Завершается при SL, TP1 или TP2."""
    log(f"🟢 Монитор PLZL (БОЕВОЙ СЧЁТ) запущен. Интервал: {interval_sec}с ({interval_sec//60} мин)")
    log(f"   SL:  1,886.0 ₽ → стоп-лосс, позиция закрывается")
    log(f"   TP1: 2,006.5 ₽ → отмена SL + установка SL_be 1,937 → ЗАВЕРШЕНИЕ")
    log(f"   TP2: 2,100.0 ₽ → алерт → ЗАВЕРШЕНИЕ")
    verify_api_state()

    while True:
        try:
            if not is_market_open():
                now = datetime.now()
                log(f"Рынок закрыт (ждём {SESSION_START.strftime('%H:%M')}–{SESSION_END.strftime('%H:%M')} МСК, сегодня {now.strftime('%a')})", "INFO")
                time.sleep(interval_sec)
                continue

            result = check_trade()
            if result is None:
                log("Сделка не найдена или закрыта — завершение монитора", "INFO")
                break

            action = result.get("action", "hold")

            # ── Терминальные события: завершение работы ──
            if action == "sl_to_breakeven":
                verify_api_state()
                log("🏁 TP1 достигнут. SL переведён в безубыток. Монитор завершает работу.", "ALERT")
                log("   Оставшиеся 6 lots защищены SL_be=1,937 ₽. TP2=2,100 автоисполнится.", "INFO")
                break

            if action == "tp2_reached":
                verify_api_state()
                log("🏁 TP2 достигнут! Монитор завершает работу.", "ALERT")
                break

            if action == "sl_hit":
                log("🛑 SL СРАБОТАЛ! Позиция закрыта. Монитор завершает работу.", "ALERT")
                break

            # Проверка: сработал ли SL (цена ≤ SL)
            current_sl = load_trade()
            if current_sl:
                sl_price = current_sl.get("sl_price", 0)
                if result["price"] <= sl_price:
                    log(f"🛑 Цена {result['price']:.1f} ≤ SL {sl_price:.1f} — стоп-лосс сработал!", "ALERT")
                    # Обновим статус сделки
                    current_sl["status"] = "CLOSED"
                    current_sl["exit_price"] = result["price"]
                    current_sl["exit_reason"] = "SL"
                    current_sl["exit_time"] = datetime.now().isoformat()
                    save_trade(current_sl)
                    break

            time.sleep(interval_sec)

        except KeyboardInterrupt:
            log("⏹ Монитор остановлен пользователем")
            break
        except Exception as e:
            log(f"Ошибка в цикле: {e}", "ERROR")
            time.sleep(60)


def main():
    parser = argparse.ArgumentParser(
        description="Монитор сделки PLZL на БОЕВОМ СЧЁТЕ — TP1/TP2 + безубыточный SL"
    )
    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()

    if args.verify:
        verify_api_state()
        return

    if args.daemon:
        run_daemon(interval_sec=args.interval)
    else:
        log("🔍 Однократная проверка сделки PLZL (БОЕВОЙ СЧЁТ)...")
        verify_api_state()
        result = check_trade()
        if result is None:
            log("❌ Сделка не найдена или закрыта")
            sys.exit(1)

        action = result.get("action", "hold")
        if action == "sl_to_breakeven":
            log(f"✅ SL переведён в безубыток ({result.get('new_sl', '?')} ₽)")
        elif action == "tp2_reached":
            log(f"🏁 TP2 достигнут! Цена: {result.get('price', '?')} ₽")
        elif action == "sl_warning":
            log(f"⚠️  Цена близка к SL! До SL: {result['distance_to_sl']:.1f} ₽")
        else:
            log(
                f"Статус: {result['price']:.1f} ₽ | PnL {result['pnl_pct']:+.2f}% | "
                f"до TP1: {result['distance_to_tp1']:.1f} | до SL: {result['distance_to_sl']:.1f}"
            )


if __name__ == "__main__":
    main()
