"""
QUANTUM VELOCITY K5 — Named Pipe / Unix Domain Socket IPC Fast-Path
====================================================================

Provides sub-millisecond local inter-process communication between the NAVIG
CLI, the Daemon, and the navig-bridge VS Code extension — replacing slow JSON-RPC
HTTP polling for the local-only hot path.

Architecture:
    CLI ──[ Named Pipe / UDS ]──► Daemon
         (< 1ms)            ──► WebSocket JSON-RPC (existing, fallback)
                              Shadow comparison on first 100 calls

Platform:
    Windows:    ``\\\\.\\pipe\\navig-daemon-ipc`` (multiprocessing.connection)
    Linux/macOS: /tmp/navig-daemon-{uid}.sock (Unix Domain Socket)

Shadow Execution Protocol:
    1. Send request over pipe  → receive fast result
    2. Send same request over WebSocket → receive safe result (background)
    3. Compare results — if mismatch log anomaly to ~/.navig/perf/shadow_ipc.jsonl
    4. After SHADOW_PROMOTE_AFTER consecutive matches, promote pipe as primary
       (stored in ~/.navig/.ipc_promoted flag)
    5. On ANY pipe failure, instantly fall back to WebSocket (no user-visible error)

Usage:
    # Server side (inside NavigDaemon):
    server = IPCPipeServer(handler=my_request_handler)
    server.start()  # non-blocking background thread

    # Client side (from CLI):
    client = IPCPipeClient()
    result = client.send({"cmd": "host_list"})

    # High-level shadow wrapper (recommended):
    bridge = ShadowIPCBridge(ws_url="ws://127.0.0.1:8765")
    result = bridge.call({"cmd": "host_list"})
"""

from __future__ import annotations

import json
import logging
import os
import sys
import threading
import time
from collections.abc import Callable
from pathlib import Path
from typing import Any

logger = logging.getLogger("navig.ipc_pipe")

# ─────────────────────────────────────────────────────────────────────────────
# Constants
# ─────────────────────────────────────────────────────────────────────────────

_IS_WINDOWS: bool = sys.platform == "win32"

# Number of shadow-validated calls before the pipe path is auto-promoted
SHADOW_PROMOTE_AFTER: int = 100

# Timeout for a single pipe call (ms → seconds)
IPC_TIMEOUT_S: float = 2.0

# File used to record that the pipe path has been validated and promoted
_PROMOTED_FLAG: Path = Path.home() / ".navig" / ".ipc_promoted"

# Shadow anomaly log
_SHADOW_LOG: Path = Path.home() / ".navig" / "perf" / "shadow_ipc.jsonl"

# In-memory shadow match counter (resets each process)
_shadow_match_count: int = 0
_shadow_lock = threading.Lock()


def _pipe_address() -> str:
    """Return the platform-appropriate pipe / socket address."""
    if _IS_WINDOWS:
        return r"\\.\pipe\navig-daemon-ipc"
    # Linux / macOS — use a per-UID socket so multiple users don't collide
    uid = os.getuid() if hasattr(os, "getuid") else 0
    return f"/tmp/navig-daemon-{uid}.sock"


def _is_promoted() -> bool:
    """Return True if the pipe fast-path has been promoted to primary."""
    return _PROMOTED_FLAG.exists()


def _promote_pipe() -> None:
    """Mark the pipe fast-path as validated and ready to be primary."""
    try:
        _PROMOTED_FLAG.parent.mkdir(parents=True, exist_ok=True)
        _PROMOTED_FLAG.touch()
        logger.info(
            "IPC pipe fast-path promoted to primary after %d shadow matches",
            SHADOW_PROMOTE_AFTER,
        )
    except Exception:  # noqa: BLE001
        pass  # best-effort; failure is non-critical


def _log_shadow_anomaly(event: str, data: dict) -> None:
    """Append an anomaly entry to the shadow IPC log (never raises)."""
    try:
        _SHADOW_LOG.parent.mkdir(parents=True, exist_ok=True)
        entry = {"ts": time.time(), "event": event, "data": data}
        with open(_SHADOW_LOG, "a", encoding="utf-8") as _f:
            _f.write(json.dumps(entry) + "\n")
    except Exception:  # noqa: BLE001
        pass  # best-effort; failure is non-critical


# ─────────────────────────────────────────────────────────────────────────────
# IPCPipeClient — send a request to the daemon over the local pipe/socket
# ─────────────────────────────────────────────────────────────────────────────


class IPCPipeClient:
    """
    Lightweight IPC client for the NAVIG daemon.

    Thread-safe.  Each call opens a fresh connection (persistent connections
    for CLI → daemon patterns typically offer no benefit and complicate error
    handling).
    """

    def __init__(self, address: str | None = None, timeout: float = IPC_TIMEOUT_S):
        self.address = address or _pipe_address()
        self.timeout = timeout

    # ------------------------------------------------------------------
    def send(self, payload: dict[str, Any]) -> dict[str, Any] | None:
        """
        Send *payload* to the daemon and return the parsed response dict.

        Returns None if the connection fails or the daemon is not reachable.
        Never raises (callers should fall back to WebSocket on None result).
        """
        try:
            if _IS_WINDOWS:
                return self._send_windows(payload)
            else:
                return self._send_unix(payload)
        except Exception as exc:
            logger.debug("IPC pipe send failed: %s", exc)
            return None

    def _send_windows(self, payload: dict[str, Any]) -> dict[str, Any] | None:
        from multiprocessing.connection import Client as _Client

        conn = _Client(self.address, family="AF_PIPE")
        try:
            conn.send(json.dumps(payload).encode())
            conn._handle.SetReadTimeout(int(self.timeout * 1000))  # ms
            raw = conn.recv_bytes(maxlength=4 * 1024 * 1024)  # 4 MB max
            return json.loads(raw.decode())
        finally:
            conn.close()

    def _send_unix(self, payload: dict[str, Any]) -> dict[str, Any] | None:
        import socket as _socket

        sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM)
        sock.settimeout(self.timeout)
        try:
            sock.connect(self.address)
            data = json.dumps(payload).encode()
            # Prefix with 4-byte length header (big-endian)
            sock.sendall(len(data).to_bytes(4, "big") + data)
            length_bytes = _recvall(sock, 4)
            if not length_bytes:
                return None
            length = int.from_bytes(length_bytes, "big")
            response_bytes = _recvall(sock, length)
            return json.loads(response_bytes.decode()) if response_bytes else None
        finally:
            sock.close()


def _recvall(sock, n: int) -> bytes | None:
    """Receive exactly *n* bytes from *sock*, or return None on EOF."""
    buf = bytearray()
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            return None
        buf.extend(chunk)
    return bytes(buf)


# ─────────────────────────────────────────────────────────────────────────────
# IPCPipeServer — receive requests in the daemon and dispatch to a handler
# ─────────────────────────────────────────────────────────────────────────────


class IPCPipeServer:
    """
    Background IPC server that listens on the local pipe/socket.

    Calls *handler(request_dict) → response_dict* for each incoming request.
    Runs in a daemon thread so the hosting process exits cleanly.
    """

    def __init__(
        self,
        handler: Callable[[dict[str, Any]], dict[str, Any]],
        address: str | None = None,
        backlog: int = 16,
    ):
        self.handler = handler
        self.address = address or _pipe_address()
        self.backlog = backlog
        self._thread: threading.Thread | None = None
        self._stop_event = threading.Event()

    def start(self) -> None:
        """Start the server in a background daemon thread."""
        self._thread = threading.Thread(
            target=self._serve_loop,
            name="navig-ipc-server",
            daemon=True,
        )
        self._thread.start()

    def stop(self) -> None:
        """Signal the server to stop (best-effort)."""
        self._stop_event.set()

    # ------------------------------------------------------------------
    def _serve_loop(self) -> None:
        try:
            if _IS_WINDOWS:
                self._serve_windows()
            else:
                self._serve_unix()
        except Exception as exc:
            logger.warning("IPC pipe server exiting: %s", exc)

    def _serve_windows(self) -> None:
        from multiprocessing.connection import Listener as _Listener

        with _Listener(self.address, family="AF_PIPE") as listener:
            logger.info("IPC pipe server listening on %s", self.address)
            while not self._stop_event.is_set():
                try:
                    conn = listener.accept()
                    threading.Thread(
                        target=self._handle_windows,
                        args=(conn,),
                        daemon=True,
                    ).start()
                except Exception:  # noqa: BLE001
                    pass  # best-effort; failure is non-critical

    def _handle_windows(self, conn) -> None:
        try:
            raw = conn.recv_bytes(maxlength=4 * 1024 * 1024)
            request = json.loads(raw.decode())
            response = self.handler(request)
            conn.send_bytes(json.dumps(response).encode())
        except Exception as exc:
            logger.debug("IPC handler error: %s", exc)
        finally:
            conn.close()

    def _serve_unix(self) -> None:
        import socket as _socket

        # Clean up leftover socket file
        try:
            os.unlink(self.address)
        except FileNotFoundError:
            pass  # file already gone; expected
        with _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM) as server:
            server.bind(self.address)
            try:
                os.chmod(self.address, 0o600)
            except (OSError, PermissionError):
                pass
            server.listen(self.backlog)
            server.settimeout(1.0)  # poll for stop_event every second
            logger.info("IPC unix socket listening on %s", self.address)
            while not self._stop_event.is_set():
                try:
                    conn, _ = server.accept()
                    threading.Thread(
                        target=self._handle_unix,
                        args=(conn,),
                        daemon=True,
                    ).start()
                except TimeoutError:
                    pass
                except Exception:  # noqa: BLE001
                    pass  # best-effort; failure is non-critical

    def _handle_unix(self, conn) -> None:
        try:
            with conn:
                conn.settimeout(IPC_TIMEOUT_S)
                length_bytes = _recvall(conn, 4)
                if not length_bytes:
                    return
                length = int.from_bytes(length_bytes, "big")
                raw = _recvall(conn, length)
                if not raw:
                    return
                request = json.loads(raw.decode())
                response = self.handler(request)
                data = json.dumps(response).encode()
                conn.sendall(len(data).to_bytes(4, "big") + data)
        except Exception as exc:
            logger.debug("IPC unix handler error: %s", exc)


# ─────────────────────────────────────────────────────────────────────────────
# ShadowIPCBridge — the Aegis-approved safe wrapper
# ─────────────────────────────────────────────────────────────────────────────


class ShadowIPCBridge:
    """
    🛡️ AEGIS-APPROVED Shadow Execution wrapper for the IPC fast-path.

    - Tries the Named Pipe / UDS fast-path first (<1ms).
    - Runs the legacy WebSocket path in the background for validation.
    - Accumulates shadow match statistics.
    - Auto-promotes the pipe path after SHADOW_PROMOTE_AFTER matches.
    - Falls back to WebSocket instantly if the pipe fails or mismatches.

    Usage:
        bridge = ShadowIPCBridge(ws_send_fn=my_ws_send)
        result = bridge.call({"cmd": "host_list"})
    """

    def __init__(
        self,
        ws_send_fn: Callable[[dict[str, Any]], dict[str, Any] | None] | None = None,
        pipe_address: str | None = None,
    ):
        """
        Args:
            ws_send_fn: Function that sends a request over WebSocket and returns
                        the response dict (the existing slow path). May be None
                        if shadow validation is not needed.
            pipe_address: Override the default pipe/socket address.
        """
        self._client = IPCPipeClient(address=pipe_address)
        self._ws_send = ws_send_fn
        self._promoted = _is_promoted()

    # ------------------------------------------------------------------
    def call(
        self,
        payload: dict[str, Any],
        shadow: bool = True,
    ) -> dict[str, Any] | None:
        """
        Send *payload* to the daemon using the fastest available path.

        Shadow Execution flow:
            1. Try pipe → fast_result
            2. If shadow=True and ws_send_fn provided: start background WS call
            3. Compare results asynchronously; update promotion counter
            4. Return fast_result if pipe succeeded, else WS result

        Args:
            payload: Request dict.
            shadow:  Enable shadow WS validation (disable for perf-critical loops).

        Returns:
            Response dict, or None if both paths fail.
        """
        fast_result = self._client.send(payload)

        if fast_result is not None:
            # Pipe succeeded — optionally validate in background
            if shadow and self._ws_send and not self._promoted:
                threading.Thread(
                    target=self._shadow_validate,
                    args=(payload, fast_result),
                    daemon=True,
                ).start()
            return fast_result

        # Pipe failed → fall back to WebSocket
        logger.debug("IPC pipe unavailable, falling back to WebSocket")
        if self._ws_send:
            return self._ws_send(payload)
        return None

    # ------------------------------------------------------------------
    def _shadow_validate(self, payload: dict[str, Any], fast_result: dict[str, Any]) -> None:
        """Run the WebSocket path and compare against the fast result."""
        global _shadow_match_count
        try:
            slow_result = self._ws_send(payload)
            if slow_result is None:
                return

            # Compare results (shallow key equality)
            if fast_result == slow_result:
                with _shadow_lock:
                    _shadow_match_count += 1
                    if _shadow_match_count >= SHADOW_PROMOTE_AFTER and not self._promoted:
                        _promote_pipe()
                        self._promoted = True
            else:
                # Mismatch — log anomaly, reset counter
                _log_shadow_anomaly(
                    "result_mismatch",
                    {
                        "payload_cmd": payload.get("cmd"),
                        "fast_keys": list(fast_result.keys()),
                        "slow_keys": list(slow_result.keys()),
                    },
                )
                with _shadow_lock:
                    _shadow_match_count = 0

        except Exception as exc:
            logger.debug("Shadow IPC validation error: %s", exc)


# ─────────────────────────────────────────────────────────────────────────────
# Convenience helpers
# ─────────────────────────────────────────────────────────────────────────────


def get_pipe_status() -> dict[str, Any]:
    """Return a diagnostics dict for `navig evolve status`."""
    return {
        "address": _pipe_address(),
        "promoted": _is_promoted(),
        "shadow_matches_this_session": _shadow_match_count,
        "promote_after": SHADOW_PROMOTE_AFTER,
        "platform": sys.platform,
        "shadow_log": str(_SHADOW_LOG),
    }
