# DOCS_REF:
# - docs/user/reference/env-vars.md
# - docs/user/01-quickstart-local.md
# DOCS_SYNC:
# - If boot env vars/defaults change, update env-vars reference and local
#   quickstart guidance.

from __future__ import annotations

from dataclasses import dataclass
import os
import socket
import subprocess
import sys
import threading
import time
from typing import Tuple

from flownet.config import RuntimeConfigInput


NODE_MODE_ENV = "FLOWNET_NODE_MODE"
NODE_ADDR_ENV = "FLOWNET_NODE_ADDR"
NODE_ID_ENV = "FLOWNET_NODE_ID"
NODE_THREADS_ENV = "FLOWNET_NODE_THREADS"
RUNTIME_ROLE_ENV = "FLOWNET_RUNTIME_ROLE"
TRANSPORT_MODE_ENV = "FLOWNET_TRANSPORT_MODE"
LOCAL_IPC_DIR_ENV = "FLOWNET_LOCAL_IPC_DIR"

DEFAULT_NODE_MODE = "auto_start_user"
DEFAULT_NODE_ADDRESS = "127.0.0.1:8787"

_node_runtime_process: subprocess.Popen | None = None
_auto_boot_lock = threading.Lock()
_auto_booted = False


@dataclass(frozen=True)
class BootConfig:
    node_mode: str = DEFAULT_NODE_MODE
    node_address: str = DEFAULT_NODE_ADDRESS
    node_id: str | None = None
    node_threads: int | None = None
    connect_timeout: float = 5.0


def boot_user_runtime(
    config: RuntimeConfigInput | None = None,
    *,
    node_mode: str | None = None,
    node_address: str | None = None,
    node_id: str | None = None,
    node_threads: int | None = None,
    connect_timeout: float | None = None,
):
    if config is not None:
        import flownet.api
        flownet.api.configure_runtime(config)

    resolved = _resolve_boot_config(
        node_mode=node_mode,
        node_address=node_address,
        node_id=node_id,
        node_threads=node_threads,
        connect_timeout=connect_timeout,
    )
    _persist_boot_env(resolved)
    ensure_node_runtime(resolved)
    global _auto_booted
    _auto_booted = True
    from flownet.runtime.runtime import get_runtime
    runtime = get_runtime()
    return runtime


def auto_boot_user_runtime(
    *,
    node_mode: str | None = None,
    node_address: str | None = None,
    node_id: str | None = None,
    node_threads: int | None = None,
    connect_timeout: float | None = None,
) -> None:
    global _auto_booted
    if _auto_booted:
        return
    with _auto_boot_lock:
        if _auto_booted:
            return
        role = os.environ.get(RUNTIME_ROLE_ENV, "user")
        if role != "user":
            _auto_booted = True
            return
        resolved = _resolve_boot_config(
            node_mode=node_mode,
            node_address=node_address,
            node_id=node_id,
            node_threads=node_threads,
            connect_timeout=connect_timeout,
        )
        _persist_boot_env(resolved)
        if resolved.node_mode == "disabled":
            _auto_booted = True
            return
        ensure_node_runtime(resolved)
        _auto_booted = True


def ensure_node_runtime(config: BootConfig) -> None:
    if config.node_mode == "disabled":
        return
    if _try_connect(config.node_address):
        return

    if config.node_mode == "connect_only":
        raise RuntimeError(f"Node runtime not available at {config.node_address}")
    if config.node_mode == "auto_start_user":
        _start_node_runtime_process(config)
        if not _wait_for_node_runtime(config.node_address, config.connect_timeout):
            raise RuntimeError(f"Node runtime failed to start at {config.node_address}")
        return
    if config.node_mode == "auto_start_root":
        raise RuntimeError(
            "auto_start_root is not supported in user startup. "
            "Use systemd/launchd or a privileged supervisor instead."
        )

    raise ValueError(f"Unknown node mode: {config.node_mode}")


def _resolve_boot_config(
    *,
    node_mode: str | None,
    node_address: str | None,
    node_id: str | None,
    node_threads: int | None,
    connect_timeout: float | None,
) -> BootConfig:
    resolved_mode = node_mode or os.environ.get(NODE_MODE_ENV) or DEFAULT_NODE_MODE
    resolved_address = node_address or os.environ.get(NODE_ADDR_ENV) or DEFAULT_NODE_ADDRESS
    resolved_node_id = node_id or os.environ.get(NODE_ID_ENV)
    resolved_threads = node_threads
    if resolved_threads is None:
        env_threads = os.environ.get(NODE_THREADS_ENV)
        resolved_threads = int(env_threads) if env_threads else None
    resolved_timeout = connect_timeout if connect_timeout is not None else 5.0
    return BootConfig(
        node_mode=resolved_mode,
        node_address=resolved_address,
        node_id=resolved_node_id,
        node_threads=resolved_threads,
        connect_timeout=resolved_timeout,
    )


def _persist_boot_env(config: BootConfig) -> None:
    # Keep effective node attach context visible to runtime APIs that need
    # a default control-plane target (for example actor deployment).
    os.environ[NODE_MODE_ENV] = config.node_mode
    os.environ[NODE_ADDR_ENV] = config.node_address
    if config.node_id:
        os.environ[NODE_ID_ENV] = config.node_id
    if config.node_threads is not None:
        os.environ[NODE_THREADS_ENV] = str(config.node_threads)


def _split_address(address: str) -> Tuple[str, int]:
    host, port_str = address.rsplit(":", 1)
    return host, int(port_str)


def _try_connect(address: str) -> bool:
    try:
        host, port = _split_address(address)
        with socket.create_connection((host, port), timeout=0.2):
            return True
    except Exception:
        return False


def _wait_for_node_runtime(address: str, timeout: float) -> bool:
    deadline = time.time() + timeout
    while time.time() < deadline:
        if _try_connect(address):
            return True
        time.sleep(0.1)
    return False


def _start_node_runtime_process(config: BootConfig) -> None:
    global _node_runtime_process
    if _node_runtime_process is not None and _node_runtime_process.poll() is None:
        return

    host, port = _split_address(config.node_address)
    args = [
        sys.executable,
        "-u",
        "-m",
        "flownet.platform.start_node_runtime",
        f"--host={host}",
        f"--port={port}",
    ]
    if config.node_threads is not None:
        args.append(f"--threads={config.node_threads}")
    if config.node_id:
        args.append(f"--node-id={config.node_id}")
    transport_mode = os.environ.get(TRANSPORT_MODE_ENV)
    if transport_mode:
        args.append(f"--transport-mode={transport_mode}")
    local_ipc_dir = os.environ.get(LOCAL_IPC_DIR_ENV)
    if local_ipc_dir:
        args.append(f"--local-ipc-dir={local_ipc_dir}")

    env = os.environ.copy()
    _ensure_pythonpath(env)
    _node_runtime_process = subprocess.Popen(args, env=env)


def _ensure_pythonpath(env: dict[str, str]) -> None:
    try:
        import pathlib
        pkg_root = pathlib.Path(__file__).resolve().parents[4]
        existing = env.get("PYTHONPATH", "")
        paths = [p for p in existing.split(os.pathsep) if p]
        if str(pkg_root) not in paths:
            env["PYTHONPATH"] = os.pathsep.join([str(pkg_root)] + paths)
    except Exception:
        return


__all__ = [
    "BootConfig",
    "boot_user_runtime",
    "ensure_node_runtime",
    "auto_boot_user_runtime",
    "DEFAULT_NODE_ADDRESS",
]
