"""
Startup Gate — 启动前自检门禁。

在任何交易进程启动前运行，检查：
1. 状态文件存在且可读（agent_state, execution_state, position_lifecycle, agent_decision_contract）
2. 代码一致性（release 目录 vs 源码目录，防止漂移）
3. 关键契约不变量（macro direction → allowed_side 一致）
4. 桥接路径存在
5. 商业化门禁：许可证有效性
6. 商业化门禁：风险披露已接受

返回: {"ok": True/False, "checks": [...], "failures": [...]}
所有不通过即标红 FAIL，不允许 "看起来还行"。

Gate 5 和 Gate 6 默认 SKIP（向后兼容），设置环境变量
  GAT_COMMERCIAL_MODE=1  启用商业化门禁
"""

from __future__ import annotations

import json
import hashlib
import os
import shutil
import sys
import time
from pathlib import Path
from typing import Any, Optional

from src.runtime_topology import inspect_runtime_config, inspect_runtime_processes

_HEARTBEAT_STALE_SEC = 120.0
_EXEC_REPORT_STALE_SEC = 300.0


def _resolve_project_root() -> Path:
    return Path(__file__).resolve().parents[1]


def _resolve_app_dir() -> Optional[Path]:
    env = os.environ.get("LOBSTERQUANT_APP_DIR", "").strip()
    if env:
        return Path(env)
    try:
        import config_manager
        d = config_manager.get_app_dir()
        return Path(d) if d else None
    except Exception:
        return None


def _resolve_release_root(app_dir: Optional[Path]) -> Optional[Path]:
    """Auto-detect the active managed_app release directory."""
    if app_dir is None:
        return None
    managed = app_dir / "managed_app" / "releases"
    if not managed.exists():
        return None
    # Look for the highest version release that's not staging
    best = None
    best_ver = (0, 0, 0)
    for d in managed.iterdir():
        if not d.is_dir() or d.name.startswith("."):
            continue
        try:
            parts = tuple(int(x) for x in d.name.split("."))
            if len(parts) >= 2 and parts > best_ver:
                best_ver = parts
                best = d
        except ValueError:
            pass
    return best


def _check_file(path: Path, label: str) -> dict:
    if not path.exists():
        return {"check": label, "status": "FAIL", "reason": f"file missing: {path}"}
    try:
        with open(path, encoding="utf-8") as f:
            json.load(f)
        return {"check": label, "status": "PASS"}
    except json.JSONDecodeError as e:
        return {"check": label, "status": "FAIL", "reason": f"invalid JSON: {e}"}
    except Exception as e:
        return {"check": label, "status": "FAIL", "reason": str(e)}


def _check_bridge_dirs(project_root: Path) -> list[dict]:
    """Check bridge to_ea/from_ea directories via ea_bridge_paths."""
    results = []
    try:
        try:
            from src.ea_bridge_paths import resolve_bridge_layout
        except ImportError:
            sys.path.insert(0, str(project_root))
            from src.ea_bridge_paths import resolve_bridge_layout  # type: ignore[no-redef]
        layout = resolve_bridge_layout(project_root)
        for key in ["to_ea", "from_ea"]:
            d = layout.get(key)
            if d and d.exists() and d.is_dir():
                results.append({"check": f"bridge/{key}", "status": "PASS", "detail": str(d)})
            else:
                results.append({"check": f"bridge/{key}", "status": "FAIL",
                                "reason": f"directory missing or inaccessible: {d}"})
    except Exception as e:
        results.append({"check": "bridge/layout", "status": "FAIL", "reason": str(e)})
    return results


def _resolve_code_root() -> Path:
    return Path(__file__).resolve().parents[1]


def _bridge_ea_source_files() -> list[Path]:
    code_root = _resolve_code_root()
    bridge_dir = code_root / "bridge"
    return [
        bridge_dir / "GoldAutoTradeBridgeEA.mq5",
        bridge_dir / "PositionReporterEA.mq5",
    ]


def _discover_mt5_experts_dirs(*, include_deep_scan: bool = True) -> list[Path]:
    try:
        import config_manager
    except Exception:
        return []

    config = config_manager.load_app_config()
    candidates: list[Path] = []
    seen: set[str] = set()

    def _add(path: Path, *, allow_missing: bool = False) -> None:
        try:
            resolved = str(path.resolve())
        except Exception:
            resolved = str(path)
        if resolved in seen:
            return
        if (path.exists() and path.is_dir()) or allow_missing:
            seen.add(resolved)
            candidates.append(path)

    configured = str(config.get("mt5_experts_dir", "") or "").strip()
    if configured:
        _add(Path(configured), allow_missing=True)

    try:
        mt5_common = Path(config_manager.get_mt5_dir())
        if (
            mt5_common.name.lower() == "files"
            and mt5_common.parent.name.lower() == "common"
            and mt5_common.parent.parent.exists()
        ):
            terminal_root = mt5_common.parent.parent
            for child in terminal_root.iterdir():
                if not child.is_dir() or child.name.lower() in {"common", "community", "help"}:
                    continue
                _add(child / "MQL5" / "Experts", allow_missing=True)
    except Exception:
        pass

    roaming_roots: list[Path] = []
    appdata = os.environ.get("APPDATA", "").strip()
    if appdata:
        roaming_roots.append(Path(appdata))
    userprofile = os.environ.get("USERPROFILE", "").strip()
    if userprofile:
        roaming_roots.append(Path(userprofile) / "AppData" / "Roaming")

    for roaming_root in roaming_roots:
        terminal_root = roaming_root / "MetaQuotes" / "Terminal"
        if not terminal_root.exists():
            continue
        for child in terminal_root.iterdir():
            if not child.is_dir() or child.name.lower() in {"common", "community", "help"}:
                continue
            _add(child / "MQL5" / "Experts", allow_missing=True)

            origin_path = child / "origin.txt"
            if origin_path.exists():
                try:
                    origin_dir = Path(origin_path.read_text(encoding="utf-8").strip())
                    if origin_dir.exists() and origin_dir.is_dir():
                        _add(origin_dir / "MQL5" / "Experts", allow_missing=True)
                except Exception:
                    pass

    for process_name in ("terminal64", "terminal", "terminal64.exe", "terminal.exe"):
        try:
            import psutil

            for proc in psutil.process_iter(["name", "exe"]):
                if str(proc.info.get("name", "")).lower() not in (process_name.lower(), process_name.lower().replace(".exe", "")):
                    continue
                exe_path = str(proc.info.get("exe", "") or "").strip()
                if not exe_path:
                    continue
                install_dir = Path(exe_path).parent
                if (install_dir / "MQL5").is_dir():
                    _add(install_dir / "MQL5" / "Experts", allow_missing=True)
                elif (install_dir.parent / "MQL5").is_dir():
                    _add(install_dir.parent / "MQL5" / "Experts", allow_missing=True)
        except Exception:
            pass

    # 5. Windows where 命令搜索 terminal64.exe（兜底）
    if include_deep_scan and sys.platform == "win32":
        try:
            import subprocess as _sp
            for search_root in (
                os.environ.get("ProgramFiles", "C:\\Program Files"),
                os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)"),
            ):
                if not search_root or not Path(search_root).exists():
                    continue
                try:
                    output = _sp.check_output(
                        ["where", "/r", search_root, "terminal64.exe"],
                        timeout=15, stderr=_sp.DEVNULL
                    ).decode("utf-8", errors="replace").strip()
                    if not output:
                        continue
                    for line in output.splitlines():
                        line = line.strip()
                        if not line:
                            continue
                        p = Path(line)
                        if not p.exists():
                            continue
                        experts = p.parent / "MQL5" / "Experts"
                        if experts.exists():
                            _add(experts)
                        else:
                            _add(experts, allow_missing=True)
                except Exception:
                    continue
        except Exception:
            pass

    return candidates


def ensure_mt5_bridge_ea_ready(*, include_deep_scan: bool = True) -> list[dict]:
    source_files = [path for path in _bridge_ea_source_files() if path.exists()]
    if not source_files:
        return [
            {
                "check": "mt5_bridge/ea_autodeploy",
                "status": "FAIL",
                "reason": "bundled EA source files are missing",
            }
        ]

    # * 创建桥接目录 Common/Files/gold_autotrade/bridge/ （确保 EA 通信目录就绪）
    bridge_dirs_created: list[str] = []
    try:
        import config_manager as _cm
        _mt5_common = Path(_cm.get_mt5_dir())
        if _mt5_common.exists() and _mt5_common.is_dir():
            for _sub in ("", "to_ea", "from_ea"):
                _d = _mt5_common / "gold_autotrade" / "bridge" / _sub
                _d.mkdir(parents=True, exist_ok=True)
                bridge_dirs_created.append(str(_d))
    except Exception:
        pass

    experts_dirs = _discover_mt5_experts_dirs(include_deep_scan=include_deep_scan)
    if not experts_dirs:
        return [
            {
                "check": "mt5_bridge/ea_autodeploy",
                "status": "SKIP" if not bridge_dirs_created else "PASS",
                "reason": "no MT5 Experts directories detected" if not bridge_dirs_created else "",
                "detail": f"bridge directories created, EA deploy pending MT5 Experts discovery"
                         if bridge_dirs_created else "no MT5 Experts directories detected",
                "metadata": {
                    "bridge_dirs_created": bridge_dirs_created,
                },
            }
        ]

    copied: list[str] = []
    unchanged: list[str] = []
    failed: list[str] = []

    for experts_dir in experts_dirs:
        for src in source_files:
            target = experts_dir / src.name
            try:
                experts_dir.mkdir(parents=True, exist_ok=True)
                src_hash = hashlib.sha256(src.read_bytes()).hexdigest()
                dst_hash = hashlib.sha256(target.read_bytes()).hexdigest() if target.exists() else ""
                if target.exists() and dst_hash == src_hash:
                    unchanged.append(str(target))
                    continue
                shutil.copy2(src, target)
                copied.append(str(target))
            except Exception as exc:
                failed.append(f"{target}: {exc}")

    if failed:
        return [
            {
                "check": "mt5_bridge/ea_autodeploy",
                "status": "FAIL",
                "reason": "; ".join(failed[:3]),
                "metadata": {
                    "copied": copied,
                    "unchanged": unchanged,
                    "failed": failed,
                    "bridge_dirs_created": bridge_dirs_created,
                },
            }
        ]

    action = "copied" if copied else "already_synced"
    detail = (
        f"copied {len(copied)} file(s) into {len(experts_dirs)} MT5 Experts dir(s)"
        if copied
        else f"already synchronized across {len(experts_dirs)} MT5 Experts dir(s)"
    )
    return [
        {
            "check": "mt5_bridge/ea_autodeploy",
            "status": "PASS",
            "detail": detail,
            "metadata": {
                "action": action,
                "copied": copied,
                "unchanged": unchanged,
                "bridge_dirs_created": bridge_dirs_created,
                "experts_dirs": [str(path) for path in experts_dirs],
                "source_files": [str(path) for path in source_files],
            },
        }
    ]


def _check_code_integrity(project_root: Path, release_root: Optional[Path]) -> list[dict]:
    """Check that release directory code matches source (no drift)."""
    if release_root is None:
        return [{"check": "code_integrity", "status": "SKIP", "reason": "no release_root available"}]

    critical_files = [
        "bridge/gold_trader.py",
        "src/pipeline.py",
        "bridge/commander_client.py",
        "src/mt5_execution.py",
        "bridge/exit_optimizer.py",
        "src/position_lifecycle.py",
    ]
    results = []
    for f in critical_files:
        src = project_root / f
        rel = release_root / f
        if not src.exists():
            results.append({"check": f"code_integrity/{f}", "status": "SKIP", "reason": "source missing"})
            continue
        if not rel.exists():
            results.append({"check": f"code_integrity/{f}", "status": "FAIL", "reason": f"release file missing: {rel}"})
            continue
        try:
            src_hash = hashlib.md5(src.read_bytes()).hexdigest()
            rel_hash = hashlib.md5(rel.read_bytes()).hexdigest()
            if src_hash == rel_hash:
                results.append({"check": f"code_integrity/{f}", "status": "PASS"})
            else:
                results.append({"check": f"code_integrity/{f}", "status": "FAIL",
                                "reason": f"hash mismatch src={src_hash[:8]} rel={rel_hash[:8]}"})
        except Exception as e:
            results.append({"check": f"code_integrity/{f}", "status": "FAIL", "reason": str(e)})
    return results


def _check_contract_invariants(app_dir: Path) -> list[dict]:
    """Verify key invariants: macro direction → allowed_side consistency."""
    results = []
    contract_path = app_dir / "data" / "agent_decision_contract.json"
    if not contract_path.exists():
        return [{"check": "contract_invariant", "status": "SKIP", "reason": "contract file missing"}]

    try:
        with open(contract_path, encoding="utf-8") as f:
            contract = json.load(f)
    except Exception as e:
        return [{"check": "contract_invariant", "status": "FAIL", "reason": str(e)}]

    macro = contract.get("macro", {}) if isinstance(contract.get("macro", {}), dict) else {}
    direction = str(macro.get("direction", "NEUTRAL") or "NEUTRAL").upper()
    degraded = bool(macro.get("degraded", True))
    confidence = float(macro.get("confidence", 0.0) or 0.0)

    # Invariant 1: direction must be LONG/SHORT/NEUTRAL
    if direction in ("LONG", "SHORT", "NEUTRAL"):
        results.append({"check": "invariant/macro_direction_valid", "status": "PASS"})
    else:
        results.append({"check": "invariant/macro_direction_valid", "status": "FAIL",
                        "reason": f"invalid direction: {direction}"})

    # Invariant 2: confidence range [0, 1]
    if 0.0 <= confidence <= 1.0:
        results.append({"check": "invariant/confidence_range", "status": "PASS"})
    else:
        results.append({"check": "invariant/confidence_range", "status": "FAIL",
                        "reason": f"confidence out of range: {confidence}"})

    # Invariant 3: if FRESH and directional, binding must match
    binding = str(macro.get("binding_direction", "NEUTRAL") or "NEUTRAL").upper()
    allowed_side = str(macro.get("allowed_side", "BOTH") or "BOTH").upper()
    if not degraded and binding in ("LONG", "SHORT"):
        expected_allowed = "BUY" if binding == "LONG" else "SELL"
        if allowed_side == expected_allowed or allowed_side == "BOTH":
            results.append({"check": "invariant/binding_allowed_side", "status": "PASS"})
        else:
            results.append({"check": "invariant/binding_allowed_side", "status": "FAIL",
                            "reason": f"binding={binding} but allowed_side={allowed_side} (expected {expected_allowed})"})
    else:
        results.append({"check": "invariant/binding_allowed_side", "status": "PASS",
                        "detail": f"degraded={degraded} binding={binding}"})

    return results


def _check_runtime_topology(project_root: Path, *, current_pid: Optional[int] = None) -> list[dict]:
    report = inspect_runtime_processes(project_root, current_pid=current_pid)
    checks: list[dict] = []
    issues = report.get("issues", [])
    if issues:
        for issue in issues:
            checks.append(
                {
                    "check": f"runtime_topology/{issue.get('code', 'unknown')}",
                    "status": "FAIL",
                    "reason": issue.get("message", "runtime topology issue"),
                }
            )
    counts = report.get("process_counts", {}) if isinstance(report, dict) else {}
    if int(counts.get("pipeline_live_trade", 0) or 0) > 0:
        checks.append(
            {
                "check": "runtime_topology/existing_live_pipeline",
                "status": "FAIL",
                "reason": "another live pipeline process is still running; clear stale runtime before starting",
            }
        )
    if int(counts.get("gold_trader", 0) or 0) > 0:
        checks.append(
            {
                "check": "runtime_topology/existing_gold_trader",
                "status": "FAIL",
                "reason": "another gold_trader process is still running; clear stale runtime before starting",
            }
        )
    if not checks:
        checks.append(
            {
                "check": "runtime_topology",
                "status": "PASS",
                "detail": "no conflicting runtime processes detected",
            }
        )
    return checks


def _check_required_runtime_params(project_root: Path) -> list[dict]:
    report = inspect_runtime_config(project_root)
    issues = report.get("issues", [])
    if issues:
        return [
            {
                "check": f"runtime_config/{issue.get('code', 'unknown')}",
                "status": "FAIL" if issue.get("level") == "ERROR" else "PASS",
                "reason": issue.get("message", "runtime config issue"),
            }
            for issue in issues
        ]
    return [
        {
            "check": "runtime_config/required_params",
            "status": "PASS",
            "detail": "required live params are explicitly configured",
        }
    ]


def _read_json_dict(path: Path) -> dict[str, Any]:
    try:
        if path.exists():
            payload = json.loads(path.read_text(encoding="utf-8"))
            if isinstance(payload, dict):
                return payload
    except Exception:
        pass
    return {}


def _payload_has_open_positions(payload: dict[str, Any]) -> bool:
    if bool(payload.get("position_open", False)):
        return True
    for key in ("positions_all", "positions"):
        rows = payload.get(key, [])
        if isinstance(rows, list) and any(isinstance(row, dict) for row in rows):
            return True
    return False


def _runtime_position_open(app_dir: Optional[Path]) -> bool:
    if app_dir is None:
        return False
    runtime_guard = _read_json_dict(app_dir / "data" / "runtime_open_guard.json")
    if _payload_has_open_positions(runtime_guard):
        return True
    lifecycle = _read_json_dict(app_dir / "data" / "position_lifecycle.json")
    return _payload_has_open_positions(lifecycle)


def _resolve_terminal_status_path() -> Optional[Path]:
    try:
        import config_manager

        mt5_dir = Path(config_manager.get_mt5_dir() or "")
        if not str(mt5_dir).strip():
            return None
        candidates = [
            mt5_dir / "gold_autotrade" / "bridge" / "terminal_status.json",
            mt5_dir / "gold_autotrade" / "bridge" / "market_snapshot.json",
            mt5_dir / "terminal_status.json",
            mt5_dir / "market_snapshot.json",
        ]
        for candidate in candidates:
            if candidate.exists():
                return candidate
    except Exception:
        return None
    return None


def _file_age_sec(path: Optional[Path]) -> Optional[float]:
    if path is None or not path.exists():
        return None
    try:
        return max(0.0, time.time() - float(path.stat().st_mtime))
    except Exception:
        return None


def _collect_bridge_health(app_dir: Optional[Path]) -> dict[str, Any]:
    report_path = app_dir / "data" / "inbox" / "execution_report.json" if app_dir is not None else None
    terminal_status_path = _resolve_terminal_status_path()
    heartbeat_age_sec = _file_age_sec(terminal_status_path)
    exec_report_age_sec = _file_age_sec(report_path)
    heartbeat_ok = heartbeat_age_sec is not None and heartbeat_age_sec <= _HEARTBEAT_STALE_SEC
    exec_report_ok = exec_report_age_sec is not None and exec_report_age_sec <= _EXEC_REPORT_STALE_SEC
    position_open = _runtime_position_open(app_dir)
    management_signal_ok = heartbeat_ok or exec_report_ok
    return {
        "terminal_status_path": str(terminal_status_path) if terminal_status_path else "",
        "execution_report_path": str(report_path) if report_path else "",
        "heartbeat_ok": heartbeat_ok,
        "heartbeat_age_sec": heartbeat_age_sec,
        "execution_report_ok": exec_report_ok,
        "execution_report_age_sec": exec_report_age_sec,
        "position_open": position_open,
        "management_signal_ok": management_signal_ok,
        "full_trading_ready": management_signal_ok,
    }


def _derive_runtime_mode(
    *,
    fatal_failures: list[dict],
    degraded_failures: list[dict],
    bridge_health: dict[str, Any],
) -> str:
    if fatal_failures:
        return "BOOTSTRAP_REPAIR"
    if degraded_failures:
        if bool(bridge_health.get("position_open", False)) and bool(bridge_health.get("management_signal_ok", False)):
            return "MANAGE_ONLY"
        return "SAFE_OBSERVE"
    return "FULL_TRADING"


def _summarize_checks(checks: list[dict], *, app_dir: Optional[Path] = None) -> dict[str, Any]:
    failures = [c for c in checks if c.get("status") == "FAIL"]
    degraded_failures = [c for c in failures if _is_bridge_related_failure(c)]
    fatal_failures = [c for c in failures if c not in degraded_failures]
    bridge_health = _collect_bridge_health(app_dir)
    runtime_mode = _derive_runtime_mode(
        fatal_failures=fatal_failures,
        degraded_failures=degraded_failures,
        bridge_health=bridge_health,
    )
    permissions = _permissions_for_runtime_mode(runtime_mode)
    return {
        "ok": len(failures) == 0,
        "total_checks": len(checks),
        "passed": sum(1 for c in checks if c.get("status") == "PASS"),
        "failed": len(failures),
        "skipped": sum(1 for c in checks if c.get("status") == "SKIP"),
        "runtime_mode": runtime_mode,
        "permissions": permissions,
        "issues": [
            {
                "check": c.get("check", ""),
                "reason": c.get("reason", ""),
                "bridge_related": _is_bridge_related_failure(c),
            }
            for c in failures
        ],
        "self_heal_actions": _collect_self_heal_actions(checks),
        "bridge_health": bridge_health,
        "checks": checks,
        "failures": [f"{c['check']}: {c.get('reason', 'unknown')}" for c in failures],
    }


def _is_bridge_related_failure(check: dict[str, Any]) -> bool:
    name = str(check.get("check", "") or "")
    return name.startswith("bridge/") or name.startswith("mt5_bridge/")


def _permissions_for_runtime_mode(runtime_mode: str) -> dict[str, bool]:
    if runtime_mode == "FULL_TRADING":
        return {
            "can_open_new_position": True,
            "can_close_position": True,
            "can_modify_sl_tp": True,
            "can_sync_positions": True,
            "can_emit_alerts": True,
        }
    if runtime_mode == "MANAGE_ONLY":
        return {
            "can_open_new_position": False,
            "can_close_position": True,
            "can_modify_sl_tp": True,
            "can_sync_positions": True,
            "can_emit_alerts": True,
        }
    return {
        "can_open_new_position": False,
        "can_close_position": False,
        "can_modify_sl_tp": False,
        "can_sync_positions": False,
        "can_emit_alerts": True,
    }


def _collect_self_heal_actions(checks: list[dict]) -> list[str]:
    actions: list[str] = []
    for check in checks:
        check_name = str(check.get("check", "") or "")
        status = str(check.get("status", "") or "").upper()
        if check_name == "mt5_bridge/ea_autodeploy":
            metadata = check.get("metadata", {}) if isinstance(check.get("metadata"), dict) else {}
            if metadata.get("bridge_dirs_created"):
                actions.append("bridge_dirs_seeded")
            if status == "PASS" and metadata.get("copied"):
                actions.append("ea_sources_synced")
        elif check_name.startswith("bridge/") and status == "FAIL":
            actions.append("bridge_layout_repair_needed")
    deduped: list[str] = []
    seen: set[str] = set()
    for action in actions:
        if action in seen:
            continue
        seen.add(action)
        deduped.append(action)
    return deduped


def run_runtime_startup_gate(
    project_root: Optional[Path] = None,
    app_dir: Optional[Path] = None,
    *,
    current_pid: Optional[int] = None,
) -> dict[str, Any]:
    """Focused startup gate for live runtime launch paths."""
    if project_root is None:
        project_root = _resolve_project_root()
    if app_dir is None:
        app_dir = _resolve_app_dir()
    if app_dir is None:
        return {"ok": False, "checks": [], "failures": ["APP_DIR not resolved"]}

    checks: list[dict] = []
    checks.extend(_check_runtime_topology(project_root, current_pid=current_pid))
    checks.extend(_check_required_runtime_params(project_root))
    checks.extend(ensure_mt5_bridge_ea_ready(include_deep_scan=False))
    return _summarize_checks(checks, app_dir=app_dir)


def run_startup_gate(
    project_root: Optional[Path] = None,
    release_root: Optional[Path] = None,
    app_dir: Optional[Path] = None,
) -> dict[str, Any]:
    """Run all startup checks. Returns {"ok": bool, "checks": [...], "failures": [...]}."""
    if project_root is None:
        project_root = _resolve_project_root()
    if app_dir is None:
        app_dir = _resolve_app_dir()
    if app_dir is None:
        return {"ok": False, "checks": [], "failures": ["APP_DIR not resolved"]}

    all_checks: list[dict] = []

    # Gate 0: runtime topology + required live params
    all_checks.extend(_check_runtime_topology(project_root))
    all_checks.extend(_check_required_runtime_params(project_root))

    # Gate 1: State files
    state_files = [
        (app_dir / "data" / "bridge" / "state" / "agent_state.json", "agent_state.json"),
        (app_dir / "data" / "execution_state.json", "execution_state.json"),
        (app_dir / "data" / "position_lifecycle.json", "position_lifecycle.json"),
        (app_dir / "data" / "agent_decision_contract.json", "agent_decision_contract.json"),
    ]
    for path, label in state_files:
        all_checks.append(_check_file(path, label))

    # Gate 2: Bridge directories
    all_checks.extend(_check_bridge_dirs(project_root))
    all_checks.extend(ensure_mt5_bridge_ea_ready())

    # Gate 3: Code integrity (source vs release)
    if release_root is None:
        release_root = _resolve_release_root(app_dir)
    all_checks.extend(_check_code_integrity(project_root, release_root))

    # Gate 4: Contract invariants
    all_checks.extend(_check_contract_invariants(app_dir))

    # Gate 5: License check (commercial mode only)
    commercial = os.environ.get("GAT_COMMERCIAL_MODE", "0") == "1"
    if commercial:
        all_checks.append(_check_license())
        all_checks.append(_check_compliance())

    return _summarize_checks(all_checks, app_dir=app_dir)


def _check_license() -> dict:
    """Gate 5: Verify license is valid (commercial mode)."""
    try:
        from src.license.validator import load_license, get_license_status
        lic = load_license()
        if not lic:
            return {"check": "commercial/license", "status": "FAIL",
                    "reason": "No license found. Run: python3 -m src.license.activate --key <key>"}
        if lic.get("error"):
            return {"check": "commercial/license", "status": "FAIL",
                    "reason": f"License error: {lic['error']}"}
        return {"check": "commercial/license", "status": "PASS",
                "detail": get_license_status()}
    except ImportError as e:
        return {"check": "commercial/license", "status": "SKIP", "reason": str(e)}
    except Exception as e:
        return {"check": "commercial/license", "status": "FAIL", "reason": str(e)}


def _check_compliance() -> dict:
    """Gate 6: Verify risk disclosure accepted (commercial mode)."""
    try:
        from src.compliance.disclaimers import should_block_live_trading
        if should_block_live_trading():
            return {"check": "commercial/risk_disclosure", "status": "FAIL",
                    "reason": "Risk disclosure not accepted. Live trading blocked."}
        return {"check": "commercial/risk_disclosure", "status": "PASS"}
    except ImportError as e:
        return {"check": "commercial/risk_disclosure", "status": "SKIP", "reason": str(e)}
    except Exception as e:
        return {"check": "commercial/risk_disclosure", "status": "FAIL", "reason": str(e)}


if __name__ == "__main__":
    result = run_startup_gate()
    print(json.dumps(result, indent=2, ensure_ascii=False))
    sys.exit(0 if result["ok"] else 1)
