"""THOR ALPHA GOLD v1.2.3 - Mirror Trading Bot with Web Dashboard"""
import json, time, logging, sys, os, threading, webbrowser, socket
from http.server import HTTPServer, BaseHTTPRequestHandler
try:
    from http.server import ThreadingHTTPServer
except ImportError:
    import socketserver
    class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
        daemon_threads = True
from datetime import datetime, timedelta
from typing import Optional, Dict, List
from dataclasses import dataclass, field
import requests, zlib, base64

try:
    import MetaTrader5 as mt5
    MT5_AVAILABLE = True
except ImportError:
    MT5_AVAILABLE = False

# Logging
LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
os.makedirs(LOG_DIR, exist_ok=True)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(os.path.join(LOG_DIR, "bot.log"), encoding="utf-8"),
        logging.StreamHandler(),
    ],
)
logger = logging.getLogger("TFT-Bot")

# Dashboard HTML (compressed)
_DASHBOARD_HTML_GZ = " "

def _get_html():
    return zlib.decompress(base64.b64decode(_DASHBOARD_HTML_GZ)).decode("utf-8")

# Config
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")

DEFAULT_CONFIG = {
    "server_url": "https://api.kai-alpha.com",
    "license_key": "YOUR-LICENSE-KEY-HERE",
    "mt5_account": "", "mt5_password": "", "mt5_server": "",
    "symbol": "XAUUSD", "mt5_symbol": "",
    "timeframe": "H1", "risk_percent": 0.02, "max_positions": 2,
    "update_interval_seconds": 60, "bars_to_fetch": 250,
    "client_version": "1.2.3",
    "telegram_bot_token": "", "telegram_chat_id": "",
    "trailing_activation_atr": 1.5, "trailing_distance_atr": 1.0,
    "trailing_step_atr": 0.3, "breakeven_activation_atr": 0.7,
    "breakeven_offset_atr": 0.1,
    "partial_tp_levels": [
        {"pct": 0.50, "distance_pct": 0.50},
        {"pct": 0.30, "distance_pct": 0.75}
    ]
}

def load_config():
    if not os.path.exists(CONFIG_PATH):
        with open(CONFIG_PATH, "w", encoding="utf-8") as f:
            json.dump(DEFAULT_CONFIG, f, indent=2, ensure_ascii=False)
        return DEFAULT_CONFIG.copy()
    with open(CONFIG_PATH, "r", encoding="utf-8") as f:
        cfg = json.load(f)
    for k, v in DEFAULT_CONFIG.items():
        if k not in cfg:
            cfg[k] = v
    return cfg

def save_config(data: dict):
    cfg = load_config()
    # Ensure correct types for numeric fields
    type_map = {"risk_percent": float, "max_positions": int, "update_interval_seconds": int, "bars_to_fetch": int}
    for k, v in data.items():
        if v == "":
            continue
        if k in type_map:
            try:
                cfg[k] = type_map[k](v)
            except (ValueError, TypeError):
                pass
        else:
            cfg[k] = v
    with open(CONFIG_PATH, "w", encoding="utf-8") as f:
        json.dump(cfg, f, indent=2, ensure_ascii=False)
    return cfg

# Global bot state
_bot_state = {
    "signal": {"action": "NONE", "confidence": 0.0, "atr": 0.0, "sl_price": 0.0, "tp_price": 0.0, "reasoning": ""},
    "account": {"balance": 0.0, "equity": 0.0, "pnl": 0.0, "win_rate": 0.0, "total_trades": 0},
    "price": {"bid": 0.0, "ask": 0.0, "spread": 0.0},
    "config": {},
    "status": "STANDBY",
    "status_msg": "",
    "running": False,
    "logs": [],
    "positions": [],
    "equity_history": [],
}
_bot_lock = threading.Lock()
_bot_instance = None

def _log(msg, level="INFO"):
    ts = datetime.now().strftime("%H:%M:%S")
    entry = {"time": ts, "level": level, "msg": msg}
    logger.info(msg) if level == "INFO" else logger.error(msg)
    with _bot_lock:
        _bot_state["logs"].append(entry)
        if len(_bot_state["logs"]) > 200:
            _bot_state["logs"] = _bot_state["logs"][-200:]

# MT5 Client
GOLD_SYMBOLS = ["XAUUSD", "XAUUSDm", "XAUUSDc", "XAUUSDn", "XAUUSD.", "GOLD", "XAUUSDa", "GOLDm", "XAU/USD"]

class MT5Client:
    def __init__(self, config):
        self.config = config
        self.symbol = config.get("mt5_symbol") or config.get("symbol", "XAUUSD")
        self.connected = False

    def _get_filling_mode(self):
        try:
            info = mt5.symbol_info(self.symbol)
            if info is None:
                return mt5.ORDER_FILLING_RETURN
            filling = info.filling_mode
            if filling & 4:
                return mt5.ORDER_FILLING_RETURN
            elif filling & 1:
                return mt5.ORDER_FILLING_FOK
            elif filling & 2:
                return mt5.ORDER_FILLING_IOC
        except Exception:
            pass
        return mt5.ORDER_FILLING_RETURN

    def _auto_detect_symbol(self):
        cfg_sym = self.config.get("mt5_symbol", "")
        if cfg_sym:
            if mt5.symbol_info(cfg_sym):
                mt5.symbol_select(cfg_sym, True)
                return cfg_sym
        for sym in GOLD_SYMBOLS:
            if mt5.symbol_info(sym):
                mt5.symbol_select(sym, True)
                _log(f"Tu dong phat hien symbol: {sym}")
                return sym
        all_syms = mt5.symbols_get()
        if all_syms:
            for s in all_syms:
                n = s.name.upper()
                if ("XAU" in n and "USD" in n) or n == "GOLD":
                    mt5.symbol_select(s.name, True)
                    return s.name
        return "XAUUSD"

    def connect(self):
        if not MT5_AVAILABLE:
            _log("MetaTrader5 chua cai dat", "ERROR")
            return False
        if not mt5.initialize():
            _log(f"MT5 init that bai: {mt5.last_error()}", "ERROR")
            return False
        acc = self.config.get("mt5_account")
        pwd = self.config.get("mt5_password")
        srv = self.config.get("mt5_server")
        if acc and pwd and srv:
            if not mt5.login(int(acc), password=pwd, server=srv):
                _log(f"MT5 login that bai: {mt5.last_error()}", "ERROR")
                mt5.shutdown()
                return False
        self.connected = True
        info = mt5.account_info()
        self.symbol = self._auto_detect_symbol()
        _log(f"MT5 OK: #{info.login}, ${info.balance:.2f}, Symbol: {self.symbol}")
        return True

    def disconnect(self):
        if MT5_AVAILABLE and self.connected:
            mt5.shutdown()
        self.connected = False

    def get_balance(self):
        info = mt5.account_info()
        return float(info.balance) if info else 0.0

    def get_equity(self):
        info = mt5.account_info()
        return float(info.equity) if info else 0.0

    def get_ohlcv(self, bars=250):
        tf_map = {
            "M1": mt5.TIMEFRAME_M1, "M5": mt5.TIMEFRAME_M5,
            "M15": mt5.TIMEFRAME_M15, "H1": mt5.TIMEFRAME_H1,
            "H4": mt5.TIMEFRAME_H4, "D1": mt5.TIMEFRAME_D1,
        }
        tf = tf_map.get(self.config.get("timeframe", "H1"), mt5.TIMEFRAME_H1)
        rates = mt5.copy_rates_from_pos(self.symbol, tf, 0, bars)
        if rates is None or len(rates) == 0:
            return []
        return [
            {
                "timestamp": datetime.utcfromtimestamp(r["time"]).isoformat(),
                "open": float(r["open"]), "high": float(r["high"]),
                "low": float(r["low"]), "close": float(r["close"]),
                "volume": float(r["tick_volume"]),
            }
            for r in rates
        ]

    def get_tick(self):
        return mt5.symbol_info_tick(self.symbol)

    def open_position(self, action, lots, sl, tp, comment="TFT AI v1.2"):
        order_type = mt5.ORDER_TYPE_BUY if action == "BUY" else mt5.ORDER_TYPE_SELL
        tick = mt5.symbol_info_tick(self.symbol)
        price = tick.ask if action == "BUY" else tick.bid
        # MT5 comment field max ~31 chars
        safe_comment = (comment or "TFT AI v1.2")[:31]
        req = {
            "action": mt5.TRADE_ACTION_DEAL,
            "symbol": self.symbol,
            "volume": lots,
            "type": order_type,
            "price": price,
            "sl": sl,
            "tp": tp,
            "deviation": 20,
            "magic": 234000,
            "comment": safe_comment,
            "type_time": mt5.ORDER_TIME_GTC,
            "type_filling": self._get_filling_mode(),
        }
        result = mt5.order_send(req)
        if result and result.retcode == mt5.TRADE_RETCODE_DONE:
            _log(f"LENH OK: {action} {lots} lots @ {result.price} #{result.order} [{safe_comment}]")
            return result
        error = result.comment if result else str(mt5.last_error())
        _log(f"LENH THAT BAI: {error}", "ERROR")
        return None

    def modify_position(self, ticket, sl, tp):
        req = {"action": mt5.TRADE_ACTION_SLTP, "position": ticket,
               "symbol": self.symbol, "sl": sl, "tp": tp}
        result = mt5.order_send(req)
        return result and result.retcode == mt5.TRADE_RETCODE_DONE

    def close_position(self, ticket, lots=None):
        position = mt5.positions_get(ticket=ticket)
        if not position:
            return False
        pos = position[0]
        close_type = mt5.ORDER_TYPE_SELL if pos.type == 0 else mt5.ORDER_TYPE_BUY
        tick = mt5.symbol_info_tick(self.symbol)
        price = tick.bid if pos.type == 0 else tick.ask
        volume = lots if lots else pos.volume
        req = {
            "action": mt5.TRADE_ACTION_DEAL, "symbol": self.symbol,
            "volume": volume, "type": close_type, "position": ticket,
            "price": price, "deviation": 20, "magic": 234000,
            "comment": "TFT AI Close",
            "type_filling": self._get_filling_mode(),
        }
        result = mt5.order_send(req)
        if result and result.retcode == mt5.TRADE_RETCODE_DONE:
            _log(f"DONG LENH #{ticket} ({volume} lots)")
            return True
        return False

    def get_positions(self):
        return list(mt5.positions_get(symbol=self.symbol) or [])


# Server API
class ServerAPI:
    def __init__(self, config):
        self.base = config["server_url"].rstrip("/")
        self.license = config.get("license_key", "")
        self.account = config.get("mt5_account", "")
        self.version = config.get("client_version", "1.2.1")

    def health_check(self):
        try:
            return requests.get(f"{self.base}/health", timeout=5).status_code == 200
        except Exception:
            return False

    def check_version(self):
        try:
            return requests.get(f"{self.base}/version",
                                params={"client_version": self.version}, timeout=10).json()
        except Exception:
            return {"update_required": False}

    def verify_license(self):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            ip = s.getsockname()[0]
            s.close()
        except Exception:
            ip = "unknown"
        try:
            return requests.post(
                f"{self.base}/verify",
                json={"license": self.license, "account": self.account, "ip": ip},
                timeout=10,
            ).json()
        except Exception as e:
            return {"valid": False, "message": str(e)}

    def execute(self, balance, current_price, spread_pips, positions, ohlcv,
                risk_percent=0.02, hwid=""):
        """Call /execute on server. Returns dict with `commands` list (mirror trading)."""
        try:
            payload = {
                "license": self.license,
                "account": self.account,
                "hwid": hwid,
                "balance": float(balance),
                "risk_percent": float(risk_percent),
                "current_price": float(current_price),
                "spread_pips": float(spread_pips),
                "positions": positions,
                "market_data": ohlcv,
            }
            r = requests.post(f"{self.base}/execute", json=payload, timeout=30)
            r.raise_for_status()
            return r.json()
        except Exception as e:
            return {"commands": [], "reasoning": f"execute error: {e}", "atr": 0.0, "confidence": 0.0}

    def auto_update(self, expected_sha256=None):
        """Download new bot_pro.py, verify, replace, return True on success."""
        import hashlib
        try:
            script_path = os.path.abspath(__file__)
            new_path = script_path + ".new"
            bak_path = script_path + ".bak"

            _log("Dang tai ban cap nhat...")
            r = requests.get(f"{self.base}/api/update/bot_pro", timeout=30)
            if r.status_code != 200:
                _log(f"Tai update that bai: HTTP {r.status_code}", "ERROR")
                return False

            data = r.content
            # Verify size
            if len(data) < 1000:
                _log("File update qua nho, bo qua", "ERROR")
                return False

            # Verify content is valid Python bot
            text = data.decode("utf-8", errors="ignore")
            if "def main():" not in text or "_DASHBOARD_HTML_GZ" not in text:
                _log("File update khong hop le", "ERROR")
                return False

            # Verify SHA256
            file_hash = hashlib.sha256(data).hexdigest()
            server_hash = expected_sha256 or r.headers.get("X-SHA256", "")
            if server_hash and file_hash != server_hash:
                _log(f"SHA256 khong khop! Bo qua update", "ERROR")
                return False

            # Write new file
            with open(new_path, "wb") as f:
                f.write(data)

            # Backup current → .bak, new → current
            if os.path.exists(bak_path):
                os.remove(bak_path)
            os.rename(script_path, bak_path)
            os.rename(new_path, script_path)

            _log(f"Cap nhat thanh cong! ({len(data):,} bytes, SHA: {file_hash[:12]}...)")
            return True
        except Exception as e:
            _log(f"Loi cap nhat: {e}", "ERROR")
            return False


# Telegram
class TelegramNotifier:
    def __init__(self, token, chat_id):
        self.token = token
        self.chat_id = chat_id
        self.enabled = bool(token and chat_id)

    def send(self, msg):
        if not self.enabled:
            return
        try:
            requests.post(
                f"https://api.telegram.org/bot{self.token}/sendMessage",
                json={"chat_id": self.chat_id, "text": msg, "parse_mode": "HTML"},
                timeout=5,
            )
        except Exception:
            pass


# Position Manager
class PositionManager:
    def __init__(self, config, mt5_client):
        self.config = config
        self.mt5 = mt5_client
        self.entries = {}

    def register(self, ticket, action, entry_price, sl, tp, lots, atr):
        self.entries[ticket] = {
            "action": action, "entry": entry_price, "sl": sl, "tp": tp,
            "lots": lots, "atr": atr, "be_done": False, "partials": 0,
        }

    def manage_all(self, price):
        closed = []
        for ticket, e in self.entries.items():
            atr = e["atr"]
            if atr <= 0:
                continue
            is_buy = e["action"] == "BUY"
            dist = (price - e["entry"]) if is_buy else (e["entry"] - price)

            # Breakeven
            if not e["be_done"]:
                be_act = self.config.get("breakeven_activation_atr", 0.7) * atr
                if dist >= be_act:
                    offset = self.config.get("breakeven_offset_atr", 0.1) * atr
                    new_sl = e["entry"] + offset if is_buy else e["entry"] - offset
                    if self.mt5.modify_position(ticket, new_sl, e["tp"]):
                        e["sl"] = new_sl
                        e["be_done"] = True
                        _log(f"#{ticket} Breakeven @ {new_sl:.2f}")

            # Trailing
            trail_act = self.config.get("trailing_activation_atr", 1.5) * atr
            if dist >= trail_act:
                trail_dist = self.config.get("trailing_distance_atr", 1.0) * atr
                trail_step = self.config.get("trailing_step_atr", 0.3) * atr
                if is_buy:
                    new_sl = price - trail_dist
                    if new_sl > e["sl"] + trail_step:
                        if self.mt5.modify_position(ticket, new_sl, e["tp"]):
                            e["sl"] = new_sl
                            _log(f"#{ticket} Trail SL -> {new_sl:.2f}")
                else:
                    new_sl = price + trail_dist
                    if new_sl < e["sl"] - trail_step:
                        if self.mt5.modify_position(ticket, new_sl, e["tp"]):
                            e["sl"] = new_sl
                            _log(f"#{ticket} Trail SL -> {new_sl:.2f}")

            # Check if closed by broker
            if MT5_AVAILABLE:
                mt5_pos = mt5.positions_get(ticket=ticket)
                if not mt5_pos:
                    closed.append(ticket)

        for t in closed:
            _log(f"Lenh #{t} da dong")
            del self.entries[t]


# Bot Core
class TradingBotCore:
    def __init__(self):
        self.config = load_config()
        self.mt5 = MT5Client(self.config)
        self.server = ServerAPI(self.config)
        self.telegram = TelegramNotifier(
            self.config.get("telegram_bot_token", ""),
            self.config.get("telegram_chat_id", ""),
        )
        self.pos_mgr = None
        self.running = False
        self._thread = None
        self._authorized = False
        self._last_verify = None
        self._verify_interval = 900
        self._trades_total = 0

    def start(self):
        if self.running:
            return
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()

    def stop(self):
        self.running = False
        self.mt5.disconnect()
        with _bot_lock:
            _bot_state["running"] = False
            _bot_state["status"] = "STANDBY"
        _log("Bot da dung. Nhan START de chay lai.")

    def reload_config(self):
        self.config = load_config()
        self.server = ServerAPI(self.config)
        self.telegram = TelegramNotifier(
            self.config.get("telegram_bot_token", ""),
            self.config.get("telegram_chat_id", ""),
        )
        _log("Config da tai lai")

    def _run(self):
        self.running = True
        with _bot_lock:
            _bot_state["running"] = True
            _bot_state["status"] = "CONNECTING"

        _log("Kiem tra ket noi server...")
        if not self.server.health_check():
            _log(f"Khong ket noi duoc server: {self.config['server_url']}", "ERROR")
            self.stop()
            return

        ver = self.server.check_version()
        _log(f"Server version: {ver.get('version', '?')}")
        if ver.get("update_required"):
            _log(f"Phat hien phien ban moi: {ver.get('version')}! Dang cap nhat...")
            sha = ver.get("bot_pro_sha256")
            if self.server.auto_update(expected_sha256=sha):
                _log("Cap nhat thanh cong! Dang khoi dong lai...")
                self.telegram.send("\U0001f504 <b>Bot da tu dong cap nhat va khoi dong lai!</b>")
                time.sleep(1)
                sys.exit(42)  # exit code 42 = restart
            else:
                _log("Cap nhat that bai, tiep tuc voi phien ban hien tai", "ERROR")

        _log("Xac minh license...")
        lic = self.server.verify_license()
        if not lic.get("valid"):
            _log(f"License khong hop le: {lic.get('message', 'Unknown')}", "ERROR")
            self.stop()
            return
        _log(f"License OK - {lic.get('plan')}, {lic.get('expires_in_days')} ngay")
        self._authorized = True
        self._last_verify = time.time()

        _log("Ket noi MT5...")
        if not self.mt5.connect():
            _log("Khong ket noi duoc MT5!", "ERROR")
            self.stop()
            return

        # Mirror trading: KHONG dung PositionManager local (master quan ly trail/breakeven)
        self.pos_mgr = None

        with _bot_lock:
            _bot_state["status"] = "RUNNING"
            _bot_state["config"] = {
                k: v for k, v in self.config.items() if k != "mt5_password"
            }
        _log("Bot dang chay!")
        self.telegram.send("\U0001f916 <b>Bot da khoi dong!</b>")

        interval = self.config.get("update_interval_seconds", 60)
        _reconnect_fails = 0
        while self.running:
            try:
                # Kiểm tra kết nối MT5 trước mỗi tick
                if not mt5.terminal_info():
                    _log("MT5 mat ket noi! Dang reconnect...", "WARNING")
                    mt5.shutdown()
                    time.sleep(3)
                    if self.mt5.connect():
                        _log("Reconnect MT5 thanh cong!")
                        _reconnect_fails = 0
                    else:
                        _reconnect_fails += 1
                        _log(f"Reconnect that bai ({_reconnect_fails}/5)", "ERROR")
                        if _reconnect_fails >= 5:
                            _log("Qua nhieu lan reconnect that bai, dung bot", "ERROR")
                            break
                        time.sleep(10)
                        continue
                self._tick()
                _reconnect_fails = 0
            except Exception as e:
                _log(f"Loi tick: {e}", "ERROR")
            time.sleep(interval)
        self.stop()

    def _reauth(self):
        now = time.time()
        if self._last_verify and now - self._last_verify < self._verify_interval:
            return self._authorized
        lic = self.server.verify_license()
        self._last_verify = now
        self._authorized = lic.get("valid", False)
        if not self._authorized:
            _log("Xac thuc that bai!", "ERROR")
        return self._authorized

    def _tick(self):
        if not self._reauth():
            return

        tick = self.mt5.get_tick()
        if tick:
            with _bot_lock:
                _bot_state["price"] = {
                    "bid": float(tick.bid),
                    "ask": float(tick.ask),
                    "spread": round(float(tick.ask - tick.bid) / 0.01, 1),
                }

        bal = self.mt5.get_balance()
        eq = self.mt5.get_equity()
        with _bot_lock:
            _bot_state["account"]["balance"] = bal
            _bot_state["account"]["equity"] = eq
            _bot_state["account"]["pnl"] = round(eq - bal, 2)
            _bot_state["equity_history"].append(eq)
            if len(_bot_state["equity_history"]) > 200:
                _bot_state["equity_history"] = _bot_state["equity_history"][-200:]

        if not tick:
            return

        # Get OHLCV (still required by /execute schema, even if mirror ignores it)
        ohlcv = self.mt5.get_ohlcv(self.config.get("bars_to_fetch", 250))
        if not ohlcv or len(ohlcv) < 100:
            return

        open_pos = self.mt5.get_positions()

        # Build positions payload (include MT5 comment for mirror tracking)
        positions_payload = [
            {
                "ticket": int(p.ticket),
                "type": "BUY" if p.type == 0 else "SELL",
                "lots": float(p.volume),
                "price": float(p.price_open),
                "sl": float(p.sl) if p.sl else 0.0,
                "tp": float(p.tp) if p.tp else 0.0,
                "profit": float(p.profit),
                "open_time": datetime.utcfromtimestamp(p.time).isoformat() if hasattr(p, "time") else None,
                "comment": getattr(p, "comment", "") or "",
            }
            for p in open_pos
        ]

        with _bot_lock:
            _bot_state["positions"] = [
                {
                    "ticket": pp["ticket"],
                    "type": pp["type"],
                    "lots": pp["lots"],
                    "price": pp["price"],
                    "profit": pp["profit"],
                    "comment": pp["comment"],
                }
                for pp in positions_payload
            ]
            _bot_state["account"]["total_trades"] = self._trades_total

        spread_pips = round(float(tick.ask - tick.bid) / 0.01, 1)
        if spread_pips > 50:
            _log(f"Spread qua rong: {spread_pips:.1f} pips, bo qua tick", "ERROR")
            return

        # ── Mirror trading: hoi server lay command list ──
        resp = self.server.execute(
            balance=bal,
            current_price=float(tick.bid),
            spread_pips=spread_pips,
            positions=positions_payload,
            ohlcv=ohlcv,
            risk_percent=float(self.config.get("risk_percent", 0.02)),
        )

        commands = resp.get("commands", []) or []
        reasoning = resp.get("reasoning", "")
        atr_val = float(resp.get("atr", 0) or 0)

        with _bot_lock:
            _bot_state["signal"] = {
                "action": "MIRROR",
                "confidence": float(resp.get("confidence", 0) or 0),
                "atr": atr_val,
                "sl_price": 0.0,
                "tp_price": 0.0,
                "reasoning": reasoning,
            }

        if not commands:
            if reasoning:
                _log(f"No commands ({reasoning})")
            return

        _log(f"Mirror commands: {len(commands)} ({reasoning})")
        self._dispatch_commands(commands)

    def _dispatch_commands(self, commands):
        """Execute the list of commands returned by /execute (mirror trading)."""
        for cmd in commands:
            try:
                ctype = (cmd.get("cmd") or "").upper()
                reason = cmd.get("reason", "")

                if ctype == "OPEN":
                    action = (cmd.get("action") or "").upper()
                    lots = float(cmd.get("lots", 0))
                    sl = float(cmd.get("sl", 0))
                    tp = float(cmd.get("tp", 0))
                    comment = cmd.get("comment", "MIRROR")
                    if action not in ("BUY", "SELL") or lots <= 0:
                        _log(f"Skip OPEN: invalid action/lots ({action}/{lots})", "ERROR")
                        continue
                    _log(f"OPEN {action} {lots} lots [{comment}] — {reason}")
                    result = self.mt5.open_position(action, lots, sl, tp, comment=comment)
                    if result:
                        self._trades_total += 1
                        self.telegram.send(
                            f"\U0001f916 <b>MIRROR OPEN</b>\n"
                            f"{action} {lots} lots\n"
                            f"SL={sl:.2f} TP={tp:.2f}\n"
                            f"{comment}"
                        )

                elif ctype == "MODIFY":
                    ticket = int(cmd.get("ticket", 0))
                    sl = float(cmd.get("sl", 0))
                    tp = float(cmd.get("tp", 0))
                    if ticket <= 0:
                        continue
                    _log(f"MODIFY #{ticket} sl={sl:.2f} tp={tp:.2f} — {reason}")
                    self.mt5.modify_position(ticket, sl, tp)

                elif ctype == "PARTIAL_CLOSE":
                    ticket = int(cmd.get("ticket", 0))
                    lots = float(cmd.get("lots", 0))
                    if ticket <= 0 or lots <= 0:
                        continue
                    _log(f"PARTIAL_CLOSE #{ticket} {lots} lots — {reason}")
                    if self.mt5.close_position(ticket, lots=lots):
                        self.telegram.send(
                            f"\U0001f4b0 <b>PARTIAL CLOSE</b>\n#{ticket} {lots} lots\n{reason}"
                        )

                elif ctype == "CLOSE":
                    ticket = int(cmd.get("ticket", 0))
                    if ticket <= 0:
                        continue
                    _log(f"CLOSE #{ticket} — {reason}")
                    if self.mt5.close_position(ticket):
                        self.telegram.send(
                            f"\u274c <b>MIRROR CLOSE</b>\n#{ticket}\n{reason}"
                        )

                else:
                    _log(f"Unknown command type: {ctype}", "ERROR")

            except Exception as e:
                _log(f"Loi dispatch command {cmd}: {e}", "ERROR")


# HTTP Handler
class BotHTTPHandler(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def do_GET(self):
        if self.path == "/" or self.path == "/index.html":
            html = _get_html().encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", len(html))
            self.end_headers()
            self.wfile.write(html)
        elif self.path == "/api/state":
            with _bot_lock:
                data = json.loads(json.dumps(_bot_state))
            self._json(data)
        elif self.path == "/api/config":
            cfg = load_config()
            cfg.pop("mt5_password", None)
            self._json(cfg)
        else:
            self.send_response(404)
            self.end_headers()

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length)
        global _bot_instance
        if self.path == "/api/config":
            try:
                data = json.loads(body)
                save_config(data)
                if _bot_instance:
                    _bot_instance.reload_config()
                self._json({"ok": True})
            except Exception as e:
                self._json({"ok": False, "error": str(e)}, 500)
        elif self.path == "/api/start":
            if _bot_instance and not _bot_instance.running:
                _bot_instance.reload_config()
                _bot_instance.start()
                self._json({"ok": True})
            else:
                self._json({"ok": False, "message": "Bot dang chay"})
        elif self.path == "/api/stop":
            if _bot_instance:
                _bot_instance.stop()
            self._json({"ok": True})
        else:
            self.send_response(404)
            self.end_headers()

    def _json(self, data, code=200):
        b = json.dumps(data, ensure_ascii=False).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", len(b))
        self.end_headers()
        self.wfile.write(b)


# Main
def main():
    global _bot_instance

    port = 5678
    for p in range(5678, 5700):
        try:
            s = socket.socket()
            s.bind(("127.0.0.1", p))
            s.close()
            port = p
            break
        except OSError:
            pass

    _bot_instance = TradingBotCore()
    with _bot_lock:
        _bot_state["config"] = {
            k: v for k, v in _bot_instance.config.items() if k != "mt5_password"
        }

    url = f"http://127.0.0.1:{port}"
    print("=" * 55)
    print("  THOR ALPHA GOLD v1.2.3")
    print("  AI-Powered XAUUSD Trading System")
    print("  (c) 2026 KAI-ALPHA.COM")
    print(f"  Dashboard: {url}")
    print("=" * 55)
    print("  Dashboard da mo trong trinh duyet.")
    print("  Giu cua so nay mo. Nhan Ctrl+C de thoat.")
    print("=" * 55)

    threading.Timer(1.5, lambda: webbrowser.open(url)).start()

    # Auto start if config is filled
    cfg = _bot_instance.config
    if (
        not cfg.get("license_key", "").startswith("YOUR")
        and cfg.get("license_key", "").strip()
        and cfg.get("mt5_account", "")
        and cfg.get("mt5_password", "")
    ):
        _log("Config day du - tu dong khoi dong bot...")
        threading.Timer(2.0, _bot_instance.start).start()
    else:
        _log("Mo tab SETTINGS de nhap License Key va thong tin MT5")

    server = ThreadingHTTPServer(("127.0.0.1", port), BotHTTPHandler)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nDang thoat...")
        if _bot_instance:
            _bot_instance.stop()


if __name__ == "__main__":
    main()
