"""Core/gateway/OTEL process lifecycle and Docker operations for CLI.

All subprocess-based Docker invocations are in clearly named functions
so they can be replaced with Docker SDK for Python in a single place.
"""

import os
import subprocess
import sys
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from armorer.config import ConfigManager
from armorer import docker_runner

from armorer.cli.common import armorer_config_dir, console, docker_daemon_unavailable, probe_docker_daemon

_CORE_PROVIDER_ENV_KEYS = {
    "openai": "OPENAI_API_KEY",
    "anthropic": "ANTHROPIC_API_KEY",
    "gemini": "GEMINI_API_KEY",
    "openrouter": "OPENROUTER_API_KEY",
}


def inspect_named_container(name: str) -> Dict[str, Any]:
    """Return container state for the given name (exists, running, status)."""
    result = docker_runner.docker_ps(
        args=[
            "-a",
            "--filter",
            f"name=^{name}$",
            "--format",
            "{{.Names}}\t{{.Status}}",
        ]
    )
    if not result.ok:
        return {"exists": False, "running": False, "status": ""}
    for raw_line in (result.stdout or "").splitlines():
        line = raw_line.strip()
        if not line:
            continue
        container_name, _, status = line.partition("\t")
        if container_name.strip() != name:
            continue
        status_text = status.strip()
        return {
            "exists": True,
            "running": status_text.startswith("Up"),
            "status": status_text,
        }
    return {"exists": False, "running": False, "status": ""}


def is_pid_alive(pid: int) -> bool:
    if pid <= 0:
        return False
    try:
        os.kill(pid, 0)
        return True
    except Exception:
        return False


def list_running_telegram_gateway_pids(exclude_pid: Optional[int] = None) -> List[int]:
    current_pid = int(exclude_pid or 0)
    try:
        result = subprocess.run(
            ["ps", "-axo", "pid=,args="],
            capture_output=True,
            text=True,
        )
    except Exception:
        return []
    if result.returncode != 0:
        return []
    pids: List[int] = []
    for raw_line in (result.stdout or "").splitlines():
        line = raw_line.strip()
        if not line:
            continue
        pid_text, _, args = line.partition(" ")
        try:
            pid = int(pid_text.strip())
        except Exception:
            continue
        normalized_args = " ".join(args.split())
        if "-m armorer.main gateway telegram" not in normalized_args:
            continue
        if current_pid and pid == current_pid:
            continue
        pids.append(pid)
    return pids


def list_running_host_core_pids(exclude_pid: Optional[int] = None) -> List[int]:
    current_pid = int(exclude_pid or 0)
    try:
        result = subprocess.run(
            ["ps", "-axo", "pid=,args="],
            capture_output=True,
            text=True,
        )
    except Exception:
        return []
    if result.returncode != 0:
        return []
    pids: List[int] = []
    for raw_line in (result.stdout or "").splitlines():
        line = raw_line.strip()
        if not line:
            continue
        pid_text, _, args = line.partition(" ")
        try:
            pid = int(pid_text.strip())
        except Exception:
            continue
        normalized_args = " ".join(args.split())
        if "src/armorer/core.py" not in normalized_args:
            continue
        if current_pid and pid == current_pid:
            continue
        pids.append(pid)
    return pids


@contextmanager
def advisory_lock(lock_path: Path):
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    handle = lock_path.open("a+")
    try:
        try:
            import fcntl

            fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
        except Exception:
            pass
        yield
    finally:
        try:
            try:
                import fcntl

                fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
            except Exception:
                pass
            handle.close()
        except Exception:
            pass


def release_owned_pid_file(pid_file: Path, pid: int) -> None:
    try:
        if pid_file.exists() and pid_file.read_text().strip() == str(pid):
            pid_file.unlink()
    except Exception:
        pass


def claim_telegram_gateway_singleton(config_dir: Path, *, pid: Optional[int] = None) -> Tuple[bool, Optional[int]]:
    owner_pid = int(pid or os.getpid())
    pid_file = config_dir / "gateway-telegram.pid"
    lock_file = config_dir / "gateway-telegram.lock"

    with advisory_lock(lock_file):
        if pid_file.exists():
            try:
                existing_pid = int(pid_file.read_text().strip() or "0")
            except Exception:
                existing_pid = 0
            if existing_pid and existing_pid != owner_pid and is_pid_alive(existing_pid):
                return False, existing_pid
            if existing_pid != owner_pid:
                try:
                    pid_file.unlink()
                except Exception:
                    pass

        running_pids = list_running_telegram_gateway_pids(exclude_pid=owner_pid)
        if running_pids:
            active_pid = running_pids[0]
            try:
                pid_file.write_text(str(active_pid))
            except Exception:
                pass
            return False, active_pid

        pid_file.write_text(str(owner_pid))
        return True, owner_pid


def core_provider_env(config_manager: ConfigManager) -> Dict[str, str]:
    provider = (config_manager.get_key("MODEL_PROVIDER") or "openai").strip().lower() or "openai"
    env = {
        "MODEL_PROVIDER": provider,
        "Armorer_MODEL": config_manager.get_key("Armorer_MODEL") or "",
    }
    if provider == "ollama":
        base_url = config_manager.get_key("OLLAMA_BASE_URL")
        if base_url:
            env["OLLAMA_BASE_URL"] = base_url
        return env
    key_name = _CORE_PROVIDER_ENV_KEYS.get(provider)
    if key_name:
        env[key_name] = config_manager.get_key(key_name) or ""
    return env


def ensure_host_core_process() -> bool:
    """Ensure a host Python Armorer core process is running."""
    config_dir = armorer_config_dir()
    config_dir.mkdir(parents=True, exist_ok=True)
    pid_file = config_dir / "core-host.pid"
    log_file = config_dir / "core-host.log"
    lock_file = config_dir / "core-host.lock"

    with advisory_lock(lock_file):
        if pid_file.exists():
            try:
                pid = int(pid_file.read_text().strip() or "0")
            except Exception:
                pid = 0
            if is_pid_alive(pid):
                return True
            try:
                pid_file.unlink()
            except Exception:
                pass

        running_pids = list_running_host_core_pids()
        if running_pids:
            try:
                pid_file.write_text(str(running_pids[0]))
            except Exception:
                pass
            return True

        env = os.environ.copy()
        env.setdefault("PYTHONPATH", str(Path.cwd() / "src"))
        with open(log_file, "a") as logh:
            proc = subprocess.Popen(
                [sys.executable, "src/armorer/core.py"],
                stdout=logh,
                stderr=logh,
                stdin=subprocess.DEVNULL,
                env=env,
                start_new_session=True,
                cwd=str(Path.cwd()),
            )
        pid_file.write_text(str(proc.pid))
    return True


def _proactive_settings() -> Dict[str, Any]:
    from armorer.cli import telegram_admin as telegram_admin_cli

    return telegram_admin_cli.proactive_settings(armorer_config_dir())


def ensure_telegram_gateway_autostart() -> None:
    if os.path.exists("/.dockerenv"):
        return
    if os.getenv("ARMORER_DISABLE_GATEWAY_AUTOSTART") == "1":
        return
    cm = ConfigManager()
    enabled = (cm.get_key("ARMORER_GATEWAY_TELEGRAM_ENABLED") or "true").strip().lower()
    if enabled in {"0", "false", "off", "no"}:
        return
    token = cm.get_key("TELEGRAM_BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
    if not token:
        return
    config_dir = armorer_config_dir()
    try:
        config_dir.mkdir(parents=True, exist_ok=True)
    except PermissionError:
        console.print("[dim]Skipping Telegram gateway autostart: Armorer config dir is not writable.[/dim]")
        return
    pid_file = config_dir / "gateway-telegram.pid"
    log_file = config_dir / "gateway-telegram.log"
    lock_file = config_dir / "gateway-telegram.lock"

    with advisory_lock(lock_file):
        if pid_file.exists():
            try:
                pid = int(pid_file.read_text().strip() or "0")
            except Exception:
                pid = 0
            if is_pid_alive(pid):
                return
            try:
                pid_file.unlink()
            except Exception:
                pass

        running_pids = list_running_telegram_gateway_pids()
        if running_pids:
            try:
                pid_file.write_text(str(running_pids[0]))
            except Exception:
                pass
            return

        env = os.environ.copy()
        env["ARMORER_DISABLE_GATEWAY_AUTOSTART"] = "1"
        env["TELEGRAM_BOT_TOKEN"] = token
        env.setdefault("PYTHONPATH", str(Path.cwd() / "src"))
        try:
            with open(log_file, "a") as logh:
                proc = subprocess.Popen(
                    [sys.executable, "-m", "armorer.main", "gateway", "telegram"],
                    stdout=logh,
                    stderr=logh,
                    stdin=subprocess.DEVNULL,
                    env=env,
                    start_new_session=True,
                    cwd=str(Path.cwd()),
                )
            pid_file.write_text(str(proc.pid))
        except PermissionError:
            console.print("[dim]Skipping Telegram gateway autostart: Armorer config dir is not writable.[/dim]")
            return
    console.print("[dim]📡 Telegram gateway auto-started.[/dim]")


def write_otel_collector_config(config_dir: Path) -> Path:
    otel_dir = config_dir / "otel"
    otel_dir.mkdir(parents=True, exist_ok=True)
    events_dir = config_dir / "otel_events"
    events_dir.mkdir(parents=True, exist_ok=True)
    try:
        os.chmod(events_dir, 0o777)
    except Exception:
        pass
    cfg_path = otel_dir / "collector-config.yaml"
    cfg = """
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
processors:
  batch: {}
exporters:
  file/armorer:
    path: /var/lib/armorer-otel/otel-events.jsonl
service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [file/armorer]
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [file/armorer]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [file/armorer]
""".strip()
    cfg_path.write_text(cfg + "\n")
    return cfg_path


def ensure_otel_bridge_autostart() -> None:
    if os.path.exists("/.dockerenv"):
        return
    if os.getenv("ARMORER_DISABLE_OTEL_AUTOSTART") == "1":
        return
    cm = ConfigManager()
    enabled = (cm.get_key("ARMORER_OTEL_COLLECTOR_ENABLED") or "true").strip().lower()
    if enabled in {"0", "false", "off", "no"}:
        return
    config_dir = armorer_config_dir()
    try:
        cfg_path = write_otel_collector_config(config_dir)
    except PermissionError:
        console.print("[dim]Skipping OTEL bridge autostart: Armorer config dir is not writable.[/dim]")
        return
    events_dir = config_dir / "otel_events"
    port = int((cm.get_key("ARMORER_OTEL_COLLECTOR_PORT") or "4318").strip() or "4318")
    image = (cm.get_key("ARMORER_OTEL_COLLECTOR_IMAGE") or "otel/opentelemetry-collector-contrib:0.120.0").strip()
    name = "armorer-otel-collector"

    ps = subprocess.run(
        ["docker", "ps", "--filter", f"name={name}", "--format", "{{.Names}}"],
        capture_output=True,
        text=True,
    )
    if name in (ps.stdout or ""):
        return

    docker_runner.docker_rm([name])
    run_cmd = [
        "docker", "run", "-d",
        "--name", name,
        "--restart", "unless-stopped",
        "--user", "0:0",
        "-p", f"127.0.0.1:{port}:4318",
        "-v", f"{cfg_path}:/etc/otelcol-contrib/config.yaml:ro",
        "-v", f"{events_dir}:/var/lib/armorer-otel",
        image,
        "--config=/etc/otelcol-contrib/config.yaml",
    ]
    result = subprocess.run(run_cmd, capture_output=True, text=True)
    if result.returncode != 0:
        console.print(f"[yellow]⚠️ OTEL bridge autostart failed: {(result.stderr or result.stdout).strip()}[/yellow]")
        return
    cm.set_key("ARMORER_OTEL_ENDPOINT", f"http://127.0.0.1:{port}")
    console.print("[dim]📈 OTEL collector bridge auto-started.[/dim]")


def ensure_self_docker_image(project_root: Optional[Path] = None, *, force_rebuild: Optional[bool] = None) -> bool:
    root = Path(project_root or Path.cwd())
    rebuild = force_rebuild
    if rebuild is None:
        rebuild = str(os.getenv("ARMORER_REBUILD_IMAGE", "")).strip().lower() in {"1", "true", "yes", "on"}

    inspect_result = subprocess.run(
        ["docker", "image", "inspect", "armorer-cli"],
        capture_output=True,
        text=True,
    )
    if inspect_result.returncode == 0 and not rebuild:
        return True

    build_result = subprocess.run(
        ["docker", "build", "-t", "armorer-cli", "."],
        cwd=root,
        capture_output=True,
        text=True,
    )
    if build_result.returncode == 0:
        return True
    detail = ((build_result.stderr or "") + "\n" + (build_result.stdout or "")).strip()
    console.print("[yellow]Failed to build Armorer sandbox image.[/yellow]")
    if detail:
        console.print(f"[dim]{detail[:600]}[/dim]")
    return False


def best_effort_core_autostarts() -> None:
    for action in (ensure_otel_bridge_autostart, ensure_telegram_gateway_autostart):
        try:
            action()
        except Exception:
            continue


def ensure_armorer_core() -> None:
    """Ensure the Armorer Core background service is running and healthy."""
    if os.path.exists("/.dockerenv"):
        return

    core_name = "armorer-core"
    is_dev = os.getenv("ARMORER_DEV") == "1"

    host_pid = armorer_config_dir() / "core-host.pid"
    if host_pid.exists():
        try:
            host_core_pid = int(host_pid.read_text().strip() or "0")
        except Exception:
            host_core_pid = 0
        if is_pid_alive(host_core_pid):
            best_effort_core_autostarts()
            return

    core_state = inspect_named_container(core_name)
    if core_state.get("running") and not is_dev:
        best_effort_core_autostarts()
        return
    if core_state.get("exists"):
        remove_result = docker_runner.docker_rm([core_name])
        if not remove_result.ok:
            detail = (remove_result.stderr or remove_result.stdout).strip()
            console.print(f"[bold red]Failed to remove stale '{core_name}' container.[/bold red]")
            if detail:
                console.print(f"[dim]{detail[:600]}[/dim]")
            raise SystemExit(1)

    project_root = Path.cwd()
    host_root = os.getenv("HOST_PROJECT_ROOT", str(project_root))
    host_config = os.getenv("HOST_ARMORER_CONFIG", str(armorer_config_dir()))
    host_installed = str(Path(host_root) / "installed")

    if not ensure_self_docker_image(force_rebuild=is_dev):
        console.print("[yellow]⚠ Docker image build failed. Falling back to host core process.[/yellow]")
        ensure_host_core_process()
        best_effort_core_autostarts()
        return

    docker_version = "1.41"
    try:
        ver_res = subprocess.run(
            ["docker", "version", "--format", "{{.Server.APIVersion}}"],
            capture_output=True,
            text=True,
        )
        if ver_res.returncode == 0:
            docker_version = ver_res.stdout.strip()
    except Exception:
        pass

    config_manager = ConfigManager()
    provider_env = core_provider_env(config_manager)
    proactive = _proactive_settings()

    with console.status("[bold cyan]Starting Armorer Core Service..."):
        cmd = [
            "docker", "run", "-d", "--name", core_name,
            "-v", "/var/run/docker.sock:/var/run/docker.sock",
            "-v", f"{project_root / 'src'}:/app/src:ro",
            "-v", f"{host_installed}:/app/installed:rw",
            "-v", f"{host_config}:/root/.armorer:rw",
            "-e", f"DOCKER_API_VERSION={docker_version}",
            "-e", f"HOST_PROJECT_ROOT={host_root}",
            "-e", f"MODEL_PROVIDER={provider_env.get('MODEL_PROVIDER', 'openai')}",
            "-e", f"Armorer_MODEL={provider_env.get('Armorer_MODEL', '')}",
            "-e", f"LANGFUSE_ENABLED={config_manager.get_key('LANGFUSE_ENABLED') or 'false'}",
            "-e", f"LANGFUSE_BASE_URL={config_manager.get_key('LANGFUSE_BASE_URL') or 'https://cloud.langfuse.com'}",
            "-e", f"LANGFUSE_PUBLIC_KEY={config_manager.find_vault_key('LANGFUSE_PUBLIC_KEY') or ''}",
            "-e", f"LANGFUSE_SECRET_KEY={config_manager.find_vault_key('LANGFUSE_SECRET_KEY') or ''}",
            "-e", f"TELEGRAM_BOT_TOKEN={config_manager.find_vault_key('TELEGRAM_BOT_TOKEN') or ''}",
            "-e", f"TELEGRAM_ALERT_CHAT_ID={proactive.get('telegram_alert_chat_id') or config_manager.get_key('TELEGRAM_ALERT_CHAT_ID') or ''}",
            "-e", f"ARMORER_TELEGRAM_ADMIN_CHAT_IDS={','.join(proactive.get('telegram_admin_chat_ids', []) or [])}",
            "-e", f"ARMORER_TELEGRAM_ADMIN_USER_IDS={','.join(proactive.get('telegram_admin_user_ids', []) or [])}",
            "-e", f"ARMORER_TELEGRAM_ALLOW_UNCONFIGURED_ADMINS={str(bool(proactive.get('telegram_allow_unconfigured_admins', False))).lower()}",
            "-e", f"ARMORER_CONFIRM_TTL_S={config_manager.get_key('ARMORER_CONFIRM_TTL_S') or '60'}",
            "-e", "BYPASS_TOOL_CONSENT=true",
        ]
        for key_name in ("OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "OLLAMA_BASE_URL"):
            if key_name in provider_env:
                cmd.extend(["-e", f"{key_name}={provider_env[key_name]}"])
        if os.path.isabs(host_root):
            cmd.extend(["-v", f"{host_root}:{host_root}:rw"])

        ollama_url = provider_env.get("OLLAMA_BASE_URL")
        if ollama_url:
            import platform
            if "localhost" in ollama_url.lower() or "127.0.0.1" in ollama_url.lower():
                if platform.system() in ["Darwin", "Windows"]:
                    ollama_url = ollama_url.replace("localhost", "host.docker.internal").replace("127.0.0.1", "host.docker.internal")
            cmd.extend(["-e", f"OLLAMA_BASE_URL={ollama_url}"])

        cmd += [
            "-e", "PYTHONPATH=/app/src",
            "--entrypoint", "python",
            "armorer-cli", "src/armorer/core.py",
        ]

        try:
            run_result = subprocess.run(cmd, capture_output=True, timeout=25)
        except subprocess.TimeoutExpired:
            console.print("[yellow]⚠ Core container startup timed out. Falling back to host core process.[/yellow]")
            ensure_host_core_process()
            best_effort_core_autostarts()
            return
        if run_result.returncode != 0:
            console.print("[yellow]⚠ Failed to start core container. Falling back to host core process.[/yellow]")
            err = run_result.stderr.decode() if run_result.stderr else (run_result.stdout.decode() if run_result.stdout else "Unknown error")
            console.print(f"[dim]{err.strip()}[/dim]")
            ensure_host_core_process()
            best_effort_core_autostarts()
            return
        console.print("[dim]🛡️  Armorer Core Service is active.[/dim]")
    best_effort_core_autostarts()


def check_self_dockerize() -> None:
    """Secure-by-default: run Armorer inside Docker unless explicitly disabled."""
    if os.path.exists("/.dockerenv"):
        return
    raw_pref = str(os.getenv("ARMORER_SANDBOX", "")).strip().lower()
    if raw_pref in {"0", "false", "off", "no", "host"}:
        return

    args = sys.argv[1:]
    project_root = Path.cwd()
    armorer_config = armorer_config_dir()
    armorer_config.mkdir(parents=True, exist_ok=True)

    try:
        exec_cmd: Optional[List[str]] = None
        with console.status("[bold cyan]🛡️  Armorer is entering the sandbox (secure default)..."):
            daemon_check = probe_docker_daemon()
            if daemon_check is not None:
                raise RuntimeError(daemon_check)
            force_rebuild = str(os.getenv("ARMORER_SANDBOX_REBUILD", "")).strip().lower() in {"1", "true", "yes", "on"}
            sandbox_name = os.getenv("ARMORER_SANDBOX_NAME", "armorer-sandbox")

            def _container_state(name: str) -> Optional[str]:
                inspect = subprocess.run(
                    ["docker", "inspect", "-f", "{{.State.Status}}", name],
                    capture_output=True,
                    text=True,
                )
                if inspect.returncode != 0:
                    return None
                return (inspect.stdout or "").strip() or None

            image_present = subprocess.run(
                ["docker", "image", "inspect", "armorer-cli"],
                capture_output=True,
                text=True,
            ).returncode == 0
            if force_rebuild:
                subprocess.run(["docker", "rm", "-f", sandbox_name], capture_output=True, text=True)
            if force_rebuild or not image_present:
                console.print("[dim]Preparing sandbox image 'armorer-cli'...[/dim]")
                subprocess.run(["docker", "build", "-t", "armorer-cli", "."], cwd=project_root, check=True)
            else:
                console.print("[dim]Reusing existing sandbox image 'armorer-cli'.[/dim]")

            state = _container_state(sandbox_name)
            if state != "running":
                if state is not None:
                    subprocess.run(["docker", "rm", "-f", sandbox_name], capture_output=True, text=True)
                installed_root = project_root / "installed"
                installed_root.mkdir(parents=True, exist_ok=True)
                run_cmd = [
                    "docker", "run", "-d",
                    "--name", sandbox_name,
                    "--restart", "unless-stopped",
                    "-v", "/var/run/docker.sock:/var/run/docker.sock",
                    "-v", f"{project_root}:/app",
                    "-v", f"{installed_root}:/app/installed",
                    "-v", f"{project_root}:{project_root}",
                    "-v", f"{armorer_config}:/root/.armorer",
                    "-w", "/app",
                    "-e", f"HOST_PROJECT_ROOT={project_root}",
                    "-e", f"HOST_ARMORER_CONFIG={armorer_config}",
                    "-e", "BYPASS_TOOL_CONSENT=true",
                    "-e", "PYTHONPATH=/app/src",
                    "--entrypoint", "sh",
                    "armorer-cli",
                    "-lc", "while true; do sleep 3600; done",
                ]
                subprocess.run(run_cmd, check=True, capture_output=True, text=True)
                console.print(f"[dim]Started persistent sandbox container '{sandbox_name}'.[/dim]")
            else:
                console.print(f"[dim]Using persistent sandbox container '{sandbox_name}'.[/dim]")

            exec_cmd = ["docker", "exec"]
            if sys.stdin.isatty():
                exec_cmd.append("-it")
            elif sys.stdout.isatty():
                exec_cmd.append("-t")
            else:
                exec_cmd.append("-i")
            exec_cmd.extend([sandbox_name, "python", "src/armorer/main.py"])
            exec_cmd.extend(args)
        console.print()
        if exec_cmd:
            os.execvp("docker", exec_cmd)
    except Exception as e:
        from armorer.messaging.bridge import _sanitize_bridge_user_text

        safe_detail = _sanitize_bridge_user_text(str(e).strip() or e.__class__.__name__)[:300]
        if docker_daemon_unavailable(safe_detail):
            console.print("[bold yellow]Docker is not running or unreachable.[/bold yellow]")
            console.print("[yellow]Start Docker Desktop and try again.[/yellow]")
            console.print("[yellow]On macOS, run:[/yellow] [dim]open -a Docker[/dim]")
            console.print("[yellow]Wait until Docker is running (the whale icon is steady) and rerun your command.[/yellow]")
            console.print("[bold red]Refusing insecure host fallback. Set ARMORER_SANDBOX=0 only if you intentionally want host mode.[/bold red]")
        else:
            console.print(f"[bold red]Failed to start Dockerized Armorer: {safe_detail}[/bold red]")
            console.print("[bold red]Refusing insecure host fallback. Set ARMORER_SANDBOX=0 only if you intentionally want host mode.[/bold red]")
        raise SystemExit(1)
