import os, json, time, uuid, logging, requests, threading, csv
from datetime import datetime, timedelta, timezone, time as dtime
from flask import Flask, render_template_string
from dotenv import load_dotenv

BASE_DIR   = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(BASE_DIR, "tok.env"))
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.FileHandler(os.path.join(BASE_DIR, "bot.log")), logging.StreamHandler()]
)
os.environ['SSL_CERT_FILE'] = '/etc/ssl/certs/ca-certificates.crt'
os.environ['GRPC_DEFAULT_SSL_ROOTS_FILE_PATH'] = '/etc/ssl/certs/ca-certificates.crt'

from t_tech.invest import Client, OrderDirection, OrderType, CandleInterval, InstrumentIdType

MSK_TZ, STATE_FILE, CONFIG_FILE, TRADE_LOG, TELEMETRY_LOG = timezone(timedelta(hours=3)), os.path.join(BASE_DIR, "state_prod.json"), os.path.join(BASE_DIR, "config.json"), os.path.join(BASE_DIR, "trades.csv"), os.path.join(BASE_DIR, "market_telemetry.csv")
state_lock, TG_PROXY = threading.Lock(), "https://muddy-firefly-3862.y-afanasiev.workers.dev"

def q_to_float(q): return q.units + q.nano / 1e9 if q else 0.0

def load_config():
    try:
        with open(CONFIG_FILE, 'r') as f: return json.load(f)
    # FIX: Добавлен ACCOUNT_ID в fallback для защиты от KeyError
    except: return {"TICKER": "UNKNOWN", "WEB_PORT": 5000, "ACCOUNT_ID": ""}

def save_state_atomic(state):
    tmp = STATE_FILE + ".tmp"
    with state_lock:
        try:
            with open(tmp, 'w') as f: json.dump(state, f, indent=2)
            os.replace(tmp, STATE_FILE)
        except Exception as e: logging.error(f"Save Error: {e}")

def send_tg(msg, level="INFO"):
    token, chat_id = os.getenv("TG_TOKEN"), os.getenv("TG_CHAT_ID")
    if not token or not chat_id: return
    icons = {"TRADE":"🛒","PROFIT":"💰","LOSS":"🩸","CRIT":"🚨","INFO":"🛰️","AI":"🧠"}
    try: requests.post(f"{TG_PROXY}/bot{token}/sendMessage", json={"chat_id": chat_id, "text": f"{icons.get(level,'🤖')} *{level}*\n{msg}", "parse_mode": "Markdown"}, timeout=15)
    except: pass

def send_tg_report(file_path, caption=""):
    token, chat_id = os.getenv("TG_TOKEN"), os.getenv("TG_CHAT_ID")
    if not token or not chat_id or not os.path.exists(file_path): return
    try:
        with open(file_path, 'rb') as f:
            requests.post(f"{TG_PROXY}/bot{token}/sendDocument", data={"chat_id": chat_id, "caption": caption}, files={"document": f}, timeout=30)
    except: pass

def calc_rsi(closes, period=14):
    if len(closes) < period + 1: return 50.0
    gains, losses = [max(closes[i] - closes[i-1], 0) for i in range(1, len(closes))], [max(closes[i-1] - closes[i], 0) for i in range(1, len(closes))]
    ag, al = sum(gains[-period:]) / period, sum(losses[-period:]) / period
    return round(100 - 100 / (1 + ag / al), 1) if al > 0 else 100.0

def log_trade(action, price, lots, shares, reason="", pnl=0):
    exists = os.path.isfile(TRADE_LOG)
    with open(TRADE_LOG, 'a', newline='') as f:
        w = csv.writer(f)
        if not exists: w.writerow(["Date","Action","Price","Lots","Shares","Reason","PnL"])
        w.writerow([datetime.now(MSK_TZ).strftime("%Y-%m-%d %H:%M:%S"), action, price, lots, shares, reason, pnl])

def log_telemetry(ticker, price, rsi, trend, spread, vola):
    exists = os.path.isfile(TELEMETRY_LOG)
    with open(TELEMETRY_LOG, 'a', newline='') as f:
        w = csv.writer(f)
        if not exists: w.writerow(["Time","Ticker","Price","RSI","Trend","Spread","Vola%"])
        w.writerow([datetime.now(MSK_TZ).strftime("%H:%M:%S"), ticker, price, rsi, trend, spread, vola])

def generate_ai_prompt(conf):
    prompt_file = os.path.join(BASE_DIR, "AI_PROMPT_WEEKLY.txt")
    try:
        with open(prompt_file, 'w') as f:
            f.write("SYSTEM ROLE: Квантовый финансовый аналитик.\n")
            f.write(f"TASK: Проанализировать работу торгового бота RAPTOR для тикера {conf.get('TICKER', 'UNKNOWN')}.\n\n")
            f.write("--- CURRENT CONFIG ---\n")
            # FIX: Вырезаем приватные данные перед отправкой ИИ
            safe_conf = {k: v for k, v in conf.items() if k not in ('ACCOUNT_ID', 'TINKOFF_TOKEN')}
            f.write(json.dumps(safe_conf, indent=2) + "\n\n")
            f.write("--- LAST TRADES ---\n")
            if os.path.exists(TRADE_LOG):
                with open(TRADE_LOG, 'r') as tl: f.writelines(tl.readlines()[-10:])
            f.write("\n--- MARKET CONTEXT (Last Telemetry) ---\n")
            if os.path.exists(TELEMETRY_LOG):
                with open(TELEMETRY_LOG, 'r') as ml: f.writelines(ml.readlines()[-10:])
            f.write("\nQUESTION: На основе данных выше, предложи оптимизацию ATR_COEFF или STEP_DISTANCE_ATR.\n")
        return prompt_file
    except: return None

app = Flask(__name__)
@app.route('/')
def index():
    now_tz = datetime.now(MSK_TZ)
    # FIX: Оставили работу в выходные (без now_tz.weekday() > 4)
    is_open = any([dtime(7, 0) <= now_tz.time() <= dtime(18, 45), dtime(19, 0) <= now_tz.time() <= dtime(23, 50)])
    time_str = now_tz.strftime("%H:%M:%S")

    def get_st(p):
        # FIX: Добавлена last_vola в дефолтный словарь
        d = {"phase":"OFFLINE","live_price":0,"total_shares":0,"current_step":0,"avg_price":0,"target_act":0,"profit_goal":0,"pnl":0,"daily_pnl":0,"deals_today":0,"trend":"N/A","last_rsi":50,"filter_status":"N/A","stop_loss":0,"last_atr":0,"last_vola":0.0}
        try:
            if os.path.exists(p):
                with open(p, 'r') as f: data = json.load(f); d.update(data)
                sh, lp, ap = d.get('total_shares', 0), d.get('live_price', 0), d.get('avg_price', 0)
                d['pnl'] = round((lp - ap) * sh, 2) if sh > 0 else 0
                if d['phase'] == "SELL" and d.get('profit_goal', 0) > 0:
                    d['progress'] = round(min(100, max(0, (lp - ap) / (d['profit_goal'] - ap) * 100)), 1)
                elif d['phase'] == "BUY" and d.get('target_act', 0) > 0:
                    base = d.get('base_price', lp)
                    if base - d['target_act'] != 0: d['progress'] = round(min(100, max(0, (base - lp) / (base - d['target_act']) * 100)), 1)
                    else: d['progress'] = 0
                else: d['progress'] = 0
        except: pass
        return d
    
    sber = get_st("/root/sber_bot/state_prod.json")
    gmkn = get_st("/root/gmkn_bot/state_prod.json")
    
    return render_template_string("""<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"><title>RAPTOR v18.4 UI</title><meta http-equiv="refresh" content="5">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚀</text></svg>">
<style>
body{background:#0a0a0b;color:#eee;font-family:sans-serif;padding:12px;display:flex;justify-content:center}
.card{background:#161618;padding:20px;border-radius:18px;border:1px solid #2a2a2a;width:100%;max-width:360px;margin:8px}
.price{text-align:center;font-size:3rem;font-weight:bold;margin:8px 0}
.pnl{text-align:center;font-weight:bold;font-size:1.2rem;margin-bottom:10px}
.row{display:flex;justify-content:space-between;font-size:.82rem;color:#666;padding:3px 0;border-bottom:1px solid #1c1c1c}.row b{color:#ccc}
.progress-bg{background:#222;height:6px;border-radius:3px;margin:10px 0;overflow:hidden}
.progress-bar{background:cyan;height:100%;transition:width 0.5s}
@keyframes blink { 50% { opacity: 0.3; } }
.live-dot { animation: blink 1.5s linear infinite; font-size: 1.2rem; vertical-align: middle; }
</style></head><body><div style="max-width:820px;width:100%">
<h2 style="text-align:center;color:#444;letter-spacing:3px;margin-bottom:5px;">RAPTOR v18.4</h2>
<div style="text-align:center; margin-bottom:20px; font-size:0.9rem; background:#111; padding:8px; border-radius:10px; border:1px solid #222;">
    {% if is_open %}
        <span class="live-dot" style="color:#00e676;">🟢</span> <b style="color:#00e676; margin-right:15px;">РЫНОК ОТКРЫТ (Торги выходного дня)</b>
    {% else %}
        <span style="color:#ff5252;">🔴</span> <b style="color:#ff5252; margin-right:15px;">РЫНОК ЗАКРЫТ</b>
    {% endif %}
    <span style="color:#888;">Сервер (МСК): <b style="color:#fff;">{{ time_str }}</b></span>
</div>
<div style="display:flex;flex-wrap:wrap;justify-content:center">
{% for n, d in [('SBER', sber), ('GMKN', gmkn)] %}
<div class="card"><div style="display:flex;justify-content:space-between"><b style="color:cyan">{{ n }}</b><span style="color:{{ 'cyan' if d.phase=='BUY' else 'orange' }}">{{ d.phase }} [ст.{{ d.current_step }}]</span></div>
<div class="price">{{ d.live_price }}</div>
<div class="pnl" style="color:{{ '#00e676' if d.pnl >= 0 else '#ff5252' }}">{{ d.pnl }} ₽</div>
<div class="progress-bg"><div class="progress-bar" style="width:{{ d.progress }}%; background:{{ 'cyan' if d.phase=='BUY' else '#4fc3f7' }}"></div></div>
<div style="border-top:1px solid #222;padding-top:10px">
<div class="row"><span>Avg / Stop</span><b>{{ d.avg_price }} / <span style="color:#ff5252">{{ d.stop_loss }}</span></b></div>
<div class="row"><span>Target (TP)</span><b style="color:#4fc3f7">{{ d.profit_goal }}</b></div>
<div class="row"><span>Следующий вход</span><b>{{ d.target_act }}</b></div>
<div class="row"><span>ATR / RSI / Vola</span><b>{{ d.last_atr }} / {{ d.last_rsi }} / {{ d.last_vola }}%</b></div>
<div class="row"><span>Тренд / Фильтр</span><b style="color:{{ '#00e676' if d.trend=='UP' else '#ff9800' }}">{{ d.trend }}</b> / <b style="color:{{ '#00e676' if d.filter_status=='OK' else '#ff9800' }}">{{ d.filter_status }}</b></div>
<div class="row"><span>Сделок / P&L дня</span><b>{{ d.deals_today }} / <span style="color:{{ '#00e676' if d.daily_pnl >= 0 else '#ff5252' }}">{{ d.daily_pnl }} ₽</span></b></div>
</div></div>{% endfor %}</div></div></body></html>""", sber=sber, gmkn=gmkn, is_open=is_open, time_str=time_str)

def is_market_open():
    now = datetime.now(MSK_TZ)
    t = now.time()
    # FIX: Торги выходного дня (работает 7 дней в неделю)
    return any([dtime(7, 0) <= t <= dtime(18, 45), dtime(19, 0) <= t <= dtime(23, 50)])

def run_bot():
    conf = load_config()
    acc_id = conf.get("ACCOUNT_ID", "")
    if not acc_id: logging.warning("ACCOUNT_ID не задан в config.json!")
    
    state = {"phase": "BUY", "current_step": 0, "base_price": None, "live_price": 0, "total_shares": 0, "total_lots": 0, "avg_price": 0, "tp_activated": False, "target_act": 0, "profit_goal": 0, "stop_loss": 0, "max_p": 0, "buy_time_ts": 0, "last_sell_ts": 0, "deals_today": 0, "daily_pnl": 0.0, "last_date": str(datetime.now(MSK_TZ).date()), "trend": "WAIT", "last_rsi": 50, "filter_status": "INIT", "lot_size": 1, "last_atr": 0.0, "last_vola": 0.0, "reported": -1, "partial_sold": False, "ai_reported": -1}
    if os.path.exists(STATE_FILE):
        try:
            with open(STATE_FILE, 'r') as f: state.update(json.load(f))
        except: pass

    with Client(os.getenv("TINKOFF_TOKEN"), target="invest-public-api.tbank.ru:443") as client:
        try:
            instr = client.instruments.share_by(id_type=InstrumentIdType.INSTRUMENT_ID_TYPE_TICKER, class_code=conf.get("CLASS_CODE", "TQBR"), id=conf.get("TICKER", "UNKNOWN")).instrument
            target_uid, lot_size, min_inc = instr.uid, instr.lot, q_to_float(instr.min_price_increment)
            state['lot_size'] = lot_size
        except Exception as e:
            logging.error(f"Init Error: {e}"); return

        last_indicators_ts = last_telemetry_ts = 0; atr = 2.0
        
        while True:
            try:
                now = datetime.now(MSK_TZ)
                if str(now.date()) != state.get('last_date'):
                    state.update({"deals_today": 0, "daily_pnl": 0.0, "last_date": str(now.date()), "reported": -1})
                
                if now.hour == 23 and now.minute == 55 and state.get('reported') != now.day:
                    send_tg(f"📊 {conf['TICKER']} итог {now.strftime('%d.%m')}:\nСделок: {state['deals_today']}\nP&L дня: {state['daily_pnl']:+.0f} ₽", "INFO")
                    send_tg_report(TRADE_LOG, f"{conf['TICKER']} Trades")
                    state['reported'] = now.day; save_state_atomic(state)

                if now.weekday() == 6 and now.hour == 21 and state.get('ai_reported') != now.isocalendar()[1]:
                    prompt_path = generate_ai_prompt(conf)
                    if prompt_path:
                        send_tg(f"🧠 Сформирован бриф ИИ по {conf['TICKER']}.", "AI")
                        send_tg_report(prompt_path, f"AI Prompt {conf['TICKER']}")
                        state['ai_reported'] = now.isocalendar()[1]; save_state_atomic(state)

                if acc_id:
                    portfolio = client.operations.get_portfolio(account_id=acc_id)
                    pos = next((p for p in portfolio.positions if p.instrument_uid == target_uid), None)
                    state['total_shares'], state['total_lots'] = (int(q_to_float(pos.quantity)), int(q_to_float(pos.quantity)) // lot_size) if pos else (0, 0)
                    if pos and state['avg_price'] == 0: state['avg_price'] = q_to_float(pos.average_position_price)
                
                if state['total_shares'] == 0 and state['phase'] == "SELL":
                    state.update({"phase": "BUY", "current_step": 0, "avg_price": 0, "tp_activated": False, "max_p": 0, "buy_time_ts": 0, "profit_goal": 0, "stop_loss": 0, "partial_sold": False})

                cur_p = q_to_float(client.market_data.get_last_prices(instrument_id=[target_uid]).last_prices[0].price)
                
                # FIX: Безопасная инициализация base_price ТОЛЬКО если цена > 0.1
                if cur_p > 0.1: 
                    state['live_price'] = cur_p
                    if state.get('base_price') is None: 
                        state['base_price'] = cur_p

                if not is_market_open(): save_state_atomic(state); time.sleep(60); continue

                if time.time() - last_indicators_ts > 900:
                    c = client.market_data.get_candles(instrument_id=target_uid, from_=datetime.now(timezone.utc) - timedelta(days=5), to=datetime.now(timezone.utc), interval=CandleInterval.CANDLE_INTERVAL_HOUR).candles
                    if len(c) >= 20:
                        closes = [q_to_float(x.close) for x in c]
                        state['trend'] = "UP" if cur_p > (sum(closes[-conf.get("TREND_CANDLES", 20):]) / conf.get("TREND_CANDLES", 20)) else "DOWN"
                        state['last_rsi'] = calc_rsi(closes)
                        trs = [max(q_to_float(c[i].high) - q_to_float(c[i].low), abs(q_to_float(c[i].high) - q_to_float(c[i-1].close))) for i in range(1, len(c))]
                        atr = sum(trs[-14:]) / 14; state['last_atr'] = round(atr, 2)
                        state['last_vola'] = round((atr / cur_p) * 100, 2)
                    last_indicators_ts = time.time()
                
                if time.time() - last_telemetry_ts > 900:
                    try:
                        ob = client.market_data.get_order_book(instrument_id=target_uid, depth=1)
                        spread = round((q_to_float(ob.asks[0].price) - q_to_float(ob.bids[0].price)) / q_to_float(ob.bids[0].price) * 100, 4) if ob.asks and ob.bids else 0.0
                        log_telemetry(conf["TICKER"], cur_p, state.get('last_rsi', 50), state.get('trend', 'WAIT'), spread, state.get('last_vola', 0))
                    except: pass
                    last_telemetry_ts = time.time()
                
                buy_step = max(round(atr * conf.get("ATR_COEFF", 0.5), 2), conf.get("MIN_BUY_STEP", 1.0))
                
                if state['total_lots'] > 0:
                    state['phase'] = "SELL"
                    if state.get('max_p', 0) == 0: state['max_p'] = state['avg_price']
                    if cur_p > state.get('max_p', 0): state['max_p'] = cur_p
                    sl_price = round(state['avg_price'] - buy_step * conf["SL_COEFF"], 2)
                    if state.get('tp_activated') or state.get('partial_sold'): 
                        sl_price = max(sl_price, round(state['avg_price'] * 1.0008, 2))
                    state['stop_loss'] = sl_price
                    if not state.get('profit_goal'):
                         tp_mult = conf["PROFIT_COEFF"] if state['trend'] == "UP" else conf["PROFIT_COEFF"] * 0.7
                         state['profit_goal'] = round(state['avg_price'] + buy_step * tp_mult, 2)
                    if cur_p >= state['profit_goal']: state['tp_activated'] = True
                    rebound = max(buy_step * conf["TRAILING_REBOUND"], min_inc * 3)
                    is_tp, is_sl = state.get('tp_activated') and cur_p <= state['max_p'] - rebound, cur_p <= sl_price
                    is_time = state.get('buy_time_ts', 0) > 0 and now.timestamp() > state['buy_time_ts'] + conf["MAX_HOLD_HOURS"] * 3600
                    
                    if not state.get('partial_sold') and state['total_lots'] >= 2 and cur_p >= state['avg_price'] * (1 + conf.get("EARLY_EXIT_PCT", 0.01)):
                        qty_to_sell = state['total_lots'] // 2
                        res = client.orders.post_order(instrument_id=target_uid, quantity=qty_to_sell, direction=OrderDirection.ORDER_DIRECTION_SELL, account_id=acc_id, order_type=OrderType.ORDER_TYPE_MARKET, order_id=str(uuid.uuid4()))
                        p_sold = q_to_float(res.executed_order_price) or cur_p
                        pnl = round((p_sold - state['avg_price']) * (qty_to_sell * lot_size), 2)
                        state['daily_pnl'] = round(state.get('daily_pnl', 0) + pnl, 2)
                        # FIX: Убран deals_today += 1 при частичной продаже (по замечанию аудитора)
                        state['partial_sold'] = True
                        log_trade("PARTIAL_SELL", p_sold, qty_to_sell, qty_to_sell * lot_size, "EarlyExit", pnl)
                        send_tg(f"💰 {conf['TICKER']} Частичная фиксация 50%\nP&L: {pnl:+.2f} ₽", "PROFIT")

                    if is_tp or is_sl or is_time:
                        reason = ("TP" if is_tp else ("SL" if is_sl else "TIME"))
                        res = client.orders.post_order(instrument_id=target_uid, quantity=state['total_lots'], direction=OrderDirection.ORDER_DIRECTION_SELL, account_id=acc_id, order_type=OrderType.ORDER_TYPE_MARKET, order_id=str(uuid.uuid4()))
                        p_sold = q_to_float(res.executed_order_price) or cur_p
                        pnl = round((p_sold - state['avg_price']) * state['total_shares'], 2)
                        state['daily_pnl'] = round(state.get('daily_pnl', 0) + pnl, 2); state['deals_today'] += 1
                        log_trade("SELL", p_sold, state['total_lots'], state['total_shares'], reason, pnl)
                        send_tg(f"{conf['TICKER']} [{reason}]\nP&L: {pnl:+.2f} ₽", "PROFIT" if pnl > 0 else "LOSS")
                        state.update({"phase": "BUY", "current_step": 0, "total_shares": 0, "total_lots": 0, "avg_price": 0, "tp_activated": False, "max_p": 0, "stop_loss": 0, "profit_goal": 0, "buy_time_ts": 0, "base_price": cur_p, "last_sell_ts": now.timestamp(), "partial_sold": False})

                if state['phase'] == "BUY":
                    if state.get('daily_pnl', 0) <= conf["DAILY_LOSS_LIMIT"]:
                        state['filter_status'] = f"LIMIT {state['daily_pnl']:.0f}₽"; save_state_atomic(state); time.sleep(300); continue
                    step = state['current_step']
                    cooldown_ok, trend_ok, rsi_ok = now.timestamp() > state.get('last_sell_ts', 0) + conf["COOLDOWN_MINUTES"] * 60, state.get('trend') == "UP", state.get('last_rsi', 50) < conf["RSI_BUY_THRESHOLD"]
                    filters_ok = (cooldown_ok and trend_ok and rsi_ok) if step == 0 else True
                    
                    # FIX: Корректный статус фильтра для дашборда на шагах DCA > 0
                    if step > 0:
                        state['filter_status'] = f"Ожидание DCA ст.{step+1}"
                    else:
                        if not cooldown_ok: state['filter_status'] = "COOLDOWN"
                        elif not trend_ok: state['filter_status'] = "TREND DOWN"
                        elif not rsi_ok: state['filter_status'] = f"RSI {state.get('last_rsi', 50):.0f}"
                        else: state['filter_status'] = "OK"
                        
                    if filters_ok and step < len(conf.get("DCA_STEPS", [])):
                        if step == 0 and cur_p > state.get('base_price', cur_p): state['base_price'] = cur_p
                        state['target_act'] = round(state['base_price'] - (buy_step * conf.get("STEP_DISTANCE_ATR", 1.0)) * (step + 1), 2)
                        if cur_p <= state['target_act'] and acc_id:
                            step_budget = conf["MAX_TICKER_BUDGET"] * conf["DCA_STEPS"][step] * conf["RISK_FRACTION"]
                            qty = int(step_budget // (cur_p * lot_size))
                            if qty >= 1:
                                res = client.orders.post_order(instrument_id=target_uid, quantity=qty, direction=OrderDirection.ORDER_DIRECTION_BUY, account_id=acc_id, order_type=OrderType.ORDER_TYPE_MARKET, order_id=str(uuid.uuid4()))
                                p_bought = q_to_float(res.executed_order_price) or cur_p
                                if state['buy_time_ts'] == 0: state['buy_time_ts'] = now.timestamp()
                                state['current_step'] += 1
                                log_trade("BUY", p_bought, qty, qty * lot_size, f"Step{state['current_step']}")
                                send_tg(f"🛒 {conf['TICKER']} шаг {state['current_step']} @ {p_bought:.2f}", "TRADE")
                    elif step == 0: state['target_act'] = round(state['base_price'] - buy_step, 2)
                save_state_atomic(state); time.sleep(2)
            except Exception as e: logging.error(f"Loop Error: {e}"); time.sleep(10)

if __name__ == "__main__":
    conf_main = load_config()
    threading.Thread(target=lambda: app.run(host='0.0.0.0', port=conf_main.get("WEB_PORT", 5000), use_reloader=False), daemon=True).start()
    while True:
        try: run_bot()
        except KeyboardInterrupt: break
        except Exception as e: logging.error(f"Fatal: {e}"); time.sleep(10)
