#!/usr/bin/python3
"""Manual launcher for the captured FrameBridge Original programs."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import signal
import stat
import socket
import subprocess
import sys
import time

PREFIX = Path(__file__).resolve().parents[1]
REAL_HOME = Path.home()
DEFAULT_STATE = Path(os.environ.get("XDG_STATE_HOME", str(REAL_HOME / ".local/state"))) / "framebridge-original"
LAYOUT = json.loads((PREFIX / "layout.json").read_text())
CORE_COMMANDS = ["bash", "tmux", "tee", "flock", "awk", "sha256sum", "timeout",
                 "gnome-terminal", "wmctrl"]

def settings(args):
    state = Path(args.state_dir).expanduser().absolute()
    home = state / "home"
    runtime_base = Path("/run/user") / str(os.getuid())
    if not runtime_base.is_dir() or runtime_base.stat().st_uid != os.getuid():
        runtime_base = Path("/tmp")
    runtime = runtime_base / ("fbo-" + str(os.getuid()) + "-" + hashlib.sha256(str(state).encode()).hexdigest()[:16])
    env = os.environ.copy()
    authority = env.get("XAUTHORITY")
    if not authority and (REAL_HOME / ".Xauthority").is_file():
        authority = str(REAL_HOME / ".Xauthority")
    env.update({
        "HOME": str(home), "XDG_CONFIG_HOME": str(home / ".config"),
        "XDG_DATA_HOME": str(home / ".local/share"),
        "XDG_STATE_HOME": str(home / ".local/state"), "XDG_CACHE_HOME": str(home / ".cache"),
        "XDG_RUNTIME_DIR": str(runtime), "TMPDIR": str(runtime / "tmp"),
        "TMUX_TMPDIR": str(runtime / "tmux"), "PYTHONDONTWRITEBYTECODE": "1",
        "FBTERM_ROOT": str(home / ".local/share/framebridge/fbterm"),
        "FRAMEBRIDGE_FBTERM_ROOT_OVERRIDE": str(home / ".local/share/framebridge/fbterm"),
        "FBTERM_SESSION": "framebridge-original-fbterm",
        "FBTERM_TITLE": "FrameBridge Original",
        "FRAMEBRIDGE_FBTERM_ROOT": str(home / ".local/share/framebridge/fbterm"),
        "FRAMEBRIDGE_FBTERM_ENGINE": str(PREFIX / LAYOUT["fbterm_bin"] / "fbterm_engine.py"),
        "FRAMEBRIDGE_FBTERM_SESSION": "framebridge-original-fbterm",
        "FRAMEBRIDGE_FBTERM_TITLE": "FrameBridge Original",
        "FRAMEBRIDGE_EXEC_SESSION": "framebridge-original",
        "FRAMEBRIDGE_RESCUE_SESSION": "framebridge-original-rescue",
        "FRAMEBRIDGE_UI_STATE_DIR": str(home / ".local/share/framebridge/ui-state"),
        "FRAMEBRIDGE_PROGRESS_DIR": str(home / ".local/state/framebridge/progress"),
        "FRAMEBRIDGE_PROGRESS_LOG": str(home / ".local/state/framebridge/progress/framebridge-progress.log"),
        "FRAMEBRIDGE_TOOL_ROOT": str(runtime / "adapter"),
        "FRAMEBRIDGE_NATIVE_HOST": str(PREFIX / LAYOUT["native_host"]),
        "FRAMEBRIDGE_FRONT_DOOR_HOST": "127.0.0.1",
        "FRAMEBRIDGE_FRONT_DOOR_PORT": str(args.port),
        "FRAMEBRIDGE_FLEET_HUB_HOST": "127.0.0.1",
        "FRAMEBRIDGE_FLEET_HUB_PORT": str(args.hub_port),
        "FRAMEBRIDGE_HUB_MCP_URL": "http://127.0.0.1:" + str(args.hub_port) + "/mcp",
        "FRAMEBRIDGE_BROKER_CONTROL": "http://127.0.0.1:" + str(args.broker_port),
        "FRAMEBRIDGE_ASUS_NODE_ID": "", "FRAMEBRIDGE_GERAMEG_NODE_ID": "",
        "FRAMEBRIDGE_CLOUD_DSM_SOCKET": str(runtime / "cloud-unconfigured.sock"),
        "FRAMEBRIDGE_NODE_REGISTRY": str(home / ".config/framebridge/nodes.json"),
        "FRAMEBRIDGE_ACCOUNT_BINDING": str(home / ".config/framebridge/account-binding.json"),
        "FRAMEBRIDGE_PAIRED_DISCOVERY": str(home / ".config/framebridge/paired-nodes.json"),
        "FRAMEBRIDGE_ENABLE_GERAMEG_CUA_TTY": "0",
        "PYTHONPATH": os.pathsep.join(str(PREFIX / p) for p in [
            "original/geranas/FrameBridge-Fleet-Hub/vendor",
            "original/geranas/FrameBridge-Fleet-Hub/app", "portable"]),
    })
    env.pop("TMUX", None)
    env.pop("TMUX_PANE", None)
    if authority:
        env["XAUTHORITY"] = authority
    return state, home, env

def prepare(args):
    state, home, env = settings(args)
    runtime = Path(env["XDG_RUNTIME_DIR"])
    try:
        runtime.mkdir(mode=0o700)
    except FileExistsError:
        pass
    st = runtime.lstat()
    if not stat.S_ISDIR(st.st_mode) or st.st_uid != os.getuid() or stat.S_IMODE(st.st_mode) != 0o700:
        raise RuntimeError("Runtime directory must be private and owned by this user: " + str(runtime))
    for p in [runtime, runtime / "tmp", runtime / "tmux", runtime / "adapter", state, home, state / "run", state / "tmp", state / "tmux",
              state / "logs", home / ".config/framebridge",
              home / ".local/share/framebridge/fbterm",
              home / ".local/share/framebridge/tool-adapter",
              home / ".local/lib/framebridge-native",
              home / "Descargas/framebridge_v0_1"]:
        p.mkdir(parents=True, exist_ok=True)
        p.chmod(0o700)
    links = {
        home / ".local/share/framebridge/fbterm/bin": PREFIX / LAYOUT["fbterm_bin"],
        home / ".local/share/framebridge/tool-adapter/bin": PREFIX / LAYOUT["adapter_bin"],
        home / ".local/lib/framebridge-native/host": PREFIX / LAYOUT["native_host_dir"],
        home / "Descargas/framebridge_v0_1/host": PREFIX / LAYOUT["native_host_dir"],
    }
    for dst, src in links.items():
        if dst.is_symlink():
            if dst.resolve() != src.resolve():
                raise RuntimeError("Unexpected link: " + str(dst))
        elif dst.exists():
            raise RuntimeError("Runtime program path already exists: " + str(dst))
        else:
            dst.symlink_to(src, target_is_directory=True)
    registry = home / ".config/framebridge/nodes.json"
    if not registry.exists():
        registry.write_text(json.dumps({"nodes": {}}, indent=2) + "\n")
        registry.chmod(0o600)
    return state, home, env

def birth(pid):
    try:
        fields = Path("/proc/" + str(pid) + "/stat").read_text().rsplit(") ", 1)[1].split()
        if fields[0] in {"Z", "X"}:
            return None
        return fields[19]
    except (OSError, IndexError):
        return None

def live(record):
    expected = record.get("birth")
    return expected is not None and birth(record["pid"]) == expected

def load_record(state):
    p = state / "processes.json"
    if not p.exists():
        return {"processes": []}
    return json.loads(p.read_text())

def stop(args):
    state, home, env = settings(args)
    records = load_record(state)
    for rec in reversed(records.get("processes", [])):
        if live(rec):
            os.killpg(rec["pid"], signal.SIGTERM)
    deadline = time.monotonic() + 5
    while any(live(r) for r in records.get("processes", [])) and time.monotonic() < deadline:
        time.sleep(.1)
    for rec in records.get("processes", []):
        if live(rec):
            os.killpg(rec["pid"], signal.SIGKILL)
    if Path(env["TMUX_TMPDIR"]).is_dir() and shutil.which("tmux"):
        subprocess.run(["tmux", "kill-server"], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    (state / "processes.json").unlink(missing_ok=True)
    print("FrameBridge Original detenido. Estado: " + str(state))

def start(args):
    missing = [name for name in CORE_COMMANDS if not shutil.which(name)]
    if missing:
        raise RuntimeError("Faltan comandos para ejecutar FrameBridge Original: " + ", ".join(missing))
    state, home, env = prepare(args)
    records = load_record(state)
    if any(live(r) for r in records.get("processes", [])):
        raise RuntimeError("Este perfil ya está ejecutándose; usa status o stop.")
    for port in (args.port, args.hub_port):
        with socket.socket() as probe:
            probe.bind(("127.0.0.1", port))
    commands = [
        ("adapter", [sys.executable, str(PREFIX / LAYOUT["adapter"]), "serve"]),
        ("fleet", [sys.executable, str(PREFIX / "original/geranas/FrameBridge-Fleet-Hub/app/fleet_hub_mcp_server.py")]),
        ("mcp", [sys.executable, str(PREFIX / "portable/front_door_mcp.py")]),
    ]
    record = {"state_dir": str(state), "mcp_url": "http://127.0.0.1:" + str(args.port) + "/mcp",
              "port": args.port, "hub_port": args.hub_port, "processes": []}
    try:
        for role, command in commands:
            log = (state / "logs" / (role + ".log")).open("ab")
            proc = subprocess.Popen(command, env=env, cwd=home, stdin=subprocess.DEVNULL,
                                    stdout=log, stderr=subprocess.STDOUT, start_new_session=True, umask=0o077)
            log.close()
            record["processes"].append({"role": role, "pid": proc.pid, "birth": birth(proc.pid)})
        (state / "processes.json").write_text(json.dumps(record, indent=2) + "\n")
        (state / "processes.json").chmod(0o600)
        deadline = time.monotonic() + 15
        while time.monotonic() < deadline:
            if not all(live(r) for r in record["processes"]):
                raise RuntimeError("Falló un proceso. Consulta los logs del perfil: " + str(state / "logs"))
            ready = (Path(env["FRAMEBRIDGE_TOOL_ROOT"]) / "adapter.sock").is_socket()
            for port in (args.port, args.hub_port):
                try:
                    with socket.create_connection(("127.0.0.1", port), timeout=.2):
                        pass
                except OSError:
                    ready = False
            if ready:
                print("FrameBridge Original iniciado: " + record["mcp_url"])
                print("Conexión a ChatGPT: manual; usa tunnel --help y las instrucciones del paquete.")
                print("Perfil local: " + str(state))
                return
            time.sleep(.15)
        raise RuntimeError("No se completó el arranque; consulta los logs del perfil.")
    except BaseException:
        stop(args)
        raise

def status(args):
    state, home, env = settings(args)
    record = load_record(state)
    print(json.dumps({"state_dir": str(state), "mcp_url": record.get("mcp_url"),
        "processes": [{"role": x["role"], "pid": x["pid"], "running": live(x)}
                      for x in record.get("processes", [])]}, indent=2))

def doctor(args):
    state, home, env = settings(args)
    required = CORE_COMMANDS
    gui = ["xdotool", "xwininfo", "xclip", "import", "convert", "identify", "xterm"]
    imports = subprocess.run([sys.executable, "-c", "import mcp,pydantic_core,cryptography"], env=env, capture_output=True)
    report = {"python": sys.version.split()[0], "python_abi_expected": "3.13",
              "mcp_imports_ok": imports.returncode == 0,
              "core_commands": {x: bool(shutil.which(x)) for x in required},
              "cua_commands": {x: bool(shutil.which(x)) for x in gui},
              "display": os.environ.get("DISPLAY", ""),
              "tunnel_sha256": hashlib.sha256((PREFIX / "original/geranas/FrameBridge-MCP-Private/bin/tunnel-client").read_bytes()).hexdigest()}
    print(json.dumps(report, indent=2))
    return 0 if sys.version_info[:2] == (3, 13) and imports.returncode == 0 and all(report["core_commands"].values()) else 1

def main():
    parser = argparse.ArgumentParser(description="FrameBridge Original: ejecución local, CUA y conexión manual a ChatGPT.")
    parser.add_argument("--state-dir", default=str(DEFAULT_STATE))
    parser.add_argument("--port", type=int, default=19865)
    parser.add_argument("--hub-port", type=int, default=19877)
    parser.add_argument("--broker-port", type=int, default=19878)
    sub = parser.add_subparsers(dest="command", required=True)
    for name in ["start", "stop", "status", "doctor"]:
        sub.add_parser(name)
    tunnel = sub.add_parser("tunnel", help="Ejecuta el cliente de túnel original; configuración manual independiente.")
    tunnel.add_argument("arguments", nargs=argparse.REMAINDER)
    args = parser.parse_args()
    if args.command == "tunnel":
        state, home, env = prepare(args)
        rest = args.arguments
        if rest[:1] == ["--"]:
            rest = rest[1:]
        explicit_refs = {x[4:] for x in rest if x.startswith("env:")}
        for x in rest:
            if "=env:" in x:
                explicit_refs.add(x.split("=env:", 1)[1])
        for key in list(env):
            if (key.startswith(("TUNNEL_CLIENT_", "CONTROL_PLANE_", "OPENAI_", "MCP_", "HEALTH_", "LOG_", "CLOUDFLARED_", "HARPOON_"))
                    or key in {"TUNNEL_ID", "TUNNEL_BIN", "CRED_FILE", "OPEN_WEB_UI", "PID_FILE",
                               "ALLOW_REMOTE_UI", "ADMIN_UI_LOG_BUFFER_EVENTS", "PROXY_CHECK_INTERVAL"}):
                if key not in explicit_refs:
                    env.pop(key, None)
        os.execve(str(PREFIX / "original/geranas/FrameBridge-MCP-Private/bin/tunnel-client"),
                  ["tunnel-client"] + rest, env)
    result = globals()[args.command](args)
    return result or 0

if __name__ == "__main__":
    try:
        sys.exit(main())
    except (OSError, ValueError, RuntimeError) as exc:
        print(str(exc), file=sys.stderr)
        sys.exit(1)
